Skip to content

html rendering

Ary Rabelo edited this page Jul 22, 2026 · 3 revisions

Good, I have enough to write the page.

HTML Rendering

Relevant source files

  • src/repodocs/render.py
  • tests/test_fix_render_links.py

Overview

render.py turns the per-page Markdown files produced by generate into a single self-contained wiki.html file: a static-site-like viewer with a sidebar nav, search filter, table of contents, GitHub citation links, and Mermaid/highlight.js rendering, all driven by a client-side JavaScript router that reads the page content out of an embedded JSON blob rather than making network requests.

Sources: src/repodocs/render.py:L413-L461

build_html: assembling the wiki

build_html(repo, out, vendor=False) is the entry point. It:

  1. Globs *.md files in out (excluding index.md) and fails via die() if none exist.
  2. Loads out/plan.json, if present, to recover each page's title and the intended page order (falling back to the slug and alphabetical order otherwise).
  3. Derives a breadcrumb (repo slug or directory name) and an optional ghroot GitHub URL via github_base(repo).
  4. Decides whether citation links can be rewritten to absolute blob/<sha> GitHub URLs (cite_base) or must stay relative, based on citations_safe() over git status --porcelain and git branch -r --contains HEAD — dirty trees or unpushed HEADs disable absolute citation rewriting and a warning is printed to stderr.
  5. Reads each Markdown file, extracts its title with md_title() (falling back to plan.json's title or the slug), and rewrites citation links with rewrite_citation_links(text, cite_base).
  6. Orders and groups pages with group_pages(), producing groups (nav sections) and a flat order list used for prev/next pagination.
  7. If vendor=True, downloads the pinned CDN assets into out/assets/ via vendor_assets() for fully offline use.
  8. Infers UI language labels from the output directory name via lang_labels(out.name) (e.g., an out dir named pt yields Portuguese labels).
  9. Renders the final document with render_html(...) and writes it to out/wiki.html.

Sources: src/repodocs/render.py:L413-L461

HTML Rendering diagram

Sources: src/repodocs/render.py:L413-L461

Page title resolution

md_title(text) scans a Markdown page line by line for the first ^#\s+(.+) match and returns the captured heading text, or None if the page has no top-level heading. build_html prefers plan.json's recorded title, then falls back to md_title, then to the slug itself.

Sources: src/repodocs/render.py:L464-L469

Navigation grouping

group_pages(slugs) buckets slugs into four fixed, deterministically ordered groups — Overview, Features, Reference, Development — while preserving the plan's relative order within each bucket, and omits empty groups from the result:

Group Membership rule
Overview slug in {"overview", "installation", "limitations", "changelog"}
Development slug in {"development", "testing", "contributing", "security", "dev-setup"}
Reference slug equals "architecture", or contains "architecture" or "interop"
Features everything else

Sources: src/repodocs/render.py:L73-L88

Asset delivery: CDN vs. vendored

render.py hard-codes pinned, exact-semver CDN URLs in CDN_ASSETS for marked, mermaid, highlight.min.js (hljs), its stylesheet (hljscss), and dompurify, each paired with a sha384 Subresource Integrity hash in SRI. When vendor=True, vendor_assets(out) downloads each of VENDOR_FILES into out/assets/ and writes THIRD-PARTY-NOTICES.txt (attribution text for marked, Mermaid, highlight.js, and DOMPurify), and render_html swaps in the local VENDOR_ASSETS paths instead of the CDN URLs. _asset_sri_attr(vendor, key) returns an empty string for vendored assets — their local bytes don't match the pinned CDN hash, so tagging them with integrity would make the browser refuse to load them — and otherwise returns the integrity="sha384-..." crossorigin="anonymous" attribute pair.

Sources: src/repodocs/render.py:L15-L50, src/repodocs/render.py:L188-L197, src/repodocs/render.py:L372-L379

Tests confirm every CDN_ASSETS URL is pinned to an exact major.minor.patch version (no floating @N/ majors) and carries a sha384- SRI hash, that CDN-mode HTML includes integrity/crossorigin on each <script>/<link> tag, and that vendor-mode HTML omits integrity entirely for those same assets.

Sources: tests/test_fix_render_links.py:L43-L67

HTML_TEMPLATE and render_html

HTML_TEMPLATE is a single self-contained HTML document (inline <style> and <script>) with placeholder tokens (__TITLE__, __BREADCRUMB__, __GHLINK__, __MARKED__, __MERMAID__, __HLJS__, __HLJSCSS__, __DOMPURIFY__, their *_SRI__ counterparts, __REPO__, __LABELS__, __GROUPS__, __ORDER__, and __PAGES__). render_html(breadcrumb, ghroot, data, groups, order, vendor, labels) substitutes each token:

  • breadcrumb and the GitHub link text/href are HTML-escaped with html.escape(..., quote=True) since they may originate from an untrusted repo directory name or remote slug.
  • __REPO__ is embedded as a JSON string with </ escaped to <\/ to prevent breaking out of the inline <script> block.
  • __PAGES__ (the full {slug: {title, md}} map) is substituted last, with the same </ escaping, so that embedded page Markdown cannot clobber earlier token replacements.

Sources: src/repodocs/render.py:L200-L269, src/repodocs/render.py:L382-L410

Regression tests assert a malicious breadcrumb containing </title><script> is fully escaped inside <title> and cannot break the shared <script> block via the REPO assignment, and that a malicious ghroot value is HTML-escaped as an attribute rather than injecting a live <script> element.

Sources: tests/test_fix_render_links.py:L15-L40

Client-side rendering pipeline

The embedded <script> in HTML_TEMPLATE implements a hash-router single-page app over the PAGES/GROUPS/ORDER/REPO/LABELS globals injected by render_html:

  • Navigation is built from GROUPS, each rendered as a .navgroup with a .navhead label (translated via LABELS.groups) and .navlink anchors per slug; a #filter text input hides/shows nav links and their group headers by substring match.
  • show(slug) looks up PAGES[slug], and if DOMPurify failed to load, refuses to render page content at all (fail-closed) rather than risk unsanitized HTML. Otherwise it renders p.md through marked.parse and sanitizes the result with DOMPurify.sanitize(..., { ADD_ATTR: ["target"] }) before inserting it into #content.
  • Fenced ```mermaid code blocks (rendered by marked as <code class="language-mermaid">) are converted to <div class="mermaid"> elements and rendered via mermaid.run(...), initialized with securityLevel: "strict".
  • collapseSources(content) wraps the first <h2> (matched structurally, not by heading text, since wording varies by language/model) plus its following <ul> file-list sibling into a collapsible <details> element — this is the page's "Relevant source files" section.
  • buildToc(content) builds the right-hand table of contents from all <h2>/<h3> headings, assigning each an id="sec-N" and a smooth-scroll anchor link.
  • pageFooter(content, slug) appends previous/next page navigation using ORDER, with its HTML also passed through DOMPurify.sanitize.
  • Highlighting is applied per code block via hljs.highlightElement when window.hljs is present.
  • The router reads location.hash on load and on hashchange to call show(), defaulting to ORDER[0] if the hash doesn't match a known slug.

Sources: src/repodocs/render.py:L271-L366

Citation link rewriting and safety checks

Page Markdown produced by generate contains relative citation links like <a href="https://github.com/aryrabelo/repodocs/blob/51ba8b18a6abbf0ed1789af067cb1c932993f747/path#L1-L2" target="_blank" rel="noopener">label</a>; before embedding, build_html calls rewrite_citation_links(text, cite_base) (from gitlinks.py) to turn these into absolute GitHub blob URLs when it is safe to do so. Safety is determined by citations_safe(), which treats a working tree as unsafe (falling back to relative links) if git status --porcelain shows tracked changes, or shows untracked files outside the out directory — since an untracked source file could be exactly what a citation points at and would not exist in the pushed commit — and also requires that HEAD is contained in a remote branch (via git branch -r --contains HEAD).

Sources: src/repodocs/render.py:L427-L442

Regression tests cover this from the gitlinks.py side: rewrite_citation_links HTML-escapes a malicious base URL (e.g. one containing " onmouseover="alert(1)) so it cannot inject a live attribute into the rendered <a href>; citations_safe rejects a dirty tree when an untracked file lies outside out_rel, allows untracked entries that are themselves under out_rel, and defaults to "dirty" when out_rel is not supplied at all.

Sources: tests/test_fix_render_links.py:L70-L112

Clone this wiki locally