Gotchas & anti-patterns
The page to read when you feel like you are fighting the parser. Most of the time Wit gets out of your way — natural prose that resembles syntax stays prose. But a handful of constructs do mis-fire, a few are outright bugs to route around, and the last section is the do/avoid checklist. Each item is re-verified against the current build.
Prose that safely looks like syntax
These all stay prose — the parser is careful not to fire by accident:
- Em-dash is not the parameter separator. The separator is the
exact three bytes space-hyphen-space; an em-dash and numeric ranges stay prose:
The key — value pair, 1995 — 2010.(em-dash-vs-hyphen). - A leading greater-than is not a blockquote. A line beginning
>is ordinary text (blockquote-leader). - A mid-sentence "digit-period-space" is not an ordered list.
It happened in 1970. It was quiet.stays one paragraph (year-period-space). - Digit-flanked asterisks are arithmetic, not emphasis.
5*6*7and2*3*4*5stay literal (mid-line-arithmetic). - A home path is not a comment.
~/Documents/notes.txtin prose is text; a line comment needs a tilde then a space(tilde-home-path). - An apostrophe after italic is safe.
word'scloses the italic and keeps the's(apostrophe-after-italic). - Nesting has no depth cap (deep-nesting), and multiple blank lines collapse to a single paragraph break (multi-blank-line).
The one that surprises everyone: an email in prose
An @ that follows a word character is not read as
opening a node — but word@ (a word pressed against a following @) is read as a node close. So an email address
written plainly in prose splits the paragraph and drops the local part:
Contact keeper@example.org for archive access.Contact
example.org for archive access.
That renders as two paragraphs — Contact and example.org for
archive access. — because keeper@ was consumed as a close
token. To show an address literally, wrap it in frozen inline code.
The colon-scatter false positive
A space-free token buried in real prose still lifts as a
parameter:
@thing the variable name:Tauraj is bad thing@
name is lifted even though you meant it as prose. Fix by escaping the colon
(name:) or adding a space (name: …). The same bites
URLs, ratios, and clock times written inside a scatter body.
The record comma trap
An unquoted comma inside a record value splits the field — E_MALFORMED_RECORD:
#cite: { author - Boud, D., year - 2024 } ~ splits into a keyless field #cite: { author: "Boud, D.", year: 2024 } ~ quote to keep the comma
Glued statements are not recognised
A (if …), (each …), or (end) is
recognised only when its ( follows whitespace or a line start. Glue
one to a preceding ) and it renders literally:
(each @xs as x)(if @x is a)Y(end)(end) ~ inner (if …) prints as text (each @xs as x) (if @x is a) Y (end) (end) ~ works
Keep a space or newline between adjacent statements.
The greedy-bind and mixing traps
A bodyless pipes call greedily swallows forward to the next matching close — E_UNCLOSED_NODE. Give it an explicit name@, a blank
line after it, or place it last. And record-arg is exclusive: combining { … } with parens or pipes collected before it is E_MIXED_PARAM_SOURCE. Both are covered in Parameters.
A comment as a container's first line
A ~comment on the first line of a container body,
with content right after, collapses the following blocks into one paragraph:
@section ~ note @h2 T h2@ Body section@
T
BodyEmits the heading wrongly nested in a paragraph — <section><p><h2>T</h2>Body</p></section>. Fix: a
blank line after the comment → <section><h2>T</h2><p>Body</p></section> (see Comments; block comments close with the exact ~~/ fence).
Multi-word key access via spaces fails
A data-access segment ends at a space, so a multi-word key written with spaces
does not resolve — @keeper.years at post reads only @keeper.years. Reach multi-word keys through the space-free camel
or snake spelling (see Data access).
Index access renders unresolved
@list.0 and @rec.0.field currently emit <span class="wit-unresolved"> rather than the element. Fix: reach into collections with (each) or @table |rows @ref|.
The loop variable is a reference not a capture
Inside (each @c as item), reference the element with @item — not ::item::, which is
capture interpolation and stays unresolved in a loop.
A single-line def with captures
A single-line #def that declares ||captures|| renders the literal capture list. Fix: use the block form for any
def with captures.
Form-fill needs two or more lines
A bare opener with a single key: value line is read as prose, not
a form-fill call. Fix: add a second field, or use the record-arg
form. A key: with an empty same-line value consumes the block
beneath; continuation must be strictly deeper indented.
The multi-line-call bugs and two cosmetic quirks
- A parens call across a newline breaks — the remainder leaks into the text . Keep parens calls on one line.
- A newline inside a single
|…|pipe value does not span lines —E_UNCLOSED_NODE(06-parameters-pipes/multi-line-value). Use a body form for multi-line values. - Following a template that has no body slot with body content silently drops that body (05-nodes-parens/parens-then-body). Pass bodies with pipes.
- Cosmetic:
@figurecan wrap a block@figcaptionin a<p>, and a frozen@@tablewill not resolve@refrows — use a plain@tablewhen rows read from data.
Delimiter and param traps
| Trap | What happens | Fix |
|---|---|---|
| colon delimiter in a data-def record | LSP rejects it as E_MALFORMED_RECORD | use the hyphen delimiter |
| multi-word colon value in parens | breaks the call | quote it or use space style |
| a dotted path inside parens or record values | confuses close detection | use pipes or form-fill for path values |
| an at-sign inside record values like an email | breaks parsing | keep at-strings out of record values |
Anti-patterns
| Do | Avoid |
|---|---|
| content in the body — metadata in params | stuffing content into params |
| lift duplicated lists into data plus each | hand-repeating list items |
| wrap opaque nodes in named defs | scattering raw node type X calls |
| name citations by idea | numbering citations by hand |
| hyphen delimiter in data-defs | colon delimiter in data-defs |
| conditionals and each over scripts | reaching for scripts first |
| place pipes-form calls last | leaving an open pipes call before a body use |
| schema-array tables | inline CSV for structured data |
The fixes, at a glance
- Email or
word@in prose → wrap it in frozen inline code. - Colon false positive →
:or a space. - Record comma → quote the value.
- Leading comment in a container → blank line after it.
- Bodyless pipes → explicit
name@, blank line, or last. - Glued statements → keep a space.
- Spaced multi-word access → camel or snake spelling.
- Index access →
(each)or a data-driven@table. - Loop element →
@item, not::item::. - Def with captures → block form.
- Single-line form-fill → add a field or use record-arg.
- Data in prose →
@field, not{{…}}.
See also
Parameters· Colon-scatter · Escapes · Data access · Conditionals · Iteration · Errors