-
Notifications
You must be signed in to change notification settings - Fork 67
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #560 from gmlueck/gmlueck/accordion-toc
Expandable TOC for HTML output
- Loading branch information
Showing
5 changed files
with
239 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
# Copyright (c) 2011-2024 The Khronos Group, Inc. | ||
# SPDX-License-Identifier: Apache-2.0 | ||
|
||
#require 'asciidoctor/extensions' unless RUBY_ENGINE == 'opal' | ||
RUBY_ENGINE == 'opal' ? (require 'accordion_toc/extension') : (require_relative 'accordion_toc/extension') | ||
|
||
Asciidoctor::Extensions.register do | ||
postprocessor MakeAccordionToc | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,183 @@ | ||
# Copyright (c) 2011-2024 The Khronos Group, Inc. | ||
# SPDX-License-Identifier: Apache-2.0 | ||
|
||
require 'asciidoctor/extensions' unless RUBY_ENGINE == 'opal' | ||
|
||
include ::Asciidoctor | ||
|
||
# Make the table of contents (TOC) expandable in the HTML render. A clickable | ||
# icon allows the user to expand or collapse each TOC entry. The number of TOC | ||
# levels that are initially expanded is controlled by the "toclevels-expanded" | ||
# Asciidoc attribute. For example: | ||
# | ||
# :toclevels-expanded: 1 | ||
# | ||
# expands the first level of headings in the TOC. Thus, both the first and | ||
# second heading levels are visible initially. To expand all levels initially, | ||
# use the value "-1". To leave all levels initially unexpanded, use the value | ||
# "0". | ||
# | ||
# A clickable icon is also added to the TOC title (usually "Table of Contents"). | ||
# Clicking this icon fully expands or fully collapses all TOC levels. If | ||
# "toclevels-expanded" is 0, the initial state is "collapsed" (so the first | ||
# click on the icon will fully expand all levels). If "toclevels-expanded" is | ||
# any other value, the initial state is "expanded" (so the first click on the | ||
# icon will fully collapse all levels). | ||
# | ||
# This extension also relies on some custom CSS styling. See the CSS entries | ||
# for the class name "toc-parent". | ||
|
||
class MakeAccordionToc < Extensions::Postprocessor | ||
TocStart = /^<div id="toc"/ | ||
TocTitle = /^<div id="toctitle">/ | ||
Li = /^<li>/ | ||
Ul = /^<ul (class="sectlevel\d*")>/ | ||
EndUl = /^<\/ul>/ | ||
EndHead = /^<\/head>/ | ||
|
||
# Add a click handler to the <span class="toc-parent"> elements. When the | ||
# element is clicked: | ||
# | ||
# * The class "toc-expanded" is toggled on the <span>, which changes the icon. | ||
# * The <ul> element that follows the <span> is made visible / invisible. | ||
# | ||
# Note that this script assumes that the <ul> element is two siblings beyond | ||
# the <span> that is clicked. This assumes the HTML format generated by | ||
# Asciidoctor looks like: | ||
# | ||
# <li> | ||
# <span class="toc-parent"></span> <-- <span> generated by this extension below | ||
# <a href="#...">...</a> | ||
# <ul class="..."> | ||
# | ||
# Also add a click handler to the <span id="toc-top> element. When this is | ||
# clicked, all "toc-parent" elements are toggled between expanded and | ||
# collapsed. | ||
Script = | ||
'<script> | ||
function addTocClickHandlers(){ | ||
var toc_parents = document.getElementsByClassName("toc-parent"); | ||
var toc_top = document.getElementById("toc-top"); | ||
for (element of toc_parents) { | ||
element.addEventListener("click", function() { | ||
this.classList.toggle("toc-expanded"); | ||
var ul = this.nextElementSibling.nextElementSibling; | ||
if (ul.style.display === "block") { | ||
ul.style.display = "none"; | ||
} else { | ||
ul.style.display = "block"; | ||
} | ||
}); | ||
} | ||
toc_top.addEventListener("click", function() { | ||
var is_expanded = this.classList.toggle("toc-expanded"); | ||
for (element of toc_parents) { | ||
if (is_expanded) { | ||
element.classList.add("toc-expanded"); | ||
var ul = element.nextElementSibling.nextElementSibling; | ||
ul.style.display = "block"; | ||
} | ||
else { | ||
element.classList.remove("toc-expanded"); | ||
var ul = element.nextElementSibling.nextElementSibling; | ||
ul.style.display = "none"; | ||
} | ||
} | ||
}); | ||
} | ||
window.addEventListener("load", addTocClickHandlers); | ||
</script> | ||
' | ||
|
||
# Postprocess the HTML performing the following modifications: | ||
# | ||
# * Each heading in the TOC is represented by an <li> element. If the heading | ||
# has sub-headings, the <li> is follows by a <ul>. Find these <li> elements | ||
# that have sub-headings and add a <span class="toc-parent"> after the <li>. | ||
# The script above uses the class name to attach a click handler. The CSS | ||
# style sheet also uses the class name to attach an icon that the user can | ||
# click. | ||
# | ||
# * Keep track of the heading levels and modify the <li> and <ul> elements to | ||
# initially expand the N topmost levels according to the attribute | ||
# "toclevels-expanded". The <li> element for an expanded level is given the | ||
# class name "toc-expanded". The CSS style sheet uses this to change the | ||
# icon to indicate that the TOC level is expanded. For an unexpanded | ||
# element, we hide the <ul> that follows by adding 'style="display:none;"'. | ||
# | ||
# * Add a <span id="toc-top"> element to the TOC title line. The script above | ||
# uses the ID to attach a click handler, and the CSS style sheet uses the ID | ||
# to attach a clickable icon. | ||
# | ||
# The Asciidoctor postprocessor pass just gets a big string of HTML, not a DOM | ||
# tree. Rather then trying to parse the HTML, we do some fairly simplistic | ||
# pattern matching to find the relevant HTML elements. This pattern matching | ||
# relies on the current output format of the Asciidoctor HTML generator. If | ||
# that format changes in the future, we will either need to change the | ||
# pattern matching in this script or do something more robust by really | ||
# parsing the HTML. | ||
def process document, output | ||
|
||
if document.basebackend? 'html' | ||
if document.attr? 'toclevels-expanded' | ||
toc_levels_expanded = (document.attr 'toclevels-expanded').to_i | ||
else | ||
toc_levels_expanded = -1 | ||
end | ||
new_output = '' | ||
is_in_toc = false | ||
hide_next_ul = false | ||
toc_level = 0 | ||
output.lines.map.each_cons(2) do |line, next_line| | ||
# Keep track of the TOC level by counting the nesting of the <ul> and | ||
# </ul> elements that are in the TOC. | ||
is_in_toc = true if TocStart.match(line) | ||
toc_level+=1 if is_in_toc and Ul.match(line) | ||
if is_in_toc and EndUl.match(line) | ||
toc_level -= 1 | ||
is_in_toc = false if toc_level == 0 | ||
end | ||
|
||
# Add a <span id="toc-top"> to the TOC title. | ||
if is_in_toc and toc_level == 0 | ||
if toc_levels_expanded == 0 | ||
line.sub! TocTitle, '\0<span id="toc-top"></span>' | ||
else | ||
line.sub! TocTitle, '\0<span id="toc-top" class="toc-expanded"></span>' | ||
end | ||
end | ||
|
||
# If an <li> is followed by a <ul>, then the <li> represents a heading | ||
# that has sub-headings. | ||
if is_in_toc and Li.match(line) and Ul.match(next_line) | ||
if (toc_levels_expanded >= 0) and (toc_level > toc_levels_expanded) | ||
line.sub! Li, '<li><span class="toc-parent"></span>' | ||
hide_next_ul = true | ||
else | ||
line.sub! Li, '<li><span class="toc-parent toc-expanded"></span>' | ||
end | ||
end | ||
|
||
# If a <ul> is under an unexpanded <li>, make it invisible. This just | ||
# sets the initial display. The user can still change the visibility | ||
# by clicking the icon. | ||
if is_in_toc and Ul.match(line) and hide_next_ul | ||
line.sub! Ul, '<ul \1 style="display:none;">' | ||
hide_next_ul = false | ||
end | ||
|
||
# Add the script that sets up the click handlers. | ||
if EndHead.match(line) | ||
line = Script + "</head>\n" | ||
end | ||
new_output << line | ||
end | ||
output = new_output | ||
end | ||
output | ||
|
||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters