Skip to content

Commit

Permalink
Merge pull request #560 from gmlueck/gmlueck/accordion-toc
Browse files Browse the repository at this point in the history
Expandable TOC for HTML output
  • Loading branch information
gmlueck committed Jun 12, 2024
1 parent 60aa448 commit 1694264
Show file tree
Hide file tree
Showing 5 changed files with 239 additions and 1 deletion.
3 changes: 2 additions & 1 deletion adoc/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,8 @@ ADOCOPTS = --doctype book $(ADOCMISCOPTS) $(ATTRIBOPTS) \
ADOCHTMLEXTS = --require $(CURDIR)/config/katex_replace.rb \
--require $(CURDIR)/config/loadable_html.rb \
--require $(CURDIR)/config/synopsis.rb
--require $(CURDIR)/config/synopsis.rb \
--require $(CURDIR)/config/accordion_toc.rb
# ADOCHTMLOPTS relies on the relative runtime path from the output HTML
# file to the katex scripts being set with KATEXDIR. This is overridden
Expand Down
9 changes: 9 additions & 0 deletions adoc/config/accordion_toc.rb
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
183 changes: 183 additions & 0 deletions adoc/config/accordion_toc/extension.rb
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
44 changes: 44 additions & 0 deletions adoc/config/khronos.css
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,50 @@ b.button:after { content: "]"; padding: 0 2px 0 3px; }
#content #toc > :first-child { margin-top: 0; }
#content #toc > :last-child { margin-bottom: 0; }

/*
* Add a triangle icon to the left of TOC entries that have sub-headings.
* The triangle points down if the heading is expanded in the TOC, and it
* points right if the TOC heading is unexpanded. This is paired with a
* click handler that allows the user to change the expansion of each TOC
* entry.
*/
span.toc-parent:before {
content: "\25B6";
font-size: 0.6em;
display: block;
padding-top: 0.1em;
position: absolute;
z-index: 1001;
width: 1.5ex;
margin-left: -1.9ex;
display: block;
}
span.toc-parent.toc-expanded:before {
content: "\25BC";
}

/*
* Add a double-chevron icon to the left of the TOC title. The chevrons point
* down when all TOC levels are expanded and they point up when all TOC levels
* are collapsed. This is also paired with a click handler to expand /
* collapse all TOC levels.
*/
#toc-top:before {
content: "\AB";
transform: rotate(90deg);
font-size: 0.9em;
display: block;
padding-left: 0.2em;
position: absolute;
z-index: 1001;
width: 1.5ex;
margin-left: -1.5ex;
display: block;
}
#toc-top.toc-expanded:before {
content: "\BB";
}

#footer { max-width: 100%; background-color: none; padding: 1.25em; }

#footer-text { color: black; line-height: 1.44; }
Expand Down
1 change: 1 addition & 0 deletions adoc/syclbase.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ The Khronos{regtitle} {SYCL_NAME}{tmtitle} Working Group
:icons: font
:toc2:
:toclevels: 10
:toclevels-expanded: 0
:sectnumlevels: 10
:max-width: 100%
:numbered:
Expand Down

0 comments on commit 1694264

Please sign in to comment.