Wit

Parameters & the invocation forms

A parameter is metadata that rides alongside a node's content — a link's address, a card's status, a figure's caption. Wit gives you five syntaxes for passing parameters, and they all bind to the same names. The guiding principle is the form you open with is the form that closes.

A single definition — here a card that names a title and a status — can be filled every way, and every buildable form renders identically.

#card ||title, status||
Title ::title:: / Status ::status::
card#
@card { title - Alpha, status - draft }       ~ record-arg (self-closing)
@card { title: Alpha, status: draft }         ~ record-arg, colon separator
@card title:Alpha status:draft card@          ~ colon-scatter (one-line body)
@card                                          ~ form-fill: the body IS the params
  title: Alpha
  status: draft
card@
@card(title Alpha, status draft)              ~ parens (self-closing)
@card(title: Alpha, status: draft)            ~ parens, colon separator
@card |title Alpha| |status draft| card@      ~ pipes (carries a body)

All render Title Alpha / Status draft. (The fixture folder 26-all-param-forms collects these; its combined showcase file intentionally fails to build, so lean on the individual forms.)

Choosing a form

  • Structured, no body? Prefer record-arg with a colon — @card { title: X }.
  • Tag a node mid-sentence? Colon-scatter@figure src:lamp caption:"the lens" figure@.
  • Many fields, or a multi-line value? Use a form-fill body.
  • One or two short params inline? Reach for parens@badge(tone good).
  • Need a body, or switching a value mid-body? Only pipes compose with body content — but they are the noisiest form and the one prone to greedy-bind, so avoid them otherwise.

The shared slot grammar

Record-arg, colon-scatter, parens, and pipes all read each slot with the same grammar:

  • Positional — a single token with no space: |full| binds a value with no name. It is a value, not a flag.
  • Namedkey value: the first word is the key, the rest is the value, which may be several words. |mood calm| sets mood to calm.
  • Named, multi-word key — join words with a spaced hyphen -: |background colour - dark slate| has key background colour. An embedded hyphen with no surrounding spaces stays in the value — |well-known|.
  • Flag — a trailing ! marks a boolean-style flag with an empty value: |full width!|. A ! in the middle of a value is just punctuation.

A value that is exactly "quoted" is unquoted, with "and \ recognised. Every non-flag value is then type-probed — run through the scalar classifier — so a number, record, or collection passed through any form becomes a real typed value you can iterate, compare, or reach into. See Type-classified scalars.

Record-arg

A record literal placed right after a handle — @name { field - value, … } — whose fields become named parameters. It is self-closing (the matching } ends the node) and the exclusive form: it will not share a node with any other parameter source. Reach for it when the arguments read like structured data.

@card { title - Alpha, status - draft }

Both fields bind to the card's captures and render Title Alpha / Status draft. The separator can be a spaced hyphen or a colon:

@card { title: Epsilon, status: draft }

The brace binds to explicit captures (template-expansion) and to captures inferred from the body (template-implicit-captures) alike.

The record grammar is identical inline and across lines, so a many-field call can breathe — fields separate on a comma or a newline, and emphasis markers inside a value stay literal:

@reference_entry {
  author - Ursula Franklin
  title - The Real World of Technology
  year - 1989
}

The comma-in-value trap

An unquoted comma splits fields. Because the parser reads a comma as a field boundary, a value that contains one is misread as a new field with no key:

@cite { author - Boud, D., year - 2024 }   ~ E_MALFORMED_RECORD

That is E_MALFORMED_RECORD — "record field missing key". Quote the value to protect the comma, which also forces a string type:

source
@cite { author: "Boud, D.", year: 2024 }   ~ renders: Boud, D. (2024)
result

~ renders: Boud, D. (2024)

(Same root cause as fixture 09-records/comma-in-value.)

Colon-scatter

In a single-line body, tokens shaped with zero whitespace are lifted out as parameters; the rest of the line stays prose. It is the lightest way to tag a node mid-sentence:

source
@figure src:lamp caption:"Second-order Fresnel" figure@
result
src

