Skip to content

html rendering

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

html Command and wiki.html Viewer

Relevant source files

  • src/repodocs/render.py
  • src/repodocs/cli.py

Overview

The html command bundles the Markdown pages produced by repodocs generate into a single self-contained HTML file, <out>/wiki.html. The page is a client-side viewer: page Markdown is embedded inline as JSON and rendered in the browser with marked, sanitized with DOMPurify, highlighted with highlight.js, and diagrammed with mermaid. It works from file:// and needs no server.

Sources: src/repodocs/cli.py:L94-L107, src/repodocs/render.py:L413-L461

CLI entry point

cmd_html (src/repodocs/cli.py:348-358) resolves the repo path and --out directory (defaulting to <repo>/repo-docs), checks for --vendor, and calls build_html(repo, out, vendor). It reports the number of .md pages bundled (excluding index.md) and, if --vendor was passed, notes that assets/ was written.

Flag Effect
[repo] Repository path to derive the GitHub breadcrumb/link from (positional, parsed by parse_repo_and_flags)
--out DIR Directory containing generated *.md pages and where wiki.html is written (default <repo>/repo-docs)
--vendor Downloads the CDN JS/CSS libs into <out>/assets/ and rewrites the page to use relative paths, making it fully offline

Sources: src/repodocs/cli.py:L348-L358, src/repodocs/cli.py:L94-L107

If --out points at a translated subdirectory (e.g. <out>/pt), the UI labels are localized based on the directory name via lang_labels, since repodocs translate writes translated pages under <out>/<lang>/ and then invokes repodocs html <repo> --out <out>/<lang>.

Sources: src/repodocs/cli.py:L110-L121, src/repodocs/render.py:L68-L70

build_html()

