Custom nodes / extending vocabulary
Core vocab is a fixed set of 52 names the renderers know. Everything else you
add either by composition — a definition, no code — or through
the opaque @node plus renderer dispatch. This page
covers both sides: what an author writes and what a renderer does with it.
Three ways to extend
- A definition.
#name … name#is pure composition; the expander inlines it, and no renderer changes. Reach for this first. - The opaque node.
@node(type: X)survivesexpandas an ordinaryNodeUse(name: 'node'), and your renderer dispatches on thetypeparam. Reach for this when you need new rendering that composition can't express. - Redefining a core name. A definition may reuse a core name; it then replaces the core meaning in that document, and the renderer never sees the core node. Renderers cannot add names to core vocab.
The opaque-node contract
@node needs a type param; all other params and the
body survive into the expanded AST. The HTML renderer dispatches two ways:
- type is itself a core-vocab name. The renderer re-emits that
element. Verified:
@node(type: img, src: ./lamp.png, alt: The keeper's lamp)→<img src="./lamp.png" alt="The keeper's lamp">. - type is anything else. The renderer emits a generic
<div>carrying every named param asdata-*. Verified:@node(type: chart, layout: horizontal) A body node@→<div data-type="chart" data-layout="horizontal"><p> A body </p></div>.
The preferred authoring pattern
Raw @node is noisy in prose. Wrap it in a definition so authors
get a natural name while the AST still carries type for the renderer:
#highlight @node(type: highlight) ::body:: node@ highlight# (highlight Some emphasised text highlight)
Some emphasised text
The author writes (highlight … highlight); the expanded AST
carries a node with type: highlight for the renderer to
dispatch on. The HTML renderer, which doesn't know the type, emits <div data-type="highlight"><p>Some emphasised text</p></div>.
Every edge
- Body or none.
@node(type: chart) … node@has a body;@node(type: img, src: a.png)on its own line ends at its)withbody: null. - Unknown types stay portable. An unknown
typeis round-trippable viadata-*, so a document survives across renderers that don't know the type. - Names are unique. A duplicate non-additive
#namethrowsE_DUPLICATE_DEFINITION. A single definition of a core name (#h1 … h1#) is allowed and wins over the core node.
Common mistakes
- Redefining a core name by accident —
#tablesilently replaces the table renderer for the whole document. Pick a distinct name. - Expecting Markdown to render an opaque container — it emits the body only.
- Reading
data-wit-type— the attribute isdata-type.
See also
Build your own renderer · Rendering · AST reference (NodeUse, raw / frozen) · Resolve & expand.