A nested table of contents in Stati

stati.page.toc is a flat list. A few lines of Eta turn it into nested lists.

SNP 078 #A47 Filed stati eta

eta
<%
  /* Flat [{ id, text, level }] for h2 to h6, nested by level */
  const tree = [];
  const stack = [{ level: 1, children: tree }];
  for (const entry of stati.page.toc) {
    while (stack.length > 1 && stack.at(-1).level >= entry.level) stack.pop();
    const node = { ...entry, children: [] };
    stack.at(-1).children.push(node);
    stack.push(node);
  }

  /* Writes its markup as it runs: call it with <% %>, not <%~ %> */
  function list(nodes) { %>
    <ul>
      <% for (const node of nodes) { %>
        <li>
          <a href="<%= `#${node.id}` %>"><%= node.text %></a>
          <% if (node.children.length) list(node.children); %>
        </li>
      <% } %>
    </ul>
  <% }
%>
<% if (tree.length) { %>
  <nav aria-label="On this page">
    <% list(tree); %>
  </nav>
<% } %>

Stati gives every h2 to h6 an id and lists them in stati.page.toc as { id, text, level }, with -1, -2 added to the ids of repeated headings. Link to those ids rather than slugifying the text again.

The function is the part to remember. Markup inside a function defined in a template goes straight to the output when the function runs, and the function itself returns undefined. <% list(tree); %> is right; <%~ list(tree) %> prints the word undefined and loses the list.

In the Stati docs: Table of contents and the table of contents recipe.