build_html(repo, out, vendor=False) (src/repodocs/render.py:413-461) is the driver:

  1. Globs <out>/*.md (excluding index.md); dies with exit 1 if none exist, instructing the user to run repodocs generate first.
  2. Reads <out>/plan.json, if present, to recover each page's declared title and the plan's page order; malformed or missing plan.json is tolerated (OSError/json.JSONDecodeError are swallowed).
  3. Derives the breadcrumb and GitHub link from github_base(repo): if the repo has a GitHub remote, slug is the <owner>/<repo> extracted from the blob URL, breadcrumb is that slug (else the local directory name), and ghroot is https://github.com/<slug>.
  4. Determines cite_base — citation links only get rewritten to blob/<sha> GitHub URLs when the working tree is clean and HEAD is pushed to a remote branch, as verified by citations_safe(); otherwise citations stay relative and a warning is printed to stderr.
  5. Reads every Markdown page, resolving each page's title (plan title, else the first # H1 extracted by md_title(), else the slug) and rewriting its citation links via rewrite_citation_links().
  6. Orders pages: plan.json order first (filtered to pages that exist), then any remaining pages alphabetically.
  7. Groups pages into nav sections with group_pages().
  8. If vendor is set, calls vendor_assets(out), converting network failures into a fatal error with die().
  9. Infers UI labels from out.name via lang_labels().
  10. Renders the template with render_html() and writes it to <out>/wiki.html.

Sources: src/repodocs/render.py:L413-L461, src/repodocs/render.py:L464-L469

flowchart TD
    A["cmd_html(args)"] --> B["build_html(repo, out, vendor)"]
    B --> C["glob out/*.md"]
    B --> D["read plan.json (titles, order)"]
    B --> E["github_base(repo) -> breadcrumb, ghroot"]
    B --> F["citations_safe() -> cite_base or None"]
    B --> G["read each .md, rewrite_citation_links()"]
    B --> H["group_pages(ordered) -> nav groups"]
    B --> I{vendor?}
    I -- yes --> J["vendor_assets(out) -> assets/*"]
    I -- no --> K[skip]
    B --> L["lang_labels(out.name)"]
    B --> M["render_html(...) -> wiki.html"]
Loading

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

Sidebar and TOC generation

group_pages(slugs) (src/repodocs/render.py:73-88) buckets page slugs deterministically into four fixed nav groups — Overview (overview, installation, limitations, changelog), Development (development, testing, contributing, security, dev-setup), Reference (slug is or contains architecture, or contains interop), and Features (everything else) — preserving plan order within each bucket and omitting empty groups.

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

The sidebar is built client-side in the embedded <script>: for each group in GROUPS, a .navgroup div with a .navhead label (localized via LABELS.groups) is created, and a .navlink anchor (href="#" + slug) is added per page. A #filter text input performs live, case-insensitive filtering over .navlink text, hiding empty groups' headers.

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

The right-hand "On this page" TOC is built by buildToc(content), which selects all h2/h3 elements in the rendered article, assigns each a sec-<i> id, and appends an anchor per heading (.toc-3 styling for h3) that smooth-scrolls to the heading on click. If a page has no headings, the TOC <aside> is hidden.

Sources: src/repodocs/render.py:L312-L325

The "Relevant source files" section — always the page's first <h2> followed by a <ul> — is collapsed into a <details>/<summary> element by collapseSources(content). The match is purely structural (first h2 whose next sibling is a ul), not text-based, so it works across languages/models.

Sources: src/repodocs/render.py:L296-L311

Prev/next page footer links are generated by pageFooter(content, slug) using the page's index in ORDER (the flattened, grouped slug order computed in build_html), sanitized through DOMPurify.sanitize() before insertion.

Sources: src/repodocs/render.py:L326-L336, src/repodocs/render.py:L448-L450

Rendering pipeline and syntax highlighting

On page navigation, show(slug) looks up PAGES[slug], and — guarding against a failed DOMPurify load by rendering a plain-text fallback message instead of unsanitized HTML — parses the page's Markdown with marked.parse() and sanitizes the result with DOMPurify.sanitize(html, { ADD_ATTR: ["target"] }) before injecting it into #content. code.language-mermaid blocks are converted into .mermaid divs and re-rendered by mermaid.run(). pre code blocks are syntax-highlighted via hljs.highlightElement(). Both hljs and mermaid calls are wrapped in try/catch since a rendering failure in either library should not break the rest of the page.

Sources: src/repodocs/render.py:L337-L362

Mermaid is initialized once at load with mermaid.initialize({ startOnLoad: false, theme: "dark", securityLevel: "strict" }), deferring diagram rendering to the explicit mermaid.run() call per page.

Sources: src/repodocs/render.py:L273-L273

The dark theme itself is a static <style> block embedded in HTML_TEMPLATE: fixed header bar, fixed-width sidebar (--side: 240px) and TOC (--toc: 220px) panes, a centered article (max-width 720px), styled code blocks/tables/<details>, and responsive breakpoints that hide the TOC below 1100px and the sidebar below 720px.

Sources: src/repodocs/render.py:L200-L256

Localization

LANG_LABELS (src/repodocs/render.py:53-62) defines UI strings (search, toc, github, prev, next, and nav groups labels) for en and pt (Brazilian Portuguese). lang_labels(name) looks up the label set by out-directory name, falling back to English for any unrecognized name.

Sources: src/repodocs/render.py:L53-L70

CDN assets, SRI, and --vendor bundling

By default, five libraries are loaded from pinned jsDelivr CDN URLs defined in CDN_ASSETS: marked@12.0.2, mermaid@11.16.0, @highlightjs/cdn-assets@11.11.1 (JS and github-dark CSS), and dompurify@3.4.12. Each is paired with a sha384 Subresource Integrity hash in SRI, computed from the exact pinned CDN bytes, so that a tampered or compromised CDN response is rejected by the browser rather than silently executed. _asset_sri_attr(vendor, key) emits the integrity/crossorigin attribute text for CDN mode and returns "" for vendored mode, since vendored files' bytes don't match the CDN hash.

Sources: src/repodocs/render.py:L15-L34, src/repodocs/render.py:L372-L379

When --vendor is passed, vendor_assets(out) (src/repodocs/render.py:188-197) downloads each file in VENDOR_FILES (mapping local filenames to the same pinned CDN URLs) into <out>/assets/, and writes THIRD_PARTY_NOTICES — reproduced license texts for marked (MIT), mermaid (MIT), highlight.js (BSD-3-Clause), and DOMPurify (Apache-2.0 OR MPL-2.0) — to <out>/assets/THIRD-PARTY-NOTICES.txt, satisfying each library's attribution requirements in the published artifact. render_html() then selects VENDOR_ASSETS (relative paths like assets/marked.min.js) instead of CDN_ASSETS for the <script>/<link> src/href values, and omits the SRI attributes for those tags. build_html() treats a download failure (OSError) as fatal via die(), instructing the user to check connectivity and rerun repodocs html --vendor.

Sources: src/repodocs/render.py:L37-L50, src/repodocs/render.py:L91-L197, src/repodocs/render.py:L451-L457, src/repodocs/render.py:L382-L410

Template assembly and escaping

render_html() fills HTML_TEMPLATE by string replacement. Because breadcrumb/ghroot can originate from an untrusted repo directory name or configured remote slug, they are HTML-escaped (html.escape(..., quote=True)) before being placed in text nodes or the GitHub link's href/text, and JSON-escaped (with </ neutralized to <\/) before being embedded as JS string/object literals (REPO, PAGES). __PAGES__ — which embeds every page's raw Markdown — is replaced last, so that Markdown content containing template-token-like text (e.g. __GROUPS__) cannot clobber earlier substitutions.

Sources: src/repodocs/render.py:L382-L410

md_title()

md_title(text) (src/repodocs/render.py:464-469) returns the first Markdown # H1 heading's text as a page's fallback title when plan.json supplies none.

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

Clone this wiki locally