and caption become params; anything else on the line is the figure's body.

The strict contract

Scatter fires only under an exact contract, and that strictness keeps ordinary prose from being mis-read:

  • The id is [A-Za-z][A-Za-z0-9_-]*.
  • The value is a bare word [A-Za-z0-9_-]+, a "quoted"string (spaces allowed), an italic or bold run, or any @node (bare, parens, or with a closer).
  • There is no whitespace anywhere — including between the : and the value.
  • The byte just before the id must be the start of the line or a non-word, non-backslash character.

@thing name:Tauraj thing@ lifts name to Tauraj; a quoted value carries spaces — @thing mode:"complex value" thing@ (body-scatter-quoted). Multiple tokens lift while residual prose remains (body-scatter-multi), and a repeated key is last-one-wins (body-scatter-override).

Because the value may itself be a node or an emphasis run, scatter lifts them as sibling content: italic (body-scatter-italic-value), bold (body-scatter-bold-value), a closer-form node (body-scatter-node-value), a self-closing parens node (body-scatter-self-closing-node-value), a node carrying its own params (body-scatter-multiple-node-params), a node then a bare token (body-scatter-node-then-bare), and overriding a value with a node (body-scatter-override-with-node).

A space breaks the contract

Add a space after the colon and the token is prose again — @thing key: value thing@ lifts nothing. A real inline node on that line stays a node; only the scatter-lift is suppressed.

To keep a literal in prose, escape the colon — name:Tauraj suppresses the lift and the backslash is stripped from the output (body-scatter-escape).

Form-fill body

When a node's body reads like a form — every line a key: value pair — the body becomes the parameters. It is the most readable form for many-field templates (citations, cards, front-matter), and the only call form that can carry multi-line values.

@cite
  author: Smith
  year: 2024
cite@

This binds author and year; year is type-probed to the number 2024.

What triggers form-fill

A body is read as form-fill when all three hold: it contains a newline; its first non-blank, non-comment line matches <id>:; and it has at least two content lines. A single content line is not form-fill — it falls through to colon-scatter. That two-line threshold is the rule most worth remembering.

The line grammar

Each line is <id>: <value>. One optional leading space after the colon is trimmed, trailing whitespace is trimmed, and ~ comments and blank lines are ignored. A line that does not match the shape is E_MALFORMED_FORM_FIELD.

A quoted value protects commas and forces a string type. And — one surprise worth internalising — emphasis markers in a value stay literal, because the inline parser is bypassed: _title_ renders as the three characters, not italic.

Multi-line values

Give a key an empty same-line value and then indent the content deeper: the block is consumed, common indentation is stripped, and lines join with newlines.

@note
  body:
    First line.
    Second line.
note@

That binds body to First line. + newline + Second line.. Blank lines inside the block are kept (value-multi-paragraph); a dedent ends the value (value-block-end-by-dedent); and — a sharp edge — an indented line that looks like key: is treated as content, not a new field (value-indented-key-shape-is-content). An empty value with no indented block simply moves on to the next field (value-empty-then-next-field).

The identical block grammar appears in definitions — a block record def and a value-block def both collapse to a record:

#point
  x: 3
  y: 7
point#

#point:
  x: 3
  y: 7
!!

(fixtures 23-form-fill/block-record-def, value-block-record-def; a realistic +#bibliography value-block wrapping a form-fill with quoted commas is bibliography-style.)

Parens

Parens — @name(p, q) — are the compact, self-closing form: the closing ) ends the node, so there is no name@ closer to write. Params are comma-separated, and colon or space separates each key from its value. Use parens for one or two short parameters and no body — @badge(tone good), @figure(src lamp.png, caption The lens).

@badge(tone good)
The keeper logged the lamp at dawn.

The badge self-closes at ); the sentence below is its own paragraph . The key/value separator can be a space or a colon interchangeably — @scene(mood calm, location harbour) and @scene(mood: calm, location: harbour) bind identically (multiple-params, colon-separator). A multi-word key joins with a spaced hyphen — @panel(background colour - dark slate) (hyphen-multi-word-key).

  • Inner whitespace is trimmed@x( a , b ) binds a and b (inner-whitespace).
  • A trailing comma is tolerated@x(a,) (trailing-comma).
  • Empty parens are legal and distinct from a bare@x carries none (empty-parens).

