Cutdown Syntax — Quick Reference
Cutdown is a markup language that produces an AST. There is no HTML output. Parsing a complete input snapshot is single-pass with bounded lookahead (≤ one line at block level, ≤ end of line at inline level); committed text is never re-lexed or re-inline-parsed (§9). A streaming implementation may retain an unresolved suffix, but every decoded Unicode-scalar prefix follows the same ordinary Cutdown rules (§16).
~~~ MetaBlock (Frontmatter) format=yaml
...
~~~
== ... Heading (Section) Level 2
- ... ListItem (unordered)
1. ... ListItem (ordered)
- [+] ... TaskItem (checklist) checked=true
/path FileRefBlock
 ImageBlock
> ... QuoteBlock
$$$ MathBlock
...
$$$
:::div NamedBlock name=div
...
:::
^^^ SpoilerBlock
...
^^^
| AA | BB | Table
^ ... Caption / Attribution (no node)
[^...]: ... RefDefinition
### CommentBlock (hidden by default)
...
###
Document Model
Each Cutdown file produces a Document with Pages. So it has at least one Page, even if empty. Pages contain blocks and inline elements. PageBreaks --- at top level and Meta fences ~~~ produce Page boundaries.
Document
└── Page[]
├── meta: Meta | null
└── children: (Section | Block)[]
- Every document has ≥ 1 Page.
---→ always closes the current Page (Ghost Page if empty) and opens a new one. Produces no node.Metablock → closes the current Page and opens a new Page carrying it asmeta— unless it is the first pagination-relevant item of the document, in which case it fills the initial Page'smeta.- Empty Page (
meta: null,children: []) = Ghost Page (valid).
The schema also admits synthetic segments that no parse produces (currently Fragment, §14): parsers never emit them, consumers must accept them.
Block Elements
Blocks are separated by blank lines. Block elements cannot interrupt a paragraph.
Paragraph → Paragraph
Any non-blank lines not matching another block. A soft break (single newline) is folded to zero — lines concatenate directly, no character emitted; trailing spaces before the break collapse to a single space, preserved as the explicit word separator; at a block boundary the space is dropped (§12). \ at line end → TextBreak.
Modern computers are remarkably powerful, but certain classes of problems remain difficult. For example, simulating molecular interactions or solving large optimization tasks may require enormous computational resources.
Researchers once believed that some shortcuts would dramatically reduce computational cost, but many of those expectations are ~~overly optimistic~~ — a point worth flagging for the next revision.
Headings → Section
= Level 1
== Level 2
=== Level 3 (up to ========= level 9)
Must be preceded by a blank line (or start of document / block container). Inline content allowed.
Sections are not parsed — they are derived by a fold (§9.5.1): a Section spans from its heading to the next heading of level ≤ its own within the same container, or the container's end. Section scope never crosses a container boundary (NamedBlock, QuoteBlock, ListItem). Skipped levels (= then ===) nest under the nearest shallower open Section; the written level is preserved, no intermediate Sections are synthesized, no diagnostic.
= Quantum Computing {id="quantum-intro" category="science"}
== **Why** Classical Computers Struggle {id="limits"}
Meta Block (Frontmatter) → Meta
~~~yaml
key: value
~~~
Formats: yaml (default), toml, json. Content is raw string. Fills Page.meta. No attributes. Used only on top level. Unclosed → warning CDN-0002.
Page Break → new Page (no node)
---
A top-level line beginning exactly ---. Closes the current Page (Ghost Page if empty), opens a new one, and produces no AST node. Everything after the leading --- — surplus hyphens, {attrs}, text — is dropped with a diagnostic (CDN-0016). Inside block containers a blank-line-surrounded --- is a literal paragraph (Paragraph(Text("---")), CDN-0017). Cutdown defines no thematic-break (horizontal-rule) element.
Lists → List / ListItem / TaskItem
- unordered item ← (marker: '- ')
- nested (2-space indent per level)
1. ordered item ← (marker: '{n}. ')
2. second item
- [ ] task item ← (marker: '- [{x / X / + / space}] ')
- [x] nest task item ← ({ checked: true})
- [+] rtl-safe task ← ({ checked: true}, bidi-neutral marker)
Only - for unordered; only {number}. delimiter for ordered. Actual numbers ignored. Nesting is stack-based and column-relative (§10.5): any positive indent delta opens a child; 2 spaces per level is the recommended style. Blank line + col-0 content ends the list; blank line + indented content is absorbed → loose: true (item content block-promoted).
File Reference → FileRef, FileRefGroup
/path/to/file.ext {attrs}
/path/to/image.png
Line starting with /. Known groups (image/video/audio) auto-wrapped in FileRefGroup. Fragment: /page.md#section-id. Query: /page.md?key=value.
Image Block → ImageBlock
 {attrs}
