From 32a959434f1e5b65b6304bb3d45000f7d5368f9d Mon Sep 17 00:00:00 2001 From: Greg Lueck Date: Fri, 24 May 2024 17:20:25 -0400 Subject: [PATCH 1/3] Expandable TOC for HTML output Add an Asciidoctor extension and CSS styling to implement an expandable table of contents for the HTML render. This allows the user to click an icon in the TOC to expand or collapse each heading level. There is also an icon on the title line of the TOC which the user can click to expand or collapse all heading levels. The initial state is that all TOC levels are collapsed, but this is tunable via the `toclevels-expanded` Asciidoc attribute. --- adoc/Makefile | 3 +- adoc/config/accordion_toc.rb | 9 ++ adoc/config/accordion_toc/extension.rb | 188 +++++++++++++++++++++++++ adoc/config/khronos.css | 44 ++++++ adoc/syclbase.adoc | 1 + 5 files changed, 244 insertions(+), 1 deletion(-) create mode 100644 adoc/config/accordion_toc.rb create mode 100644 adoc/config/accordion_toc/extension.rb diff --git a/adoc/Makefile b/adoc/Makefile index 1f9153dd..b3e6c4c8 100644 --- a/adoc/Makefile +++ b/adoc/Makefile @@ -143,7 +143,8 @@ ADOCOPTS = --doctype book $(ADOCMISCOPTS) $(ATTRIBOPTS) \ $(NOTEOPTS) $(VERBOSE) $(ADOCEXTS) ADOCHTMLEXTS = --require $(CURDIR)/config/katex_replace.rb \ - --require $(CURDIR)/config/loadable_html.rb + --require $(CURDIR)/config/loadable_html.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 diff --git a/adoc/config/accordion_toc.rb b/adoc/config/accordion_toc.rb new file mode 100644 index 00000000..1d9e91d5 --- /dev/null +++ b/adoc/config/accordion_toc.rb @@ -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 diff --git a/adoc/config/accordion_toc/extension.rb b/adoc/config/accordion_toc/extension.rb new file mode 100644 index 00000000..f86d3805 --- /dev/null +++ b/adoc/config/accordion_toc/extension.rb @@ -0,0 +1,188 @@ +# 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 = /^
/ + Li = /^
  • / + Ul = /^
      / + EndUl = /^<\/ul>/ + EndHead = /^<\/head>/ + + # Add a click handler to the elements. When the + # element is clicked: + # + # * The class "toc-expanded" is toggled on the , which changes the icon. + # * The
        element that follows the is made visible / invisible. + # + # Note that this script assumes that the
          element is two siblings beyond + # the that is clicked. This assumes the HTML format generated by + # Asciidoctor looks like: + # + #
        • + # <-- generated by this extension below + # ... + #
            + # + # Also add a click handler to the + 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); + +' + + # Postprocess the HTML performing the following modifications: + # + # * Each heading in the TOC is represented by an
          • element. If the heading + # has sub-headings, the
          • is follows by a
              . Find these
            • elements + # that have sub-headings and add a after the
            • . + # 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
            • and
                elements to + # initially expand the N topmost levels according to the attribute + # "toclevels-expanded". The
              • 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
                  that follows by adding 'style="display:none;"'. + # + # * Add a 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 + lines = output.lines + num_lines = lines.length-1 + for i in 0..num_lines + line = lines[i] + next_line = (i < num_lines) ? lines[i+1] : '' + + # Keep track of the TOC level by counting the nesting of the
                    and + #
                  elements that are in the TOC. + if TocStart.match(line) then is_in_toc = true end + if is_in_toc and Ul.match(line) then toc_level+=1 end + if is_in_toc and EndUl.match(line) + toc_level -= 1 + if toc_level == 0 then is_in_toc = false end + end + + # Add a to the TOC title. + if is_in_toc and toc_level == 0 + if toc_levels_expanded == 0 + line.sub! TocTitle, '\0' + else + line.sub! TocTitle, '\0' + end + end + + # If an
                • is followed by a
                    , then the
                  • 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, '
                  • ' + hide_next_ul = true + else + line.sub! Li, '
                  • ' + end + end + + # If a
                      is under an unexpanded
                    • , 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, '
                        ' + hide_next_ul = false + end + + # Add the script that sets up the click handlers. + if EndHead.match(line) + line = Script + "\n" + end + new_output << line + end + output = new_output + end + output + + end +end diff --git a/adoc/config/khronos.css b/adoc/config/khronos.css index e0b13dff..aff7b65b 100644 --- a/adoc/config/khronos.css +++ b/adoc/config/khronos.css @@ -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; } diff --git a/adoc/syclbase.adoc b/adoc/syclbase.adoc index 73e2ec53..c3428b8b 100644 --- a/adoc/syclbase.adoc +++ b/adoc/syclbase.adoc @@ -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: From 2c1a88474922660e4962269a958395c703495a9a Mon Sep 17 00:00:00 2001 From: Greg Lueck Date: Mon, 10 Jun 2024 16:17:28 -0400 Subject: [PATCH 2/3] Improve Ruby style --- adoc/config/accordion_toc/extension.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/adoc/config/accordion_toc/extension.rb b/adoc/config/accordion_toc/extension.rb index f86d3805..98e7171f 100644 --- a/adoc/config/accordion_toc/extension.rb +++ b/adoc/config/accordion_toc/extension.rb @@ -139,11 +139,11 @@ def process document, output # Keep track of the TOC level by counting the nesting of the
                          and #
                        elements that are in the TOC. - if TocStart.match(line) then is_in_toc = true end - if is_in_toc and Ul.match(line) then toc_level+=1 end + 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 - if toc_level == 0 then is_in_toc = false end + is_in_toc = false if toc_level == 0 end # Add a to the TOC title. From d513921589b2f96cb05c3fb1731267a9be522f56 Mon Sep 17 00:00:00 2001 From: Greg Lueck Date: Mon, 10 Jun 2024 17:07:32 -0400 Subject: [PATCH 3/3] More Ruby style improvements --- adoc/config/accordion_toc/extension.rb | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/adoc/config/accordion_toc/extension.rb b/adoc/config/accordion_toc/extension.rb index 98e7171f..d86fcbde 100644 --- a/adoc/config/accordion_toc/extension.rb +++ b/adoc/config/accordion_toc/extension.rb @@ -131,12 +131,7 @@ def process document, output is_in_toc = false hide_next_ul = false toc_level = 0 - lines = output.lines - num_lines = lines.length-1 - for i in 0..num_lines - line = lines[i] - next_line = (i < num_lines) ? lines[i+1] : '' - + output.lines.map.each_cons(2) do |line, next_line| # Keep track of the TOC level by counting the nesting of the
                          and #
                        elements that are in the TOC. is_in_toc = true if TocStart.match(line)