# Copyright (c) 2011-2025 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
