Custom nodes / extending vocabulary
Core vocab is closed: 52 reserved names, not extensible. 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. - Core vocab. Fixed and not extensible;
isReservedNodeNamerejects any redefinition. Shown here only for contrast.
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 …|→<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 ||content|| @node |type highlight| ::content:: node@ highlight# @highlight Some emphasised text highlight@
The author writes @highlight …; the expanded AST carries type highlight for the renderer to dispatch on. Fixture 20-opaque-node/user-defined-wrapper.wit.
Every edge
- Block form vs. self-closing.
@node … node@has a body;@node |type img|self-closes. Fixturesnode-with-bodyandbare-node-passthrough. - 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; core names cannot be redefined at all.
Common mistakes
- Trying to redefine
@tableor@h1— they are reserved. - 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.