Navigation tree

How src/nav.rs reads the sidebar tree out of the pages' ## Topics sections — finding the section, taking each item's first link, resolving it to a file, grouping by ### headings and stopping at pages already seen — and how src/render.rs turns the tree into the sidebar's HTML with the current page highlighted. It is a mechanism page under the viewer, for someone about to change either file; how to use Topics is in Organizing navigation.

The data

flowchart LR
  tree["nav::tree(document)"] --> groups["list of Group"]
  groups --> group["Group<br/>title, or none"]
  group --> entry["Entry<br/>label, file"]
  entry -->|"children"| groups
  click tree href "#building-the-tree-and-stopping-cycles" "Building the tree"
  click group href "#reading-a-topics-section" "Groups come from ### headings"
  click entry href "#resolving-a-link" "Resolving a link"

nav::tree returns the root page's children as groups; the root page itself is not an entry — the sidebar's document-title link stands for it.

Reading a Topics section

nav::topics(source) parses the page with comrak (default options) and walks the top-level blocks of the document in order:

Lists that aren't top-level blocks — inside a block quote, say — are not read. A page may have more than one ## Topics heading; each is read.

nav::resolve turns an href into a Target:

flowchart TD
  href["A Topics href"] --> other{"Another website,<br/>mailto: or #fragment?"}
  other -->|"no"| exists{"An existing file<br/>in the served folder?"}
  exists -->|"no"| problem["Problem"]
  exists -->|"yes"| md{"Markdown in<br/>the site's folder?"}
  md -->|"yes"| page["Page, with children"]
  md -->|"no"| doc{"A document<br/>file?"}
  doc -->|"yes"| document["Document, no children"]

Any other outcome is None. In detail:

  1. An href containing : (another website, mailto:) or starting with # is None — not an entry.
  2. Otherwise any #fragment is cut off, the rest is joined to the listing page's folder and canonicalized. If that fails, or the file is outside the served folder or not a regular file, it is a Problem: Topics entry <href> — no such file.
  3. A Markdown file inside the site's folder is a Page: an entry with its own children. A Markdown file outside the site's folder is a Problem: Topics entry <href> — outside the site's folder.
  4. A document file (document::kind_of) is a Document: an entry that opens that document, with no children.
  5. Anything else is None.

None and Problem targets are left out of the tree. A problem is reported by link checking: a missing file as the broken link it is, a page outside the site's folder as the Topics problem above. The href is used as written, without percent-decoding.

Building the tree and stopping cycles

tree starts with a set seen holding the root page, then calls children on the root. children reads the page from disk (an unreadable page has no children), and for each resolved topic:

seen is shared by the whole walk, which is depth-first in list order. So a page appears wherever it is listed, but only its first appearance in that order is expanded, and a link back up to an ancestor — or to the root — ends there. This is the case the test builds_the_tree_and_stops_at_cycles covers.

Grouping compares with the last group only, so the same ### title appearing twice, with another group between, gives two separate groups.

The tree is rebuilt for every page request, reading every page it reaches; no result is kept between requests.

Rendering the sidebar

render::navigation renders the groups as nested <ul> lists:

For a root page listing Getting started and, under a Guide group, a page with children of its own, the sidebar reads:

<ul>
  <li><a href="/docs/getting-started.md" class="current">Getting started</a></li>
  <li class="group"><span>Guide</span></li>
  <li><a href="/docs/guide/README.md">Guide</a>
    <ul>
      <li><a href="/docs/guide/writing.md">Writing</a></li>
    </ul>
  </li>
</ul>

The href is the file's address: / plus its path in the served folder. Current-page highlighting compares that with the requested page's own path built the same way: every matching entry gets class="current" — more than one when a page is listed twice. The document-title link above the tree (class document-title) gets current when the page is the root page.

The sidebar is part of every page's HTML, inside <nav id="devpage-nav">, so a live update or an in-place link swaps it together with the content.