Wit

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) survives expand as an ordinary NodeUse (name: 'node'), and your renderer dispatches on the type param. 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&#39;s lamp">.
  • type is anything else. The renderer emits a generic <div> carrying every named param as data-*. 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:

source
#highlight
@node(type: highlight) ::body:: node@
highlight#

(highlight Some emphasised text highlight)
result

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 ) with body: null.
  • Unknown types stay portable. An unknown type is round-trippable via data-*, so a document survives across renderers that don't know the type.
  • Names are unique. A duplicate non-additive #name throws E_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 — #table silently 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 is data-type.

See also

Build your own renderer · Rendering · AST reference (NodeUse, raw / frozen) · Resolve & expand.