zenops 0.20.0

Declarative system configuration management for shell config and dotfiles.
// CLI reference sidebar groomer.
//
// mdBook's toc.js (vendored, not directly editable) walks every h2–h6
// in the chapter and lifts them into the sidebar as a flat list. On
// the CLI reference page that means:
//   1. Every `###### **Subcommands:**` / `###### **Options:**` /
//      `###### **Arguments:**` heading shows up in the sidebar as
//      its own line — clutter that's useful as in-page headings but
//      noise in nav.
//   2. Every subcommand sits at the same indent level, so the
//      `zenops repo *` family reads as ten siblings of `zenops`
//      instead of a parent and its children.
//
// This script runs after toc.js, strips the noise entries from (1),
// and folds the flat list into a tree using the `zenops X Y` naming
// pattern so the sidebar reads like the actual command structure.
// The redundant `zenops` prefix is also dropped from labels — the
// chapter header already says we're in the CLI reference.
(function () {
  function isNoise(href) {
    if (!href) return false;
    return /^#(subcommands|options|arguments)(-\d+)?$/.test(href);
  }

  function dropNoise(root) {
    root.querySelectorAll('.header-item').forEach((li) => {
      const a = li.querySelector('a.header-in-summary');
      if (a && isNoise(a.getAttribute('href'))) li.remove();
    });
  }

  // Build a tree where each <li> nests under the longest existing
  // prefix match. `zenops repo status` becomes a child of `zenops repo`
  // if that entry exists; otherwise it becomes a child of `zenops`.
  function nestByPrefix(root) {
    const items = Array.from(root.querySelectorAll(':scope > .header-item'));
    if (items.length === 0) return;
    // Index entries by the full command tokens, longest first so a
    // child can find its longest matching parent before any shorter
    // ancestor wins.
    const byTokens = new Map();
    items.forEach((li) => {
      const a = li.querySelector('a.header-in-summary');
      const tokens = (a ? a.textContent : '').trim().split(/\s+/);
      li.dataset.tokens = tokens.join(' ');
      byTokens.set(tokens.join(' '), li);
    });
    // Walk children-deepest-first so a re-parented `repo status`
    // doesn't break a later attempt to re-parent `repo`.
    const sorted = [...items].sort(
      (a, b) => b.dataset.tokens.split(' ').length - a.dataset.tokens.split(' ').length,
    );
    for (const li of sorted) {
      const tokens = li.dataset.tokens.split(' ');
      if (tokens.length < 2) continue;
      // Find the longest existing ancestor (drop the last token
      // repeatedly until we find one that's still in the map).
      for (let len = tokens.length - 1; len >= 1; len--) {
        const key = tokens.slice(0, len).join(' ');
        const parent = byTokens.get(key);
        if (!parent) continue;
        let ol = parent.querySelector(':scope > ol.section');
        if (!ol) {
          ol = document.createElement('ol');
          ol.classList.add('section');
          parent.appendChild(ol);
        }
        ol.appendChild(li);
        // Trim the parent's tokens from the child's label so it reads
        // as `status` instead of `zenops repo status`.
        const a = li.querySelector('a.header-in-summary');
        if (a) {
          const remaining = tokens.slice(len).join(' ');
          a.textContent = remaining;
        }
        break;
      }
    }
  }

  // Drop the root `zenops` entry entirely. The chapter title
  // "Command-line interface" in the outer sidebar already serves as
  // the click target for the top of the page, so the root row in the
  // in-page nav is duplicate. Promote its children up one level so
  // the per-command rows still show.
  function liftRoot(root) {
    const rootLi = root.querySelector(':scope > .header-item');
    if (!rootLi) return;
    const a = rootLi.querySelector('a.header-in-summary');
    if (!a || a.textContent.trim() !== 'zenops') return;
    const childOl = rootLi.querySelector(':scope > ol.section');
    if (childOl) {
      for (const child of Array.from(childOl.children)) {
        root.appendChild(child);
      }
    }
    rootLi.remove();
  }

  function cleanup() {
    const root = document.querySelector('.on-this-page > ol.section');
    if (!root) return;
    dropNoise(root);
    nestByPrefix(root);
    liftRoot(root);
  }

  // mdBook's toc.js builds the .on-this-page list on DOMContentLoaded.
  // Run after it: if the DOM is already ready, defer one tick; otherwise
  // listen for DOMContentLoaded and run on the next tick.
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', () => setTimeout(cleanup, 0));
  } else {
    setTimeout(cleanup, 0);
  }
})();