Line starting with ![. Block-level. Consecutive image lines wrapped in FileRefGroup. Image can be declared inside Inline context as well (as ImageInline).
Quote Block → QuoteBlock
> content
> more content
>> nested quote
Every line must start with >. Nesting by counting > chars.
Code Block → CodeBlock
```language {attrs}
literal content — no inline parsing
```
Language defaults to "text". Fixed 3-backtick fence. No nesting. Unclosed → warning CDN-0001.
Math Block → MathBlock
$$$ {attrs}
\LaTeX formula
$$$
Content is literal. Unclosed → warning CDN-0003.
Named Block → NamedBlock
:::block-name {attrs}
content (any blocks, including nested :::)
:::
::: + name required — nameless ::: opener → Paragraph, warning CDN-0013. Closing ::: alone. Unclosed → warning CDN-0004. First content line establishes base indent (stripped from all lines).
Spoiler Block → SpoilerBlock
^^^ {attrs}
content (any blocks, including nested :::, but not nested ^^^)
^^^
Fixed 3-caret fence. Content is parsed as blocks (the only XXX-fence with non-literal body — code/meta/math are literal; spoiler hides meaning, not structure). Closing ^^^ alone. SpoilerBlocks do not nest. Unclosed → warning CDN-0005. First content line establishes base indent. Semantic variants via attributes ({.nsfw}, {.redacted}, etc.).
Tables → Table
Two variants: Pipe (kind: "pipe", first line starts with |) and Multiline (kind: "multiline", first line starts with +-). Standard Markdown (GFM) pipe tables parse unchanged.
| Cell A | Cell B | ← pipe table (no header, all rows type: "Row")
| Name | Score | ← pipe table with header
|:-------|------:| ← header separator; also sets alignment
| Alice | 42 | ← type: "Row"
+----------+----------+ ← multiline grid
| Header A | Header B |
|:---------|----------| ← header separator (colon = left align col 0)
| Cell A | Cell B |
+----------+----------+
Header separator: A | row whose every cell is an alignment pattern (≥ 3 dashes; the minimum keeps | - | placeholder rows as content) marks the preceding rows as type: "Header". Alignment taken from the first header separator only. In multiline it is a full separator row (closes the logical row, defines column boundaries). A + row never marks headers — colons in it are inert: in pipe tables + rows are ignored entirely; in multiline they delimit logical rows / body sections.
Alignment patterns: :--- left, ---: right, :---: center, ---, comma, ---. decimal, ---- left (default).
Multiline cells: All | content lines between two consecutive separator rows (+ rows or header separators) form one logical row. Cell content is Block[] (full block context, like ListItem). Cells soft-join multi-line content (space between lines). Trailing | optional. Column count = max() across rows.
Leading/trailing | required in pipe rows. +- required as first line for multiline.
Reference Definition → RefDefinition
[^ref-id]: inline content
Must start at line start. When the same ref is defined more than once in a document, resolution uses the last definition (last wins).
Inline Elements
Parsed in source order. An unclosed opener degrades by its class (§9.4.1):
- Symmetric doubled delimiters (
**__~~^^`$$""''): the opener alone becomesText; parsing continues — constructs after it survive.**a __b__ c→Text("**a ")+Emphasis(b)+Text(" c"). - Bracket-like openers (
[, |
Link(external) |
|
[text][page] |
Link(page) |
target is a page, Wiki-like syntax |
[text][#tag] |
Link(tag) |
resolved by consumer in Tag's namespace |
[text][^ref] |
Link(ref) |
resolved by consumer on page in Ref's / ID's namespace |
[text][@cite] |
Link(cite) |
resolved by consumer in outer Ref vocabulary |
 |
ImageInline |
|
::name {attrs} |
Span |
Empty. :: without name = literal. |
{{key}} |
Variable |
Key is ID_LITERAL. Empty/invalid key → literal text + CDN-0015. Unclosed {{ → verbatim slice. |
## … <EOL> |
Reflection entry on block | Line comment, runs to EOL. Payload stored in block.reflection[]. Single # = literal. Literal ## = \##. |
\ at line end |
TextBreak |
Cross-type nesting allowed (e.g. **__text__**). Same-type nesting not allowed (greedy close).
Inside inline context run of 3 (***, ___, ~~~, ^^^, ```, $$$, """, ''') = 2-delimiter opener + 1 literal. For ### at inline position: ## (line comment, runs to EOL) + the trailing # becomes the first character of the payload text.
Caption / Attribution
Syntax:
| Captionalb | col B |
^ Table caption text
A line starting with (^ ) immediately after a captionable block (no blank line) enriches that block with a caption field. No separate AST node is produced.
| col A | col B |
^ Table caption text → Table { caption: [...] }

^ Figure caption text → ImageBlock { caption: [...] }
```javascript
code here
```
^ Listing caption → CodeBlock { caption: [...] }
> Quoted text here.
^ Source attribution → QuoteBlock { attribution: [...] }
Captionable blocks: Table, ImageBlock, CodeBlock, MathBlock, FileRef, FileRefGroup, NamedBlock, SpoilerBlock. QuoteBlock uses attribution instead of caption.
- Blank line between block and
^→ no binding;^becomes aParagraph(CDN-0008). - Second
^line (slot already filled) →Paragraph(CDN-0008). {attrs}on a caption line → literal text (CDN-0009).- Escape:
\^at line start suppresses the opener.
Attributes
{#id .class key=value key="spaced value"}
Attach after their target on the same line (or next line, no blank line between). Whitespace between the target and the consumed {attr} is stripped — == Heading {.x} → heading text is "Heading", not "Heading ".
Block opening lines (headings, named blocks): last {...} on the line → claimed by the block. Earlier {...} attach to preceding inline elements. Empty {} as last token = no attrs on block.
Scope-chain (Rule B): trailing {...} sequence at end of inline context distributed right-to-left through the node hierarchy. Last {} → outermost container; preceding {} → next inner level. Excess front {} silently dropped (warning CDN-0011).
- item {.a}{.b} → List({.b}, ListItem({.a}, Text("item")))
| td | {.a}{.b} → Table({.b}, Row({.a}, ...)) ← last row only
| td | {.a} → Table({.a}, Row(...)) ← mid-table: 1 slot (Row only)
{{ always matched before { (longest opener wins).
Literal-span idiom. { opens an attribute scan to the matching } or end of line. Invalid attr grammar or no } → the entire slice (braces included) is one verbatim Text run, never inline-parsed: {a **b**} → Text("{a **b**}"). Note: extending the attribute grammar later is a breaking change for text using this idiom.
Comments
Cutdown has two comment constructs. Both are hidden by renderers by default.
| Form | Result | Notes |
|---|---|---|
# |
literal text | Single # does nothing — written exactly as typed. |
## … <EOL> |
Reflection entry on block |
Line comment. Recognized at line-start AND mid-line. Runs to EOL. Stored in block.reflection[], not in inline stream. Literal inside ``, $$, and quoted attribute values. |
### … ### |
CommentBlock segment |
Block comment. Bare ### opener, bare ### closer at same column. Opaque content (no parsing). No [name], no {attrs}. |
# literal hash, not a comment
## line comment → stored as reflection on nearest block
foo bar ## tail → Text("foo bar ") + reflection entry on block
###
opaque block — any content captured raw
###
Literal ## in normal text: \## or #\#. Unclosed ### → warning CDN-0006.
Opaque to other delimiters. ## consumes to \n, swallowing any ], }, |, or other closer in its path. An unclosed inline opener before ## degrades per its class (§9.4.1) — for bracket-like openers the ## cut terminates the verbatim slice. Example: [text ## here](url) → Text("[text "), reflection entry "here](url)".
Transparent to attribute resolution. ## payloads are stored in reflection, never in the inline stream. No scope-chain slot is consumed. = Heading {.c} ## note → Section({class:"c"}, heading: [Text("Heading ")], reflection: [{ loc, text: "note" }]).
Standalone line comment. ## comment on its own line closes any active Paragraph or FileRefGroup and attaches to the preceding block's reflection. No preceding block → empty Paragraph { children: [], reflection: [...] }.
Table rows. A trailing ## comment after a row's content bubbles to Table.reflection, not to any Row or cell. {attrs} on a header separator row claim the Table slot (§4.8).
Input
- Input is decoded UTF-8 text. Transport bytes are decoded before Cutdown receives input; every Unicode-scalar prefix is an ordinary valid Cutdown document (§16). Identifiers are compared under NFC; the source text is never rewritten (authors SHOULD store files in NFC).
- Leading BOM skipped (first content offset = 1). Null bytes → U+FFFD in emitted
Textvalues. \r\n,\r,\nall read as line terminators. Tabs read as a single space (except inside fences). The source is never rewritten — these are interpretive rules (§7), not transforms.- Leading and trailing blank lines (whitespace-only lines) are skipped by the block phase. A document of only blanks → empty AST.
- Every node may carry
loc: { file?, start, end }— UTF-16 code-unit offsets into the raw file, end-exclusive (§14). Conformance AST comparison ignoresloc. - Inside non-opaque containers (NamedBlock, SpoilerBlock, QuoteBlock, ListItem), leading and trailing blank lines of the body are also stripped before children are parsed. Opaque containers (CodeBlock, Meta, MathBlock, CommentBlock) preserve their body verbatim.
- HTML entities (
&etc.) are not decoded — emitted as literal text.
ID_LITERAL = [a-zA-Z0-9._-] — used for all identifier tokens (block names, span names, language tags, reference IDs). ASCII-only, case-sensitive everywhere.
Escaping
\ before a special character emits that character literally. Before a non-special character, both \ and the character are emitted.
Special characters: = # * _ ~ ^ $ [ ] ( ) ! { } : - > / \ | + " ' and `
Block-opener escape (line start)
\ before any one char of a block marker suppresses the opener; line becomes a Paragraph with marker chars as literal text.
| Marker | Escape (any of) | Result |
|---|---|---|
= ... ========= heading |
\=, \==, ... |
literal |
- list |
\- item |
literal |
> quote |
\> text |
literal |
--- page break |
\---, -\--, --\- |
literal; no page break occurs |
/path file ref |
\/path |
literal |
| pipe row / header separator |
`\ | cell |` |
+- multiline table opener / separator |
\+-, +\- |
literal |
``` code fence |
\```, etc. |
literal (residual backticks still parse inline) |
~~~ meta |
\~~~, ~\~~, ~~\~ |
literal |
$$$ math |
\$$$, $\$$, $$\$ |
literal |
### comment block |
\###, #\##, ##\# |
literal |
:::name named block |
\:::name, etc. |
literal — no CDN-0013 |
^^^ spoiler |
\^^^, etc. |
literal |
## line comment (mid-line) uses \## or #\# per §2.2.
Opaque-block closer escape
Narrow per-block escape; all other \X inside opaque content is literal.
| Block | Escape | Notes |
|---|---|---|
| CodeInline / CodeBlock | \` |
three backticks in row → \`\`\` |
| Meta | \~ |
any one of three closer chars |
| CommentBlock | \# |
always escapes #, no run-length check |
| MathBlock | — | no escape (LaTeX owns \); literal $$$ line unsupported |
NamedBlock and SpoilerBlock are not opaque — use block-opener escape on a content line.
Precedence (highest first, per §11)
- CodeBlock fence ``` — content always literal
- MetaBlock fence
~~~— content always literal - MathBlock
$$$— content always literal - CommentBlock
###— content always literal (opaque) - Inline code `` — content literal except
\` - Line comment
##— no closer, runs to EOL; payload stored in blockreflection; acts as the terminator for open inline constructs, which degrade per their class (§9.4.1) - Escape
\x— resolved before delimiter matching - Links
[...](...)and images— matched before emphasis runs - Inline math
$$— matched before emphasis; content literal - Strong
**, Emphasis__, Highlight~~, Spoiler^^, QuoteInline""''— source order, greedy - Named span
::name— matched after emphasis - Variable
{{key}}/ Attributes{...}— longest opener wins ({{before{), then source order