Parens carries no body of its own

If you follow a parens call with body content and a closer, a core node still attaches the params and renders the body — @aside(tone wry) the keeper said nothing aside@ works. But a template definition with no body slot has nowhere to put a following body, so it is silently dropped. For a body, use pipes@aside |tone wry| the keeper said nothing aside@.

A parens call must also stay on one line. Wrapping it across a newline breaks it — the remainder, including the ), leaks into the surrounding text (multi-line-call). A value long enough to want wrapping is a sign to use form-fill or record-arg.

Pipes

Pipes — @name |slot| — are the only form that composes with a body. A slot can sit on the opening line or scatter through the body, and duplicate keys resolve last-one-wins. Reach for pipes when a node has both parameters and content, or to switch a value partway through — otherwise prefer record-arg or colon-scatter.

@figure |src lamp.png| |full width!| |caption The second-order lens| figure@

That passes a named src, a full width flag, and a multi-word caption.

Pipes may appear on the opening line, or anywhere inside the body. When a key repeats, the last one wins — which makes pipes the form for switching a value mid-body:

@scene |mood calm|
The harbour was still.
|mood tense|
Then the bell rang.
scene@

Here mood ends up tense. You can also put several pipes on one line — @x |a one| |b two| |c three| x@ (multiple-pipes-per-line).

An empty || inside a body contributes no parameters (empty-pipe), and ordinary prose that merely contains pipe characters stays literal — The signal flag read red white red. renders as text (pipe-in-body-text).

The multi-line-value bug

A newline inside a single |…| value does not span lines — the pipe never closes and you get E_UNCLOSED_NODE. Keep each pipe value on one line; for a genuinely multi-line value, use a form-fill body or a record-arg brace.

The mixing rule

Two forms on one node behave differently depending on which two.

  • Parens and pipes MERGE. @card(title Alpha) |status draft| card@ binds both — verified. This is not an error.
  • Record-arg is exclusive. Combining {…} with parens or pipes collected before it is a hard error, E_MIXED_PARAM_SOURCE — verified for both @card |status Y| { title X } and @card(status Y) { title X }.
  • Record-arg then pipes drops the pipes. @card { title: X } |status Y| card@ does not merge either: the record self-closes at }, the trailing pipes are discarded, and the now-unfilled capture surfaces as E_MISSING_RECORD_FIELD — verified.

The greedy-bind hazard

A bodyless pipes call — pipes with no name@ closer on its line — greedily swallows forward to the next matching close when nothing separates them:

@card |title X|
@card |title Y| card@

This is E_UNCLOSED_NODE: the first call keeps reading until it finds card@, which belongs to the second. The fix is any one of: give the call an explicit card@ closer, put a blank line after it, or place a bodyless pipes call last.

@card |title X|

@card |title Y| card@

With the blank line, each renders on its own. This is why pipes is the noisiest form and why the other forms self-close.

Common mistakes

  • Assuming record-arg merges with pipes — it errors, or drops them.
  • Assuming parens and pipes conflict — they merge.
  • Leaving a bodyless pipes call before another same-named node (greedy-bind).
  • Leaving an unquoted comma inside a record-arg value — quote it.
  • Adding a body when a parens template has no body slot — it is dropped; use pipes.
  • Wrapping a parens call, or a single pipe value, across lines.
  • Expecting a one-line body to be form-fill — it is colon-scatter (needs a space-free ).
  • Forgetting that emphasis markers are literal inside form-fill values.
  • An accidental mid-prose lifting a stray param — escape or space it.
  • Writing |full| expecting a flag — it is a positional value; a flag needs the trailing !.

See also

Node use & the bare reference

· Type-classified scalars · Data: records · Escapes & quoting · Defining nodes · Gotchas & ambiguity