Rendering a page
How a Markdown file becomes the HTML page a reader sees: comrak turns it into
HTML with highlighted code, the title and sidebar are worked out, and the
pieces go into the page frame. It is a mechanism page under the
viewer, beside Serving pages,
for someone about to change src/render.rs or src/highlight.rs.
flowchart LR
md["page.md"] --> comrak["Markdown to HTML<br/>comrak"]
comrak --> code["Code highlighted<br/>syntect"]
code --> frame["The page frame"]
title["Title"] --> frame
side["Sidebar"] --> frame
click comrak href "#markdown-to-html" "Markdown to HTML"
click code href "#highlighted-code" "Highlighted code"
click title href "#the-title-and-the-sidebar" "The title and the sidebar"
click side href "#the-title-and-the-sidebar" "The title and the sidebar"
click frame href "#the-page-frame" "The page frame"
A file ending in .md is read as text and passed to render::page, which
does the four steps below.
Markdown to HTML
render::markdown runs comrak with these options
(options() in src/render.rs):
| Option | Effect |
|---|---|
extension.table |
GFM tables |
extension.strikethrough |
~~text~~ |
extension.autolink |
bare URLs become links |
extension.tasklist |
- [ ] / - [x] items |
extension.footnotes |
[^1] footnotes |
extension.header_id_prefix = "" |
every heading gets an id from its text, with no prefix |
render.unsafe |
raw HTML passes through |
render.github_pre_lang |
a fence's language is passed as a lang attribute — which the highlighter below then drops for every block except mermaid |
Last, every Markdown table is wrapped in <div class="table-scroll">, so a
table wider than the page scrolls inside its own box instead of widening
the page.
An image of a .design file — , or a raw
<img src="checkout.design"> however its attributes are quoted, spaced or
capitalised — becomes the design's embed (embed_designs): a
<canvas class="design"> keeping the link in src and the image's text as
its label, which the page script has the
design engine draw. Any other
image is left as it is.
Highlighted code
render::markdown also plugs in a code-fence highlighter,
highlight::Highlighter in src/highlight.rs, through comrak's Plugins
(SyntaxHighlighterAdapter). For each fenced block it:
- takes the first word of the info string as the language and looks it up
with syntect's
find_syntax_by_token(name or file extension) intwo_face::syntax::extra_newlines()— the syntax setbatships, loaded once on first use through aLazyLock; - if found, and the language isn't
mermaid, emits syntect's class-based HTML (ClassStyle::SpacedPrefixed { prefix: "hl-" }): nested spans such as<span class="hl-keyword hl-operator hl-arithmetic hl-rust">, colored by the--syntax-*tokens inassets/style.css(see Themes); - otherwise — no language, an unknown one,
mermaid, or a highlighting error partway through — writes the code HTML-escaped and unhighlighted; - writes a bare
<pre>and<code>with no language attributes, except<pre lang="mermaid">for Mermaid blocks, which is how the page script finds the diagrams to draw.
The title and the sidebar
Title. render::title takes the first line starting with # , or the
file name without its extension.
Sidebar. The navigation tree is built from the root page and rendered to nested lists, with the current page marked.
The page frame
The pieces go into one HTML page. A site's page, with a navbar and a sidebar, has this shape:
<!-- icon, theme script, import map, stylesheet, pele, devpage.js -->
<!-- logo, title, links, switches -->
<!-- the sidebar tree -->
<!-- the rendered Markdown -->
In detail:
<title>is<page title> · <document title>;- first in
<head>, the document's logo as<link rel="icon">when it has one, then a small script that setsdata-themeon<html>— the reader's stored choice, or the system's — before anything paints (choosing a theme); <body>'s class names the frame:site, pluswith-navbarandno-sidebaras the document file says;<header id="devpage-navbar">, when the site has a navbar: its logo and title, its links and dropdowns with their icons drawn as inline SVG, then the comment-mode switch (unless comments are off) and the theme selector (the navbar);<nav id="devpage-nav">, unlesssidebar: false, holds the tree — after a link to the site's root page with its logo and title when there is no navbar to hold it;- with no navbar, the comment-mode switch (unless comments are off) and the
theme selector (unless
theme-selector: false) in<div id="devpage-corner">, top right; <main id="devpage-page"><article>holds the rendered Markdown;- in
<head>, an import map naming React (react,react/jsx-runtime,react-dom/client→/__devpage/react.js) andwindow.devpageWidgets, the document's widgets with the address of each (Widgets); - the stylesheet and two scripts — pele, then
devpage.js— are loaded from/__devpage/; with comments offdevpage.jscarriesdata-no-comments, and the script shows no threads and offers no comment (Turning comments off).
Every id the frame uses starts with devpage- — devpage-page,
devpage-nav, devpage-navbar, devpage-corner, devpage-comments and
devpage-comments-toggle — and a heading whose text would give it one of
them gets -heading added to its id. So a page can have a heading called
Comments, Page or Navigation and it stays an ordinary heading, never
styled or replaced as part of the frame.
devpage-navbar, devpage-nav and devpage-page are what
live updates replace in place. A doc's and a
deck's frames carry the same head — the logo's icon link and the theme script —
and the theme corner. Titles and sidebar labels are HTML-escaped; the page body
is not, since raw HTML is allowed.
Beyond the index, nothing is cached between requests: each page request re-reads the page, its site's document file and, to build the sidebar, every page the tree reaches.