Wit

Rendering

Rendering is the last mile and the most configuration-light stage. It turns an ExpandedDocument into a string — HTML or Markdown — with a pure tree walk: no I/O, no state beyond the options you pass. Know the two knobs (mode and CSS) and the fixed AST-to-HTML mapping and you know the whole job.

HTML

renderHtml(doc: ExpandedDocument, options?: RenderHtmlOptions) → string.

RenderHtmlOptions

:

  • mode'fragment' | 'document'. Defaults to 'fragment'. fragment emits just <article class="wit-doc">…</article>; documentwraps it in a full <!doctype html> page with the stylesheet inlined into <head>.
  • title?: string — the document-mode <title> (default 'Wit document').
  • lang?: string — the document-mode <html lang> (default 'en').
  • css?: string — the document-mode stylesheet, inlined into <head>. Defaults to defaultThemeCss. Pass ''for an unstyled page, or your own string.

Theming

Two stylesheet strings ship from @witlang/render-html:

  • defaultThemeCss — the Word-like house style: CSS custom properties, a prefers-color-scheme dark mode, print and @page rules. A document with no CSS of its own renders like a word-processor page.
  • rawThemeCss — a mechanical reset only (box model, no body margin, responsive media). You style everything yourself via @@style and wrapping nodes.
import { renderHtml, defaultThemeCss, rawThemeCss } from &#39;@witlang/render-html&#39;;

renderHtml(expanded, { mode: &#39;document&#39; });                    // house style
renderHtml(expanded, { mode: &#39;document&#39;, css: rawThemeCss });  // bare reset
renderHtml(expanded, { mode: &#39;document&#39;, css: myOwnCss });     // your CSS
renderHtml(expanded, { mode: &#39;document&#39;, css: &#39;&#39; });           // no <style> at all

The AST-to-HTML mapping

The walk is fixed. The mappings that matter:

  • Paragraph<p>; empty paragraphs are dropped, so no stray <p></p>.
  • Italic<em>; Bold<strong>; Text is escaped through escapeHtml.
  • Core vocab maps to its fixed element: @h1<h1>, @a |href …|<a href=…>, @img<img>, @row / @col → flex <div>s, @br / @hr → void elements.
  • @table → its dedicated renderer (schema, rows, caption; three call forms).
  • @bibliography<div class="wit-bibliography"> with one <p> per entry.
  • An opaque @node → dispatched on its type param (see Custom nodes).
  • A definition body that collapsed to a literal Record<table class="wit-record">; a Collection<ul class="wit-collection">.
  • A surviving unknown user @name (rare) → <div|span class="wit-node" data-wit-name="…"> with each param as a data-* attribute.
  • An unresolved ::x:: interpolation → <span class="wit-unresolved">::x::</span>; a surviving unresolved access @x.y<span class="wit-unresolved">@x.y</span> — visible, never silent.

Safety

All text and attribute values pass through escapeHtml ('&#39;, "&quot;), so malformed prose can never produce malformed HTML. The raw-text elements @@style and @@script emit their body verbatim — CSS and JS must not be escaped — but the renderer guards the closing tag (</style becomes <\/style inside the body):

@@style .x{color:red} style@@
// → <style>.x { color: red; }</style>

Markdown

renderMarkdown(doc: ExpandedDocument) → string is the single export — no options. It is CommonMark-ish, hand-rolled per node name. The same source rendered both ways:

source
@h1 Title h1@

Some *bold* and _italic_ prose.

@ul
@li One li@
@li Two li@
ul@
result

Title

Some bold and italic prose.

  • One
  • Two

→ Markdown:

source
# Title

Some **bold** and *italic* prose.

- One
- Two
result

# Title

Some *bold* and italic prose.

- One - Two

Sectioning wrappers (section, article, header, footer, nav, main) are transparent; comments and scripts are dropped; unknown nodes emit their content unwrapped; there is no Markdown analog for the opaque container, so it emits just its body.

Progressive examples

  • Fragment vs document. renderHtml(expanded) gives the bare <article>; renderHtml(expanded, { mode: 'document', title: 'Essay' }) gives a full page.
  • Re-theme. Swap css: rawThemeCss for the mechanical reset, or pass your own string.
  • Two targets, one source. Call renderHtml and renderMarkdown on the same ExpandedDocument.

Every edge

  • mode default is fragment — easy to get wrong.
  • css: '' yields an unstyled document; the <style> tag is omitted entirely.
  • Because escapeHtml runs on all text, you never double-escape by letting the renderer do its job.
  • The PDF path (CLI) is just document-mode HTML paginated by headless Chrome — no renderer difference.

Common mistakes

  • Expecting document mode by default from renderHtml.
  • Expecting renderMarkdown to take options — it takes none.
  • Double-escaping — the renderer already escapes; hand it raw strings.
  • Worrying about a </style> inside @@style — it is auto-guarded.

See also

Custom nodes

(opaque dispatch) · Build your own renderer · Architecture (render is a pure walk) · Resolve & expand.