= Cutdown Markup Language Specification

- **Status:** Draft
- **Version:** 0.10.0
- **Date:** 2026-09-02
- **Inspired by:** CommonMark, Djot, Carve
- **Versioning policy:** [``policies/versioning-policy.md``](/latest/policies/versioning-policy)
- **Change publication policy:** [``policies/change-publication-policy.md``](/latest/policies/change-publication-policy)
- **Conformance policy:** [``policies/conformance-policy.md``](/latest/policies/conformance-policy)
- **Parser profile policy:** [``policies/parser-profile-policy.md``](/latest/policies/parser-profile-policy)
- **Diagnostics policy:** [``policies/diagnostics-policy.md``](/latest/policies/diagnostics-policy)
- **Diagnostic code registry policy:** [``policies/diagnostic-code-registry-policy.md``](/latest/policies/diagnostic-code-registry-policy)
- **Diagnostic code registry:** [``policies/diagnostic-code-registry.md``](/latest/policies/diagnostic-code-registry)
- **Capability policy:** [``policies/capability-policy.md``](/latest/policies/capability-policy)
- **Canonical serialization policy:** [``policies/canonical-serialization-policy.md``](/latest/policies/canonical-serialization-policy)
- **Compatibility fallback policy:** [``policies/compatibility-fallback-policy.md``](/latest/policies/compatibility-fallback-policy)
- **Conformance corpus governance:** [``policies/conformance-corpus-governance.md``](/latest/policies/conformance-corpus-governance)
- **Decision authority policy:** [``policies/decision-authority-policy.md``](/latest/policies/decision-authority-policy)
- **Profile source policy:** [``policies/profile-source-policy.md``](/latest/policies/profile-source-policy)
- **Compliance levels policy:** [``policies/compliance-levels-policy.md``](/latest/policies/compliance-levels-policy)
- **Compliance evidence freshness policy:** [``policies/compliance-evidence-freshness-policy.md``](/latest/policies/compliance-evidence-freshness-policy)
- **Compliance failure response policy:** [``policies/compliance-failure-response-policy.md``](/latest/policies/compliance-failure-response-policy)
- **Cross-implementation validation policy:** [``policies/cross-implementation-validation-policy.md``](/latest/policies/cross-implementation-validation-policy)
- **Reference parser status policy:** [``policies/reference-parser-status-policy.md``](/latest/policies/reference-parser-status-policy)
- **Governance review policy:** [``policies/governance-review-policy.md``](/latest/policies/governance-review-policy)

== Abstract

**Cutdown** is a lightweight markup language with a closed, finite syntax, parsed in a single forward pass so a document renders as it arrives rather than after it ends. Two identical characters wrap inline text; three open a block. Thus, the parser resolves every construct when it is first encountered.

Cutdown prioritizes **unambiguous parsing**, **consistency**, and **implementability**. Every syntactic construct is locally deterministic. The parser never backtracks.

It has no canonical HTML output — consuming applications interpret and render the AST.

== Table of Contents

1. [Conventions](/latest/#s-1) — Identifier charset, Segment, Block type, Inline type
2. [Comments](/latest/#s-2) — ``##`` line comment (Block.Reflection), CommentBlock (``###``)
3. [Document Model](/latest/#s-3) — Document, Page
4. [Block Segments](/latest/#s-4) — Paragraph, Section, Meta, CodeBlock, MathBlock, QuoteBlock, List, ListItem, TaskItem, Table, ImageBlock, PageBreaker, FileRef, FileRefGroup, NamedBlock, RefDefinition, SpoilerBlock, CommentBlock
5. [Inline Segments](/latest/#s-5) — Text, Emphasis, Strong, Highlight, Spoiler, CodeInline, LineBreak, Link, ImageInline, Mark, MathInline, Variable, QuoteInline
6. [Universal Attributes](/latest/#s-6)
7. [Input Interpretation](/latest/#s-7)
8. [Escaping](/latest/#s-8)
9. [Parsing Algorithm](/latest/#s-9)
10. [Block Structure and Block Boundaries](/latest/#s-10)
11. [Precedence Rules](/latest/#s-11)
12. [Whitespace Rules](/latest/#s-12)
13. [Special Character Reference](/latest/#s-13)
14. [AST Node (Segment) Reference](/latest/#s-14)
15. [Name and Compliance](/latest/#s-15)
16. [Streaming Conformance Profile](/latest/#s-16) — decoded-character snapshots, end-of-block semantics, and profile evidence
17. [Canonical Form](/latest/#s-17) — the one spelling a writer emits where the grammar accepts several

== 1. Conventions

The keywords **MUST**, **MUST NOT**, **REQUIRED**, **SHOULD**, **MAY** are used as defined in \[RFC 2119].

**Examples** are shown as:

```
Input:
  <cutdown source>

AST:
  <segment representation>
```

**The arrow symbol (``→``)** separates a construct or condition from what it produces. In examples, the left side is Cutdown source and the right side is the AST notation defined in [§14](#s-14).

**Naming characters.** A character used as a **noun** is named on first use in a numbered section, symbol first and name in parentheses — "a single ``#`` (octothorpe)", "the ``^`` (caret)" — and thereafter by symbol alone. One naming per section per character; do not repeat it. Multi-character markers (``##``, ``~~~``, ``:::``) are constructs, not characters, and are named by construct name. [§13](#s-13) is the register of names. This convention applies to the numbered sections only; ``SYNTAX.md`` and ``README.md`` are exempt.

=== 1.1 Streaming terms

A **Unicode scalar value** is a Unicode code point other than a surrogate code point. It is the character unit used by the streaming conformance profile ([§16](#s-16)). This does not change source locations: ``loc`` offsets remain UTF-16 code-unit offsets.

An **input snapshot** is the complete decoded Cutdown source available to a parser at one instant. A parser MUST treat every input snapshot as an ordinary Cutdown document, including a snapshot that ends within unfinished markup.

An **end of block** is a boundary inferred by the ordinary Cutdown grammar: a completed block boundary, a closing fence, or end of input. It is not a token or event in Cutdown source.

=== 1.2 Identifier Character Set

Throughout this spec, ``ID_LITERAL`` refers to the following ASCII character class:

```
ID_LITERAL = [a-zA-Z0-9._-]
```

This charset is used for all identifier-like tokens: block names, mark names, code language tags, reference definition IDs, and variable keys. It is ASCII-only. Matching against ``ID_LITERAL`` is case-sensitive everywhere in this specification.

``PATH_LITERAL`` extends ``ID_LITERAL`` with the forward-slash character:

```
PATH_LITERAL = [a-zA-Z0-9._/-]
```

``PATH_LITERAL`` is used for path-like values: page link targets, tag link targets, and file reference paths.

=== 1.3 Segment

A **segment** is any node in the Cutdown AST — Block or Inline. This term is used throughout the spec when no distinction between block and inline is needed.

=== 1.4 Block Type

A **Block** segment is any node that occupies a full line-level slot in the document.

```
Block =
    | Paragraph
    | Section
    | CodeBlock
    | MathBlock
    | QuoteBlock
    | List
    | Table
    | ImageBlock
    | FileRef
    | FileRefGroup
    | NamedBlock
    | SpoilerBlock
    | CommentBlock
    | RefDefinition
```

Container blocks carry a ``children`` array — ``Block[]`` for block containers (``Section``, ``QuoteBlock``, ``NamedBlock``, ``SpoilerBlock``), ``Inline[]`` for blocks whose content is inline (``Paragraph``, ``RefDefinition``). Leaf blocks carry no children. Most blocks carry ``attributes: Attribute[]``.

=== 1.5 Inline Type

An **Inline** segment is any node parsed within inline content.

```
Inline =
    | Text
    | Emphasis
    | Strong
    | Highlight
    | Spoiler
    | Link
    | CodeInline
    | MathInline
    | QuoteInline
    | ImageInline
    | Mark
    | LineBreak
    | Variable
```

Container inlines carry ``children: Inline[]``. Leaf inlines carry no children. Most inline nodes carry ``attributes: Attribute[]``.

Wherever an AST node carries ``Inline[]``, the content was produced by the inline parsing rules ([§5](#s-5)). All inline contexts are explicitly marked "parsed by inline rules."

=== 1.6 Block Scope

A **block scope** is one flat sequence of sibling blocks. A document has one block scope at its root, and one inside the child list of every block container (``ListItem``, ``TaskItem``, ``QuoteBlock``, ``NamedBlock``, ``SpoilerBlock``).

A rule described as resolved __within the current block scope__ looks only at that sequence and never past its container boundary. This applies to reflection attachment ([§2.2](#s-2-2)), caption binding ([§6.2](#s-6-2)), and the sectionization fold ([§9.5.1](#s-9-5-1)).

== 2. Comments

Cutdown has two comment constructs that share the ``#`` (octothorpe) symbol and follow the doubled/tripled-delimiter rule ([§10.4](#s-10-4)). ``##`` stores comment payloads as **Reflection entries** on blocks; ``###`` (``CommentBlock``) produces a separate AST segment. Both are hidden from rendering by default.

=== 2.1 Single ``#`` is literal

A single ``#`` is literal text in all positions. There is no whitespace rule, no line-start rule, no special treatment.

```
# foo            →  Paragraph([Text("# foo")])
foo # bar        →  Paragraph([Text("foo # bar")])
```

=== 2.2 Double octothorpe ``##`` — Line Comment (Reflection)

A double ``##`` opens a line comment that runs to the end of the line. It is recognized at line-start AND mid-line.

```
## a whole-line comment
foo ## trailing comment
```

- The ``##`` and everything up to (but not including) the next ``\n`` is the **comment payload**. The ``##`` and one leading space (if present) are stripped; the remainder is the ``text`` value **verbatim** — trailing whitespace inside the payload is preserved, since the payload is opaque ([§12](#s-12)'s trailing-space rule does not reach inside it).
- **Opaque to all other delimiters.** Once ``##`` is recognized, the parser consumes characters to ``\n`` blindly. It does NOT honour link-text ``]`` (right bracket), table cell ``|`` (pipe), attribute ``}`` (right brace), or any other inline construct's closer. An unclosed opener before ``##`` degrades to literal per [§9.4](#s-9-4).
- **``##`` boundaries are detected during Phase 2 preprocessing** ([§9.2](#s-9-2)), before block classification. Block classification operates on the pre-``##`` substring of each line.
- ``##`` is **not** recognized inside opaque block contexts: ``CodeBlock``, ``MathBlock``, ``Meta`` content, or ``CommentBlock`` content. For those blocks only the **opener line** (the first line of the fence) and the **closer line** (the closing fence) are scanned.
- ``##`` is **not** recognized inside inline opaque contexts: ``CodeInline``, ``MathInline``, and quoted attribute values.
- A literal ``##`` in normal text is written ``\##`` or ``#\#`` ([§8](#s-8)).

**``##`` does not produce an AST segment.** Instead, the comment payload is stored as a ``Reflection`` entry on the nearest enclosing block:

```typescript
interface Reflection {
  loc: Loc       // source range of the ## payload (raw-file UTF-16 offsets, §14)
  text: string   // payload after ##, one leading space stripped
}
```

Every block type carries ``reflection: Reflection[] | null`` (null when no ``##`` is present). See [§14](#s-14) for the full list.

==== Trailing inline ``##`` (on a structural line)

When ``##`` appears after content on a line that belongs to a block, the payload is recorded with the ``loc`` of that payload's source range.

**Bubbling rule.** When the structural line belongs to a __child__ of a container, the payload bubbles to the **outermost container** at that scope level:

| Structural line | ``reflection`` attaches to |
|---|---|
| Table row line | ``Table`` (not ``Row``) |
| List item line | ``List`` (not ``ListItem``) |
| NamedBlock / QuoteBlock / FileRefGroup opener | that container |

==== Standalone ``## comment`` line

A line whose pre-``##`` content is empty or whitespace is a **standalone comment line**. It acts as a blank line for block-boundary purposes ([§9.2](#s-9-2)).

- Attaches to the **immediately preceding structural block** in the current scope, carrying its own ``loc``. Consecutive standalone comments accumulate in source order.
- **Closes** any active accumulation (continuing Paragraph, open FileRefGroup) before attaching.
- **Orphan** — no preceding structural block in the current scope: produces ``Paragraph { children: [], reflection: [{ loc, text }] }``. Multiple consecutive orphan lines fold into one empty Paragraph.
- Inside a container body (NamedBlock, QuoteBlock, ListItem, etc.) the same rule applies within that block scope ([§1.6](#s-1-6)): it attaches to the preceding sibling block in that scope.

**Examples:**

```
## foo                      →  no segment; preceding block gains { loc, text: "foo" }
foo ## bar                  →  Text("foo ") — block gains { loc, text: "bar" }
``code ## not``             →  CodeInline { value: "code ## not" }
\## foo                     →  Text("## foo")
### at inline               →  reflection entry; text: "# at inline"

[text ## comment](url)
  →  Text("[text ")  (the [ has no ] closer before the ## cut; the verbatim slice runs to the cut, §9.4.1)
     Paragraph.reflection += { loc, text: "comment](url)" }

| Head ## comment | Next |
  →  pre-## `| Head ` fails row grammar (no closing |).
     Block becomes Paragraph([Text("| Head ")]).
     Paragraph.reflection += { loc, text: "comment | Next |" }

| AA | BB | ## row comment
  →  pre-## `| AA | BB |` is a valid 2-cell row.
     Table.reflection += { loc, text: "row comment" }
```

Orphan:

```
Input:
  ## note with no preceding block

AST:
  - type: Paragraph
    children: []
    reflection:
      - loc: { start: 3, end: 31 }
        text: note with no preceding block
```

=== 2.3 Triple octothorpe ``###`` — CommentBlock (block comment)

``###`` opens a block comment that runs until the next bare ``###``, or end of document.

```
###
any content
including blank lines, =headings, **markup**, ``code`` —
all captured as a single opaque string
###
```

- The opener line MUST be exactly ``###`` with no name and no attributes (no ``[name]``, no ``{attrs}`` are recognized).
- The closer is the next line whose stripped content is exactly ``###``. Indentation is not compared — the rule is the same as for every other fence ([§10.4.2](#s-10-4-2)). To keep a ``###`` line inside the body, escape it ([§8.3](#s-8-3)).
- Content between opener and closer is **opaque** — captured as a raw string with no inline or block parsing. The ``\n`` between content lines is preserved; a single trailing ``\n`` is appended.
- ``###`` is recognized at Page scope AND inside containers (``ListItem``, ``TaskItem``, ``QuoteBlock``, ``NamedBlock``, ``SpoilerBlock``). Container indentation is stripped before classification ([§10.2](#s-10-2)), so the fence is recognized wherever the container's content begins.
- Unclosed ``###`` consumes to end-of-document and emits a ``CommentBlock`` with the captured content → warning CDN-0006. The opaque content gives the parser no way to observe container boundaries from inside the comment — the same rule applies to all opaque fences (CodeBlock, Meta, MathBlock).
- ``###`` inside an open ``CodeBlock``, ``MathBlock``, or ``Meta`` body is literal content (opaque siblings win).
- **Closer escape:** ``\#`` inside the body emits a literal ``#``. A line ``\###``, ``#\##``, or ``##\#`` therefore does NOT close the fence. All other ``\X`` is literal (including ``\\`` → two chars). See [§8.3](#s-8-3).

**AST type:**

```typescript
interface CommentBlock {
  type: "CommentBlock"
  text: string  // content between opener and closer; lines joined with `\n`; single trailing `\n`
}
```

**Examples:**

```
Input:
  ###
  draft note
  TODO: revise
  ###

AST:
  CommentBlock { text: "draft note\nTODO: revise\n" }
```

```
Input:
  - item one
    ###
    todo
    ###
  - item two

AST:
  List
  ├── ListItem [Text("item one"), CommentBlock { text: "todo\n" }]
  └── ListItem [Text("item two")]
```

=== 2.4 Page assembly

Only a ``Meta`` and a PageBreaker create Page boundaries.

=== 2.5 Render policy

``Reflection`` entries and ``CommentBlock`` nodes are hidden by default. Conforming renderers (HTML, PDF, markdown round-trip) SHOULD omit them. Tooling renderers (formatter, IDE preview, comment-thread plumbing) MAY read and display them. The default is normative; the opt-in is implementation-defined.

=== 2.6 Interaction with attributes

**``##`` is transparent to attribute resolution.** Because ``##`` payloads are stored in ``reflection`` rather than in the inline stream, no scope-chain slot is consumed and no "orphan-due-to-comment" condition arises. Attributes bind exactly as if the ``##`` were not present.

```
== Heading **bb** {.a}{.b} ## trailing comment
```

- ``{.a}`` adjacent to ``**bb**`` → attaches to Strong.
- ``{.b}`` is the last ``{...}`` on the heading line → claims Section.
- ``## trailing comment`` → ``Section.reflection += { loc, text: "trailing comment" }``.

Result:

```
Section({class:"b"}, level=2,
  heading: [
    Text("Heading "),
    Strong({class:"a"}, [Text("bb")])
  ],
  reflection: [{ loc, text: "trailing comment" }]
)
```

== 3. Document Model

Cutdown introduces ``Document``, ``Page``, ``Section``, and ``Block`` segments to represent the logical structure of a document. The parser produces a tree of these segments, which is then consumed by renderers or other tools.

A Cutdown document is a tree rooted at a ``Document`` segment. Every file produces at least one ``Page``, even if empty. Every file produce exactly one ``Document`` segment, even if multiple pages or multiple transclusions are present.

=== 3.1 Document

The root node of every Cutdown file. Produced automatically — there is no explicit document syntax.

**AST type:**

```typescript
interface Document {
  type: "Document"
  children: Page[]
}
```

=== 3.2 Page

A logical division within a document. Every document has at least one Page.

**AST type:**

```typescript
interface Page {
  type: "Page"
  meta: Meta | null
  children: Block[]
}
```

Pages are not parsed — they are **derived** from the root block sequence by the pagination fold, which is defined in [§9.5.2](#s-9-5-2). Two constructs drive it: ``Meta`` blocks ([§4.3](#s-4-3)) and PageBreakers ([§4.10](#s-4-10)).

A **Ghost Page** is a Page with ``meta: null`` and ``children: []``. Ghost Pages are valid and emitted as-is; consumers decide how to handle them.

== 4. Block Segments

=== 4.1 Paragraph

**Syntax:** Any non-blank lines that do not match another block construct.

```
First paragraph line 1

Second paragraph line 1.
Second paragraph line 2.
```

A contiguous run of non-blank lines not matched by any other block type.

**No block opens inside a paragraph.** Once a run has been classified as a ``Paragraph``, it continues to the next blank line (or the end of the enclosing container, or end of input). Every later line in the run is paragraph content regardless of how it begins — a heading marker, a list marker, a ``---`` separator, a table row, and every fence opener (``\`\`\```, ``~~~``, ``$$$``, ``^^^``, ``:::``, ``###``) alike. No block opener is an exception, and none emits a diagnostic: to open a block, put a blank line before it.

Two line forms are **not** block openers and are unaffected by this rule — they attach to the preceding block instead of starting one, and may follow a paragraph line directly:

- a caption line (``^ ``, [§6.2](#s-6-2));
- an attribute-continuation line (``{…}``, [§6.1](#s-6-1)).

```
Input:
  Some text
  \```js
  const x = 1
  \```

AST:
    Paragraph { children: [Text("Some text```jsconst x = 1```")] }
```

The fence opener is ordinary text, so no ``CodeBlock`` forms. Soft breaks fold to zero ([§12.1](#s-12-1)), which is why the lines concatenate with no separator inserted.

**AST type:**

```typescript
interface Paragraph {
  type: "Paragraph"
  children: Inline[]
  attributes: Attribute[]
  reflection: Reflection[] | null
}
```

- All lines are **parsed by inline rules** and concatenated. Result is ``Inline[]``.
- A single newline between lines is a **soft break** — folded to zero; lines concatenate directly with no character emitted.
- Trailing spaces before the newline collapse to a single space, preserved as ``Text(" ")`` (explicit word boundary). At a block boundary the space is dropped. See [§12](#s-12).
- A ``\`` (backslash) at line end produces a ``LineBreak`` segment (explicit line break).

**Example:**

```
Input:
  First line
  second line\
  third line

AST:
  Paragraph
  ├── Text("First linesecond line")
  ├── LineBreak
  └── Text("third line")
```

=== 4.2 Section (Heading)

**Syntax:**

```
={n} inline-content {attrs}
```

A heading creates a ``Section`` segment. Consumers receive ``Section`` segments — there is no bare ``Heading`` node in the AST. A ``Section``'s extent is not parsed; it is derived by the sectionization fold defined in [§9.5.1](#s-9-5-1).

**AST type:**

```typescript
interface Section {
  type: "Section"
  level: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
  heading: Inline[]
  children: Block[]
  attributes: Attribute[]
  reflection: Reflection[] | null
}
```

| Syntax | Level |
|---|---|
| ``= Heading`` | 1 |
| ``== Heading`` | 2 |
| ``=== Heading`` | 3 |
| ``==== Heading`` | 4 |
| ``===== Heading`` | 5 |
| ``====== Heading`` | 6 |
| ``======= Heading`` | 7 |
| ``======== Heading`` | 8 |
| ``========= Heading`` | 9 |

- Heading content is **parsed by inline rules**. Result is ``Inline[]``.
- The content continues over following lines under the paragraph continuation rules ([§4.1](#s-4-1)): the run ends at a blank line, an attribute line, a standalone ``##`` ([§2.2](#s-2-2)), or a ``^ `` caption line (§6.5). Soft breaks fold to zero, so ``= Title`` followed by ``content`` is one heading reading ``Titlecontent`` ([§12.1](#s-12-1)).
- A heading MUST be preceded by a blank line (or be the first line of the document or block container).
- The **last ``{...}`` on the heading line** is claimed by the Section (Last-Attr Rule). Earlier ``{...}`` attach to preceding inline elements. An explicit empty ``{}`` as the last token means the Section carries no attributes.
- Sections nest by level. A level-2 heading inside a level-1 section creates a child section. A level-1 heading closes all open sections and opens a new one at the root.
- Sections may appear inside block containers (``ListItem``, ``TaskItem``, ``QuoteBlock``, ``NamedBlock``). The fold runs independently inside each container, so a ``Section`` never crosses a container boundary ([§9.5.1](#s-9-5-1)).
- **Opener escape:** ``\=`` at line start suppresses heading formation at **any** level — ``\=``, ``\==``, ``\===`` ... all become ``Paragraph([Text("= ...")])``. See [§8.2](#s-8-2).

**Examples:**

```
= H1 [with link](..){.class}
→ Section { level: 1, heading: [Text("H1 "), Link(...)], attributes: { class: ["class"] } }

= H1 [with link](..){.class}{}
→ {} is last token → Section carries no attributes; {.class} attaches to Link
→ Section { level: 1, heading: [Text("H1 "), Link(..., {class:"class"})], attributes: {} }
```

=== 4.3 Meta (Front Matter)

**Syntax:** Fenced with exactly three tildes.

```
~~~format
content
~~~
```

**AST type:**

```typescript
interface Meta {
  type: "Meta"
  format: "yaml" | "toml" | "json"  // default: "yaml"
  raw: string
}
```

- Recognized formats: ``yaml``, ``toml``, ``json`` (case-insensitive). Default: ``"yaml"``.
- Content is a raw string passed as-is to the consumer. Lines joined with ``\n``; a single trailing ``\n`` is appended.
- Fills the current ``Page.meta``; if that slot is already set, opens a new Page first ([§9.5.2](#s-9-5-2)). Never appears in ``Page.children``.
- Only valid at Page scope. Inside block containers, the entire span is emitted as a ``Paragraph`` → warning CDN-0030.
- Unclosed fence → warning CDN-0002.
- No ``attributes`` field.
- **Closer escape:** ``\~`` inside the body emits a literal ``~`` (tilde) and consumes the ``\``. A line ``\~~~``, ``~\~~``, or ``~~\~`` therefore does NOT close the fence. All other ``\X`` sequences are literal. See [§8.3](#s-8-3). Opener escape: see [§8.2](#s-8-2).

**Example:**

```
~~~
title: My Document
~~~

AST:
    Page {
        meta: Meta { format: "yaml", raw: "title: My Document\n" },
        children: []
    }
```

=== 4.4 CodeBlock

**Syntax:** Fenced with exactly three backticks.

```
\```language {attrs}
content
\```
```

**AST type:**

```typescript
interface CodeBlock {
  type: "CodeBlock"
  language: string  // default: "text"
  raw: string
  attributes: Attribute[]
  caption: Inline[] | null
  reflection: Reflection[] | null
}
```

- Language identifier uses ``[ID_LITERAL]+``. If omitted (means equal empty string) should be made default to ``"text"``.
- Content is **literal** — no inline parsing. Lines joined with ``\n``; no trailing ``\n`` appended. Blank lines preserved verbatim.
- Fixed 3-backtick fence. Variable-length fences not supported. No nesting.
- Unclosed fence: content runs to end of document → warning CDN-0001.
- Legal inside ``ListItem``, ``TaskItem``, ``QuoteBlock``, ``NamedBlock``. Container indentation is stripped from content lines.
- **Closer escape:** ``\\``` inside the body emits a literal ``\``` (backtick) and consumes the ``\``. A line ``\\`\`\```, `` \`\`` ``, or `` ``\\` `` therefore does NOT close the fence. All other ``\X`` sequences are literal (including ``\\`` → two chars). See [§8.3](#s-8-3). Opener escape: see [§8.2](#s-8-2).
- **Supports caption line ([§6.2](#s-6-2)).** A ``^ text`` line immediately after the closing fence (no blank line) sets ``caption: Inline[]`` on this node.

=== 4.5 MathBlock

**Syntax:** Fenced with exactly three dollar signs.

```
$$$ {attrs}
\LaTeX formula
$$$
```

**AST type:**

```typescript
interface MathBlock {
  type: "MathBlock"
  raw: string
  attributes: Attribute[]
  caption: Inline[] | null
  reflection: Reflection[] | null
}
```

- Content is **literal** — no inline parsing. Passed as raw string to the consumer (KaTeX or equivalent).
- Lines joined with ``\n``; no trailing ``\n`` appended. Blank lines preserved verbatim.
- Unclosed fence: content runs to end of document → warning CDN-0003.
- Legal inside block containers. Indentation handling follows the same rules as ``CodeBlock``.
- **No closer escape — LaTeX owns ``\``.** Every backslash inside a MathBlock body is literal, including ``\$``. A literal ``$$$`` line inside the body therefore prematurely closes the fence; this is an accepted unsupported case (wrap such content in a ``CodeBlock`` or split the math). See [§8.3](#s-8-3). Opener escape (``\$$$``): see [§8.2](#s-8-2).
- **Supports caption line ([§6.2](#s-6-2)).** A ``^ text`` line immediately after the closing fence (no blank line) sets ``caption: Inline[]`` on this node.

**Example:**

```
Input:
  $$$ {.display #eq1}
  \int_0^\infty e^{-x^2} dx = \frac{\sqrt{\pi}}{2}
  $$$

AST:
  MathBlock {
    raw: "\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}",
    attributes: [{ id: "eq1"}, {class: ["display"] }]
  }
```

=== 4.6 QuoteBlock

**Syntax:** Lines prefixed with ``>``.

```
> content
> more content
>> nested quote
```

**AST type:**

```typescript
interface QuoteBlock {
  type: "QuoteBlock"
  children: Block[]
  attributes: Attribute[]
  caption: Inline[] | null
  reflection: Reflection[] | null
}
```

- The ``>`` (greater-than sign) prefix opens the quote. It is required on the quote's first line.
- **Lazy continuation.** A following line without ``>`` continues the quote. The line is handed to the quote's current child block and follows that block's own continuation rules — a ``Paragraph`` absorbs it as a continuation line ([§4.1](#s-4-1)), a list item absorbs it per [§10.5](#s-10-5), a ``Section`` keeps it in the section body. This is the same continuation behaviour the line would have outside the quote.
- The quote ends at a blank line, at the end of the enclosing block container, or at the end of the document. A ``>`` line after a blank line opens a new ``QuoteBlock``.
- The ``>`` prefix and one optional following space are stripped. Content is parsed as full block content.
- Nesting: ``>>`` = blockquote inside blockquote. Both ``>>`` and ``> >`` are valid. Depth = count of leading ``>`` characters. A lazy continuation line carries no ``>``, so it does not change the current depth — it continues the innermost open quote.
- **Body edge-blank trim:** After ``>`` stripping, leading and trailing blank lines inside the quoted body are stripped before children are parsed. See [§10.6](#s-10-6).
- **Opener escape:** ``\>`` at line start → ``Paragraph([Text("> ...")])``. See [§8.2](#s-8-2).
- **Supports caption line ([§6.2](#s-6-2)).** A ``^ text`` line immediately after the closing line (no blank line) sets ``caption: Inline[]`` on this node.

**Examples:**

```
Input:
  > Line 1
  > Line 2
  > Line 3

AST:
    QuoteBlock
    └── Paragraph { children: [Text("Line 1Line 2Line 3")] }
```

```
Input:
  > Line 1
  >>> Line 2
  >> Line 3

AST:
    QuoteBlock
    ├── Paragraph { children: [Text("Line 1")] }
    └── QuoteBlock
        ├── QuoteBlock
        |   └──── Paragraph { children: [Text("Line 2")] }
        └── Paragraph { children: [Text("Line 3")] }
```

```
Input:
  > Line 1
  Line 2
  Line 3

  Line 4

AST:
    QuoteBlock
    └─── Paragraph { children: [Text("Line 1Line 2Line 3")] }
    Paragraph { children: [Text("Line 4")] }
```

```
Input:
  > - item one
  still item one

AST:
    QuoteBlock
    └─── List { kind: "bullet", loose: false }
         └── ListItem { children: [Text("item onestill item one")] }
```

=== 4.7 List

**Syntax:** One or more list items sharing a common indentation level.

```
- unordered item
  - nested item

1. ordered item
2. second item

- [ ] task item
- [x] checked task
```

**AST type:**

```typescript
interface List {
  type: "List"
  kind: "bullet" | "numbered" | "checklist"
  start: number | null  // first item number for kind: "numbered"; null otherwise
  loose: boolean
  children: (ListItem | TaskItem)[]
  attributes: Attribute[]
  reflection: Reflection[] | null
}
```

- Unordered marker: ``-`` (hyphen) followed by one space. Only ``-`` is supported.
- Ordered marker: ``{number}.`` followed by one space. The first item's number sets ``List.start``; every other item's number is ignored.
- ``kind`` is determined by the **first item's marker**: ``-`` → ``"bullet"``, ``{n}.`` → ``"numbered"``, ``- [ ]``/``- [x]``/``- [+]`` → ``"checklist"``.
- ``start`` is non-null only for ``kind: "numbered"``.
- **Tight vs loose:** A list is ``loose: true`` when a blank line appears between items within the list scope. ``loose`` is an advisory flag for consumers — the parser does not alter children based on it.
- A blank line followed by a col-0 marker ends the current list and starts a new ``List`` segment.
- **Opener escape:** ``\-`` at line start → ``Paragraph([Text("- ...")])``. See [§8.2](#s-8-2).

==== 4.7.1 ListItem

**Syntax:** A list marker followed by content, with optional indented continuation lines.

```
- unordered item
  continuation line

1. ordered item
   continuation line
```

**AST type:**

```typescript
interface ListItem {
  type: "ListItem"
  children: (Block | Inline)[]
  attributes: Attribute[]
}
```

- Content on the marker line is **parsed by inline rules**. Continuation lines at the same or deeper indentation are absorbed.
- When a blank line is absorbed inside the list (loose list), content is block-promoted: ``children`` becomes ``Block[]`` (e.g. ``Paragraph``).
- Attributes follow the scope-chain rule ([§6](#s-6)).

==== 4.7.2 TaskItem

**Syntax:**

```
- [ ] content

or

- [x] content

or

- [+] content
```

**AST type:**

```typescript
interface TaskItem {
  type: "TaskItem"
  checked: boolean
  children: (Block | Inline)[]
  attributes: Attribute[]
}
```

- Marker: ``- `` followed immediately by ``[ ]`` (unchecked) or ``[x]``/``[X]``/``[+]`` (checked), then one space and content. ``[]`` with no space is not a task marker — the item is an ordinary ``ListItem`` whose content begins ``[]``. ``[+]`` is bidi-neutral; see [§17](#s-17) for which spelling a writer emits.
- Only ``kind: "bullet"`` list items may carry a checkbox. A ``kind: "numbered"`` list encountering a task marker closes and a new ``kind: "checklist"`` List segment opens.
- A ``List`` with ``kind: "checklist"`` has ``children: TaskItem[]`` exclusively; ``kind: "bullet"`` and ``kind: "numbered"`` have ``children: ListItem[]`` exclusively.
- Mix of item types introduces a new list boundary: the first item of the new type starts a new ``List`` segment.
- Follows the same multiline and block-promotion rules as ``ListItem``.

**Example:**

```
Input:
  - [ ] Buy milk
  - [x] Write spec
  - [+] Review draft
  - plain item

AST:
  List { kind: "checklist", loose: false }
  ├── TaskItem { checked: false, children: [Text("Buy milk")] }
  ├── TaskItem { checked: true,  children: [Text("Write spec")] }
  └── TaskItem { checked: true,  children: [Text("Review draft")] }
  List { kind: "bullet", loose: false }
  └── ListItem { children: [Text("plain item")] }
```

=== 4.8 Table

A table opens with a line starting with ``|`` (pipe). Standard Markdown (GFM) pipe tables parse unchanged.

```
| Cell A | Cell B |                        ← no-header table

| Name   | Score |                         ← table with header
|:-------|------:|                         ← header separator; left / right align
| Alice  |    42 |
| Bob    |    17 |
```

**AST type:**

```typescript
interface Table {
  type: "Table"
  rows: Row[]
  columns: Column[]
  attributes: Attribute[]
  caption: Inline[] | null
  reflection: Reflection[] | null
}

interface Row {
  type: "Row" | "Header"
  children: Cell[]
  attributes: Attribute[]
}

interface Cell {
  type: "Cell"
  children: Inline[]
  row: number                    // zero-indexed position in Table.rows[]
  column: number                 // zero-indexed
}

interface Column {
  type: "Column"
  align: "start" | "left" | "right" | "center" | "comma" | "decimal"  // default: "start"
}
```

``Cell`` and ``Column`` do not carry ``attributes``. Colspan and rowspan are not supported.

==== Header rows

A row whose every cell consists solely of an alignment pattern (see Column alignment below) — optionally surrounded by spaces — is a **header separator**. It marks the group of content rows immediately preceding it (since the previous separator row, or the start of the table) as ``type: "Header"``. All other content rows are ``type: "Row"``.

```
| A | B |
|---|---|    ← header separator → preceding rows become type: "Header"
| C | D |    ← type: "Row"
```

Discontiguous header sections are valid — HTML ``<table>`` supports mixed ``<thead>``/``<tbody>`` ordering. A header separator anywhere marks only the rows in the immediately preceding section.

==== Column alignment

Alignment is taken from the cell patterns of the **first header separator** in the table. Subsequent header separators do not update alignment.

| Pattern | Alignment |
|---|---|
| ``:---`` | ``"left"`` |
| ``---:`` | ``"right"`` |
| ``:---:`` | ``"center"`` |
| ``---,`` | ``"comma"`` |
| ``---.`` | ``"decimal"`` |
| ``----`` | ``"start"`` (default) |

Each pattern requires **at least three ``-``** (with the optional alignment marks shown). The minimum is a deliberate guard: a content row of single-dash placeholder cells (``| - |``) remains content, not a header separator.

A column with no corresponding position in the header separator, and every column of a table with no header separator, takes ``"start"``.

``"start"`` is not a synonym for ``"left"``. ``"start"`` follows the text direction of the content — the left edge in a left-to-right script, the right edge in a right-to-left one. ``:---`` is an explicit request for the left edge in either. A consumer that renders to CSS maps ``"start"`` to ``text-align: start`` and ``"left"`` to ``text-align: left``.

==== Table shape

**Leading ``|`` required.** Every content row and header separator opens with ``|``. It is the detection anchor ([§9.3](#s-9-3) classifies a table by ``^\|``); without it the line is a Paragraph.

**Trailing ``|`` optional.** The last ``|`` on a line is always the **closer**, never a cell separator. It closes the final cell rather than opening an empty one.

```
| A | B |      →  2 cells
| A | B        →  2 cells
| A | B | |    →  3 cells, the third empty, then the closer
```

The closer also determines how deep a trailing ``{attrs}`` chain reaches — see __Attrs scope chain__ below and [§6](#s-6).

**Column count is fixed by the first content row.** Not ``max()`` across rows. A header separator is never a content row, so it never defines the count.

- A later row with **fewer** cells is padded with empty ``Cell`` nodes to the column count. No diagnostic — nothing is lost, this is normalisation. Padded cells carry no ``loc`` ([§14](#s-14)).
- A later row with **more** cells has the surplus cells **dropped** and emits **CDN-0018**.
- A header separator **wider** than the column count likewise drops its surplus and emits **CDN-0018** — losing alignment silently is the failure the diagnostic exists to prevent.
- A header separator **narrower** than the column count is not an error; the uncovered columns take ``"start"`` per __Column alignment__ above.
- A table whose only rows are header separators has ``columns: []`` and emits **no** diagnostic — with no content row, no column count was ever established for the separator to exceed. This is consistent with ``|`` alone yielding an empty table.

==== Row content

- Each ``|`` content line is one independent logical row.
- Cells contain ``Inline[]`` parsed by full inline rules.
- The table ends at the first line that is neither a content row nor a header separator — including a blank line, a container boundary, or any other block opener. That line is classified on its own ([§9.3](#s-9-3)); a later ``|`` line opens a **new** table.

**Attrs scope chain.** Rule B ([§6](#s-6)) applies. The slots available to a row's trailing ``{attrs}`` sequence depend on two things — which row it is, and whether the last cell was closed:

| Row | Last cell | Slots, outermost first |
|---|---|---|
| last content row | left open (no trailing ``|``) | ``Table``, ``Row``, last attr-bearing inline in the cell |
| last content row | closed by a trailing ``|`` | ``Table``, ``Row`` |
| any earlier row | left open | ``Row``, last attr-bearing inline in the cell |
| any earlier row | closed by a trailing ``|`` | ``Row`` |

``Cell`` bears no attributes, so the chain never stops at a cell — it walks past it to the inline inside. Writing the closing ``\|`` seals that cell's inline context before the chain begins, which removes the inline slot.

A chain written **before** the closer is inside the cell's inline context and distributes through the __cell's__ slots instead — the cell's last attr-bearing inline, one slot only. This applies to any cell, not just the last. A cell whose content is plain text has no such inline, so the ``{}`` is dropped with CDN-0011; neither ``Row`` nor ``Table`` receives it. See [§6](#s-6).

The inline slot searches the **last cell only**. If that cell holds no attr-bearing inline, the slot goes unclaimed and the ``{}`` is dropped with CDN-0011 ([§6.1.3](#s-6-1-3)).

``{attrs}`` on a **header separator** row claim the Table slot directly.

```
Last content row:
| td1 | td2 | {.a}{.b}       →  Table({.b}, Row({.a}, ...))     ← sealed: 2 slots
| td1 | td2 | {.a}           →  Table({.a}, Row(...))           ← single {} = Table slot
| AA | **BB** {.a}{.b}{.c}   →  Table({.c}, Row({.b}, Cell(...), Cell(Strong({.a}, "BB"))))
| AA | **BB** | {.a}{.b}{.c} →  Table({.c}, Row({.b}, ...))     ← sealed; {.a} dropped (CDN-0011)
| AA | CC {.a}{.b}{.c}       →  Table({.c}, Row({.b}, ...))     ← last cell is Text; {.a} dropped

Chain before the closer — cell chain, 1 slot:
| AA | **BB** {.a} |         →  Row(Cell(...), Cell(Strong({.a}, "BB")))
| **AA** {.x} | BB |         →  Row(Cell(Strong({.x}, "AA")), Cell("BB"))
| AA | CC {.a} |             →  Row(Cell("AA"), Cell("CC"))   ← plain text: {.a} dropped (CDN-0011)

Mid-table row:
| td1 | td2 | {.a}           →  Row({.a}, ...)                  ← sealed: 1 slot only
| AA | **BB** {.a}{.b}       →  Row({.b}, Cell(...), Cell(Strong({.a}, "BB")))
```

==== Empty tables

A single ``|`` line with no cell content is a valid empty table:

```
|            →  Table { rows: [], columns: [] }

| {.tbl}     →  Table { rows: [], columns: [], attributes: [{class:["tbl"]}] }
```

For ``| {.tbl}``: no row is present, so ``{.tbl}`` has no Row slot to claim; it falls through to the Table slot directly.

==== Reflection

Trailing ``## comment`` on any table line (content row or header separator) bubbles to ``Table.reflection`` carrying the payload's ``loc``. See [§2.2](#s-2-2). A ``##`` appearing inside cell content (before the row's closing ``|``) causes the pre-``##`` cell fragment to fail the row grammar and fall back to a Paragraph; the payload then attaches to that Paragraph's ``reflection``.

==== Caption and escaping

- **Supports caption line ([§6.2](#s-6-2)).** A ``^ text`` line immediately after the table's last line (no blank line) sets ``caption: Inline[]``.
- **Escaping:** ``\|`` at line start → ``Paragraph`` (suppresses a table row or header separator). See [§8.2](#s-8-2).

=== 4.9 ImageBlock

**Syntax:**

```
![alt text](src) {attrs}
```

A line at block level beginning with ``![`` is classified as an ``ImageBlock``.

**AST type:**

```typescript
interface ImageBlock {
  type: "ImageBlock"
  alt: Inline[]
  src: string
  attributes: Attribute[]
  caption: Inline[] | null
  reflection: Reflection[] | null
}
```

- See [§5.9](#s-5-9) for ``ImageInline`` syntax and parsing details. The same rules apply to ``ImageBlock`` alt text and src.
- Consecutive ``ImageBlock`` lines with no blank line between them are wrapped in a ``FileRefGroup { group: "image" }``.
- ``ImageBlock`` is the block-level counterpart of ``ImageInline`` ([§5](#s-5)). The difference is that ``ImageBlock`` must be the only one segment on the line.
- **Only-segment fallback.** Phase 3 classification ([§9.3](#s-9-3)) is provisional: it matches ``^!\[``. If the line carries anything after the image, the only-segment requirement fails and the line falls back to a ``Paragraph`` containing an ``ImageInline`` followed by the remaining inline content. No content is dropped.

```
![a](b)                  → ImageBlock { alt: [Text("a")], src: "b" }
![a](b) trailing text    → Paragraph([ImageInline(...), Text(" trailing text")])
```

**Cascade to watch.** ``Paragraph`` is not captionable ([§6.2](#s-6-2)), so a ``^ `` line after the fallback has no captionable predecessor and becomes a ``Paragraph`` itself plus CDN-0008. One stray word after an image therefore demotes both the image __and__ its caption to prose, with only the diagnostic to show for it.

- **Supports caption line ([§6.2](#s-6-2)).** A ``^ text`` line immediately after this line (no blank line) sets ``caption: Inline[]`` on this node. ``ImageInline`` does not support captions — it is not a block.

=== 4.10 PageBreaker

**Syntax:**

```
---
```

A top-level line beginning exactly ``---``. A PageBreaker is a pagination signal, not a block: it is consumed by the pagination fold ([§9.5.2](#s-9-5-2)) and **produces no AST node**. It unconditionally closes the current Page — as a Ghost Page if empty — opens a new one, and closes all open root-level Sections.

- The rest of the line — surplus hyphens, ``{attrs}``, any other content — is dropped, and CDN-0016 is emitted. There is no attributed form.
- Inside a **Block container** (``List``, ``QuoteBlock``, ``NamedBlock``, ``SpoilerBlock``): a blank-line-surrounded ``---`` line is not a PageBreaker — it parses as ``Paragraph([Text("---")])`` and CDN-0017 is emitted. Glued to a preceding paragraph, ``---`` is ordinary paragraph content per the no-interrupt rule ([§10.1](#s-10-1)); no diagnostic is emitted.
- **Opener escape:** ``\---``, ``-\--``, or ``--\-`` at top level → ``Paragraph([Text("---")])``; no Page boundary occurs. See [§8.2](#s-8-2).
- Cutdown performs no front-matter detection: a document-leading ``---`` is a PageBreaker like any other, yielding a leading Ghost Page.
- Cutdown defines no thematic-break (horizontal-rule) element.

**Examples:**

```
---              → page boundary, no node

Illegal:
--- {.page-end}  → page boundary, no node (tail dropped, CDN-0016)
--- some text    → page boundary, no node (tail dropped, CDN-0016)
```

=== 4.11 FileRef

**Syntax:**

```
/path/to.file {attrs}
```

Any line beginning with ``/`` is a file reference block.

**AST type:**

```typescript
interface FileRef {
  type: "FileRef"
  path: string
  fragment: string | ''
  query: string | ''
  attributes: Attribute[]
  caption: Inline[] | null
  reflection: Reflection[] | null
}
```

- ``path`` starts with ``/`` (slash) and is a run of ``PATH_LITERAL`` characters ([§1.2](#s-1-2)) extended with any other character that is not one of ``<`` ``>`` ``:`` ``"`` ``\`` ``|`` ``*`` ``{`` ``}`` or whitespace. The first space, when present, separates the path from ``{attrs}``.
- Fragment: everything from the first ``#`` (octothorpe) to the next space (or end of line, before ``{attrs}``) is extracted as ``fragment``. ``src`` stores the portion before ``#``.
- Query (``?``): if path contains ``?``, the parser extracts the query string as ``query``. ``src`` stores the portion before ``?``.
- ``fragment`` and ``query`` are mutually independent — either, both, or neither may be present. If absent, they are set to empty string ``''`` (not null).
- A line containing only ``/`` is not a ``FileRef``. The whole line is emitted as a ``Paragraph`` of literal text.
- ``group`` is set automatically by file extension (see Known Groups below). Consumers may configure the extension lists.
- **Opener escape:** ``\/path`` at line start → ``Paragraph([Text("/path")])``. See [§8.2](#s-8-2).
- **Supports caption line ([§6.2](#s-6-2)).** A ``^ text`` line immediately after this line (no blank line) sets ``caption: Inline[]`` on this node. If the ``FileRef`` is part of an active ``FileRefGroup``, the ``^ `` line closes the group and binds to it instead (see [§4.12](#s-4-12)).

**Known Groups (defaults):**

| Group | Extensions |
|---|---|
| ``image`` | ``.png`` ``.jpg`` ``.jpeg`` ``.gif`` ``.webp`` ``.svg`` |
| ``video`` | ``.mp4`` ``.avi`` ``.mov`` |
| ``audio`` | ``.mp3`` ``.wav`` ``.aac`` ``.ogg`` |

=== 4.12 FileRefGroup

Consecutive ``FileRef`` or ``ImageBlock`` lines of the **same known group** with no blank line between them are automatically wrapped in a ``FileRefGroup``. Not produced by explicit syntax — assembled during parsing.

**AST type:**

```typescript
interface FileRefGroup {
  type: "FileRefGroup"
  group: "image" | "video" | "audio"
  children: (FileRef | ImageBlock)[]
  attributes: Attribute[]
  caption: Inline[] | null
  reflection: Reflection[] | null
}
```

- A blank line breaks any active group.
- Different groups do not merge — two consecutive lines of different groups produce two separate ``FileRefGroup`` segments.
- Unknown-extension files are never grouped.
- **Supports caption line ([§6.2](#s-6-2)).** A ``^ text`` line immediately after the last member of the group (no blank line) sets ``caption: Inline[]`` on the ``FileRefGroup``. A ``^ `` line mid-run closes the group at that point; the next ``FileRef``/``ImageBlock`` starts a new group.

**Example:**

```
Input:
  /photos/a.png
  /photos/b.jpg
  /docs/report.pdf

AST:
  FileRefGroup { group: "image" }
  ├── FileRef { src: "/photos/a.png", group: "image" }
  └── FileRef { src: "/photos/b.jpg", group: "image" }
  FileRef { src: "/docs/report.pdf", group: null }
```

=== 4.13 NamedBlock

**Syntax:**

```
:::block-name {attrs}
  content
:::
```

**AST type:**

```typescript
interface NamedBlock {
  type: "NamedBlock"
  name: string
  children: Block[]
  attributes: Attribute[]
  caption: Inline[] | null
  reflection: Reflection[] | null
}
```

- Opening: ``:::`` followed immediately by a block name (``[ID_LITERAL]+``), then optional attributes.
- Closing: ``:::`` on its own line (no name).
- A ``:::`` opener not followed immediately by an ``ID_LITERAL`` character does NOT open a NamedBlock — classified as a Paragraph → warning CDN-0013. **Exception:** if the line begins with an escaped opener (``\:::``, ``:\::``, ``::\:``), no warning is emitted — the escape is a deliberate author signal. See [§8.2](#s-8-2).
- Content: any block content, including nested ``:::`` containers.
- Unclosed container: content runs to end of document → warning CDN-0004.
- **Indentation collapsing:** The first content line establishes the base indentation. That many leading spaces are stripped from all content lines before parsing.
- **Body edge-blank trim:** Leading and trailing blank lines inside the body are stripped before children are parsed. See [§10.6](#s-10-6).
- **Supports caption line ([§6.2](#s-6-2)).** A ``^ text`` line immediately after the closing ``:::`` (no blank line) sets ``caption: Inline[]`` on this node.

**Example:**

```
:::callout {.warning}
  > quoted text
:::
→ NamedBlock { name: "callout", children: [QuoteBlock([Text("quoted text")])], attributes: {class:["warning"]} }
```

=== 4.14 RefDefinition

**Syntax:**

```
[^ref]: content
```

MUST start at the beginning of a line.

**AST type:**

```typescript
interface RefDefinition {
  type: "RefDefinition"
  ref: string
  children: Inline[]
  attributes: Attribute[]
  reflection: Reflection[] | null
}
```

- ``ref`` uses ``[ID_LITERAL]+`` characters. Case-sensitive.
- Empty ``ref`` (line starting with ``[^]:``) is invalid state and produces string literal for whole line.
- Empty content (``[^ref]:``) is valid and produces an empty ``children`` array.
- Content is **parsed by inline rules**. Result is ``Inline[]``.
- Cutdown does not validate that every ``[^ref]`` link has a matching definition.
- When the same ``ref`` is defined more than once in a document, resolution uses the last definition in source order (**last wins**). Resolution is the consumer's responsibility ([§9.4](#s-9-4)).

=== 4.15 SpoilerBlock

**Syntax:** Fenced with exactly three carets.

```
^^^ {attrs}
  content
^^^
```

**AST type:**

```typescript
interface SpoilerBlock {
  type: "SpoilerBlock"
  children: Block[]
  attributes: Attribute[]
  caption: Inline[] | null
  reflection: Reflection[] | null
}
```

- Opening: ``^^^`` at line start, optionally followed by ``{attrs}``. The opening line carries no other content.
- Closing: ``^^^`` on its own line.
- Content is **parsed as blocks** — paragraphs, lists, images, and ``:::`` NamedBlocks are all permitted. The contrast is intentional, because a Spoiler hides __meaning__, not __structure__.
- **No nested SpoilerBlocks.** The first ``^^^`` line encountered inside an open SpoilerBlock always closes it. A second ``^^^`` opener on the next non-blank line starts a new sibling SpoilerBlock. Tiered reveals (a Spoiler inside a Spoiler) are out of scope; if a use case genuinely requires it, wrap the inner content in ``:::spoiler`` NamedBlock instead.
- Fixed 3-caret fence ``^^^``. Variable-length fences not supported.
- **Indentation collapsing:** the first content line establishes the base indentation; that many leading spaces are stripped from all content lines before parsing — same rule as ``NamedBlock`` ([§4.13](#s-4-13)).
- **Body edge-blank trim:** Leading and trailing blank lines inside the body are stripped before children are parsed. See [§10.6](#s-10-6).
- Unclosed fence: content runs to end of document (or end of the parent block container) → warning CDN-0005.
- Legal inside ``ListItem``, ``TaskItem``, ``QuoteBlock``, ``NamedBlock``. Container indentation is stripped from content lines.
- Semantic variants (NSFW, redacted, entertainment-spoiler) are carried in ``attributes``; ``SpoilerBlock`` has no ``kind`` field.
- **Escape:** SpoilerBlock body is **not opaque** — children are parsed as blocks. Use [§8.2](#s-8-2) block-opener escape (``\^^^``, ``^\^^``, ``^^\^``) on a content line to prevent it from closing the fence.
- **Supports caption line ([§6.2](#s-6-2)).** A ``^ text`` line immediately after the closing ``^^^`` (no blank line) sets ``caption: Inline[]`` on this node.

**Example:**

```
Input:
  ^^^ {.nsfw}
  Plot twist: __the butler__ did it.

  - and so did the gardener
  ^^^

AST:
  SpoilerBlock {
    attributes: { class: ["nsfw"] },
    children: [
      Paragraph([Text("Plot twist: "), Emphasis([Text("the butler")]), Text(" did it.")]),
      List { kind: "bullet", children: [ListItem([Text("and so did the gardener")])] }
    ]
  }
```

=== 4.16 CommentBlock

**Syntax:** Fenced with exactly three octothorpes. See [§2.3](#s-2-3) for the full normative semantics; this section restates the block-level surface.

```
###
opaque content
###
```

**AST type:**

```typescript
interface CommentBlock {
  type: "CommentBlock"
  text: string
  reflection: Reflection[] | null
}
```

- Opening: a line whose stripped content begins ``###``. **No** ``[name]`` and **no** ``{attrs}`` are recognized on the opener line. Any trailing characters on the opener line are part of the opener (ignored).
- Closing: the next line whose stripped content is exactly ``###``, as for every other fence. Indentation is not compared. To place a literal ``###`` line inside the body, escape it ([§8.3](#s-8-3)).
- Content is **opaque** — captured verbatim with no inline or block parsing. Lines joined with ``\n``; a single trailing ``\n`` is appended.
- Legal at Page scope AND inside ``ListItem``, ``TaskItem``, ``QuoteBlock``, ``NamedBlock``, ``SpoilerBlock``.
- Unclosed fence: content runs to end of document → warning CDN-0006. Same rule as ``CodeBlock`` ([§4.4](#s-4-4)), ``Meta`` ([§4.3](#s-4-3)), ``MathBlock`` ([§4.5](#s-4-5)): opaque content has no parseable structure, so the enclosing container's boundary is not observable from inside the fence. An unclosed ``###`` opened inside a container therefore absorbs every following line, including content past the container.
- ``CommentBlock`` takes no part in pagination ([§9.5.2](#s-9-5-2)): only a ``Meta`` and a PageBreaker create Page boundaries.
- Default render policy is **hidden**: conforming renderers SHOULD omit it. See [§2.5](#s-2-5).
- No ``attributes`` field.
- **Closer escape:** ``\#`` inside the body emits a literal ``#`` (consumes the ``\``). A line ``\###``, ``#\##``, or ``##\#`` therefore does NOT close the fence. The rule does not look at run length — any ``\#`` escapes. All other ``\X`` sequences are literal. See [§8.3](#s-8-3). Opener escape: see [§8.2](#s-8-2).

**Example:**

```
Input:
  intro paragraph

  ###
  draft note — revise before publish
  ###

  next paragraph

AST:
  Paragraph([Text("intro paragraph")])
  CommentBlock { text: "draft note — revise before publish\n" }
  Paragraph([Text("next paragraph")])
```

== 5. Inline Segments

Inline content is parsed in source order with no backtracking. When an opener has no valid matching closer before the end of the paragraph (or enclosing block), the opener is emitted as literal text.

The ``##`` opener ([§2.2](#s-2-2)) is special: it has no closer and terminates at end-of-line. When ``##`` is encountered with one or more inline constructs open, those unclosed openers degrade to literal per the same rule. The payload is stored as a ``Reflection`` entry on the enclosing block — it does not appear in the inline stream.

**Inline rules run in:**

| Location | Section |
|---|---|
| Heading text | [§4](#s-4) |
| Paragraph content | [§4](#s-4) |
| List item and task item content | [§4](#s-4) |
| Table cell content | [§4](#s-4) |
| ``alt`` slot of ``ImageBlock`` | [§4](#s-4) |
| ``alt`` slot of ``ImageInline`` | [§5](#s-5) |
| ``Caption`` content | [§6](#s-6) |
| ``RefDefinition`` content | [§4](#s-4) |
| Children of ``Emphasis``, ``Strong``, ``Highlight``, ``QuoteInline``, ``Spoiler`` | [§5](#s-5) |
| Children of ``Mark`` | [§5](#s-5) |
| ``[text]`` slot of ``Link`` | [§5](#s-5) |

=== 5.1 Text

**Syntax:** Any sequence of characters not matched by another inline rule.

**AST type:**

```typescript
interface Text {
  type: "Text"
  value: string
}
```

Consecutive text tokens MUST be merged into a single ``Text`` segment by the parser.

Text segments are **literal** — no inline parsing, no escape processing. A ``\`` (backslash) at the end of a line, before the line terminator, produces a ``LineBreak`` segment ([§5.13](#s-5-13)); every other character in a ``Text`` segment is literal.

=== 5.2 Emphasis

**Syntax:** ``__inline content__``

**AST type:**

```typescript
interface Emphasis {
  type: "Emphasis"
  children: Inline[]
  attributes: Attribute[]
}
```

- ``__`` opener and closer. A single ``_`` (underscore) is always literal text.
- Run of 3: ``___`` = ``__`` (opener/closer) + ``_`` (literal).
- Matching: greedy, in source order. First valid ``__`` closer wins.
- Unclosed ``__`` → ``Text("__")``.
- Same-type nesting not allowed. Cross-type nesting allowed (e.g. ``__**text**__``).
- Leading/trailing whitespace inside delimiters is stripped. See [§12](#s-12) for full whitespace rules.

**Examples:**

```
__italic__   → Emphasis([Text("italic")])
___text___   → Emphasis([Text("_text")]) + Text("_")
__ text      → Text("__") + Text(" text")   (unclosed)
_ text _     → Text("_ text _")             (single underscore = literal)
```

=== 5.3 Strong

**Syntax:** ``**inline content**``

Strong emphasis. ``**`` follows the dominant Markdown-family convention (CommonMark, GFM, Pandoc), where ``**`` marks strong emphasis.

**AST type:**

```typescript
interface Strong {
  type: "Strong"
  children: Inline[]
  attributes: Attribute[]
}
```

- ``**`` opener and closer. A single ``*`` (asterisk) is always literal text.
- Same rules as ``Emphasis``: run of 3, greedy, unclosed = literal, no same-type nesting.
- Cross-nesting with ``Emphasis`` allowed: ``**__text__**`` and ``__**text**__`` are both valid.

=== 5.4 Highlight

**Syntax:** ``~~inline content~~``

Marks a span of text as highlighted — visually emphasized as important, like a marker pen. Highlight carries no deletion semantics; Cutdown defines no strikethrough element.

**AST type:**

```typescript
interface Highlight {
  type: "Highlight"
  children: Inline[]
  attributes: Attribute[]
}
```

- ``~~`` opener and closer. A single ``~`` (tilde) is always literal text (not a meta fence in inline context).
- Same rules as ``Emphasis``: greedy, unclosed = literal, no same-type nesting.
- Cross-nesting with ``Emphasis`` and ``Strong`` allowed.

=== 5.5 Link

**Syntax:** Several forms depending on link kind.

```
[text](url)          → external link
[text][path/to/page] → page link
[text][#tag/path]    → tag link
[text][^ref-id]      → reference link
[text][@cite-id]     → citation link
```

**AST type:**

```typescript
interface Link {
  type: "Link"
  kind: "external" | "page" | "tag" | "ref" | "cite"
  children: Inline[]
  href: string            // for kind: "external", otherwise empty string
  target: string          // for kind: "page" | "tag" | "ref" | "cite", otherwise empty string
  attributes: Attribute[]
}
```

| Syntax | Kind | Field |
|---|---|---|
| ``[text](url)`` | ``"external"`` | ``href`` |
| ``[text][path/to/page]`` | ``"page"`` | ``target`` |
| ``[text][#tag/path]`` | ``"tag"`` | ``target`` |
| ``[text][^ref-id]`` | ``"ref"`` | ``target`` |
| ``[text][@cite-id]`` | ``"cite"`` | ``target`` |

- ``text`` is **parsed by inline rules**. May be empty.
- ``href`` / ``target`` may be empty strings — both are valid and preserved.
- Page target uses ``PATH_LITERAL`` characters. Tag target: ``#`` (octothorpe) + ``PATH_LITERAL``. Ref target: ``^`` (caret) + ``ID_LITERAL``. Cite target: ``@`` + any characters other than ``]`` (right bracket).
- Shorthand ``[@cite-id]`` (no text bracket) is NOT a citation link — emitted as plain bracket text.
- Cutdown does not validate link resolution. That is the consumer's responsibility.
- Cutdown does NOT validate URL syntax and URL schema. Consumers may choose to validate or sanitize ``href`` values.

**Edge cases:**

```
[][target]    → Link { kind: "page", children: [], target: "target" }
[][]          → Link { kind: "page", children: [], target: "" }
[]()          → Link { kind: "external", children: [], href: "" }
```

All cases above are valid and preserved. Consumers may choose to warn about empty targets or hrefs.

==== Link types and meaning

Ordinary links ``[text](url)`` are for external URLs. At least ``(url)`` part should be treated as HTML DOM ``<a>`` ``href`` attribute value and validated/sanitized accordingly. Consumers may choose to also validate the ``url`` against a whitelist of allowed URL schemas (e.g. ``http:``, ``https:``, ``mailto:``) and/or emit a warning for suspicious URLs (e.g. ``javascript:``).

Tag links ``[text][#tag/path]`` are for linking to a **tag or category** within the whole project where document is defined. The ``target`` field contains the tag name, which consumers can resolve according to their internal tagging logic. Cutdown does not specify where tags are defined or how they are resolved.

Reference links ``[text][^ref-id]`` are for linking to a **reference** definition elsewhere **in the same document**. There can be two way ``ref-id`` resolution. Either anywhere in the document the ``[^ref-id]: target`` syntax is used to define a ``RefDefinition`` segment with the given ``id``, or ``id`` attribute of any segment matches the ``ref-id``.

```
Sample text with a reference to [the API][^api-doc] and [the API table][^api-table].

| Endpoint | Description |
| ...      | ...         | {#api-table}

[^api-doc]: https://example.com/api-doc
```

Citation links ``[text][@cite-id]`` are for linking to a bibliography entry or similar **external reference**. The ``target`` field contains the citation ID, which consumers can resolve according to their internal citation logic.

Page links ``[text][path/to/page]`` are for linking to other pages within the same site or system. The ``target`` field contains the page path, which consumers can resolve according to their internal routing logic. Nevertheless, the client may choose to validate the ``target`` against a whitelist of allowed page paths and/or emit a warning for suspicious targets.

=== 5.6 CodeInline

**Syntax:** '\``code\``'

**AST type:**

```typescript
interface CodeInline {
  type: "CodeInline"
  value: string
  attributes: Attribute[]
}
```

- Double backtick only. Single backtick is always literal text.
- Content is **literal** — no inline parsing. **One escape sequence is processed**: ``\\``` → literal ``\``` (does not close the span). Every other backslash is literal, including ``\\`` (two literal backslashes) and any other ``\X`` (per [§8](#s-8) non-special rule). See [§8.3](#s-8-3).
- Unmatched '\``' → ``Text("\`\`")``. Single '`' → ``Text("``")`.
- Triple backtick in inline context: \`\`` = \`` (opener) + ` (literal inside).
- Whitespace collapsing does NOT apply to ``CodeInline``. A soft break inside \``...\`` is folded to zero. See [§12](#s-12) for full whitespace rules.

**Examples:**

```
``code``          → CodeInline { value: "code" }
`not code`        → Text("`") + Text("not code") + Text("`")
\```text```        → CodeInline { value: "`text" } + Text("`")
``test
continues``       → CodeInline { value: "testcontinues" }   (soft break → zero)
``\`\`\`x``       → CodeInline { value: "```x" }            (escape lets multi-backtick embed)
``a\\b``           → CodeInline { value: "a\\b" }            (\\ stays as two literal backslashes)
``\n``             → CodeInline { value: "\n" }              (unknown \X stays literal: backslash + n)
```

=== 5.7 MathInline

**Syntax:** ``$$formula$$``

**AST type:**

```typescript
interface MathInline {
  type: "MathInline"
  formula: string
  attributes: Attribute[]
}
```

- ``$$`` opener and closer. A single ``$`` is always literal text.
- Content is **literal** — no inline parsing. Passed as raw string to the consumer (KaTeX or equivalent).
- Run of 3: ``$$$`` at inline position = ``$$`` (opener/closer) + ``$`` (literal).
- Unclosed ``$$`` → ``Text("$$")``. Same-type nesting not allowed.

**Examples:**

```
$$ a^2 + b^2 $$      → MathInline { formula: " a^2 + b^2 " }
$$unclosed            → Text("$$") + Text("unclosed")
$ not math $          → Text("$ not math $")
```

=== 5.8 QuoteInline

**Syntax:** ``"" content ""`` or ``'' content ''``

**AST type:**

```typescript
interface QuoteInline {
  type: "QuoteInline"
  kind: "double" | "single"
  children: Inline[]
  attributes: Attribute[]
}
```

- ``""`` = double-quote style (``kind: "double"``). ``''`` = single-quote style (``kind: "single"``).
- A single ``"`` or ``'`` is always literal text.
- Same-kind nesting not allowed. Cross-kind nesting IS allowed: ``""'' inner ''""``.
- Unmatched opener → ``Text('""')`` or ``Text("''")``.
- Whitespace rules follow [§12](#s-12) (boundary whitespace stripped; adjacent boundaries collapsed).

**Examples:**

```
"" hello ""          → QuoteInline { kind: "double", children: [Text("hello")] }
'' hi ''             → QuoteInline { kind: "single", children: [Text("hi")] }
""'' inner ''""      → QuoteInline { kind: "double", children: [QuoteInline { kind: "single", ... }] }
"" unclosed          → Text('""') + Text(" unclosed")
```

=== 5.9 ImageInline

**Syntax:** ``![alt](src) {attrs}``

**AST type:**

```typescript
interface ImageInline {
  type: "ImageInline"
  alt: Inline[]
  src: string
  attributes: Attribute[]
}
```

- ``alt`` is **parsed by inline rules**. Result is ``Inline[]``. Despite  the ``alt`` context allows full inline syntax, it should be treated as technical possibility rather than a common use case. Consumers may choose to restrict or ignore certain inline features in ``alt`` (e.g. links, images, formatting) or strip it down to plain text. ``alt`` may be empty.
- ``src`` is a raw string. No URL validation or sanitization by Cutdown; consumers may choose to validate/sanitize as needed. ``src`` may be empty.
- ``![]()`` is valid and preserved as ``ImageInline { alt: [], src: "" }``.
- **Captions:** ``ImageInline`` does **not** participate in the caption line ([§6.2](#s-6-2)) — it is not a block. Only ``ImageBlock`` ([§4.9](#s-4-9)) is captionable.
- **ImageInline vs ImageBlock ([§4.9](#s-4-9)):** a line at block level beginning with ``![`` is classified as an ``ImageBlock``, and an ``ImageBlock`` must be the only segment on its line. An image anywhere else — mid-line, or on a line carrying other content — is an ``ImageInline``. See [§4.9](#s-4-9).

=== 5.10 Mark

**Syntax:** ``::name <content>::`` or ``::name::``

A named inline container that wraps a range of content as a hook for consumer post-processing. Supports nesting.

**AST type:**

```typescript
interface Mark {
  type: "Mark"
  name: string        // non-empty ID_LITERAL run
  children: Inline[]  // empty for the `::name::` form
  attributes: Attribute[]
}
```

**Opener.** ``::`` followed immediately by a name — a **maximal ``ID_LITERAL+`` run** ([§1.2](#s-1-2)). The character after the name decides what happens:

| Next character | Result |
|---|---|
| ``::`` | empty ``Mark``; the construct ends there |
| space | container opener; content runs to the matching closer |
| anything else | not a ``Mark`` — emit ``Text("::" + name)`` and continue parsing |

The name is **required**. ``::`` not followed by at least one ``ID_LITERAL`` character is emitted as literal ``Text("::")``. The space after the name is a delimiter and is not part of the content.

**Closer.** A bare ``::``. While scanning content, a ``::`` is a **nested opener** if it is followed by ``ID_LITERAL+`` and then a space or ``::``; otherwise it is the **closer**. A ``::`` encountered with no ``Mark`` open is literal text.

- Because the opener (``::name ``) is textually distinct from the closer (``::``), ``Mark`` is **bracket-matched by counting**, not by greedy first-closer. This makes it the only inline construct in the language that permits **same-type nesting** — including nesting a ``Mark`` of the same name ([§9.4.1](#s-9-4-1), Class 3).
- Cross-nesting with ``Emphasis``, ``Strong``, ``Highlight``, ``Spoiler``, ``QuoteInline`` and ``Link`` is allowed in both directions.
- Children are **parsed by inline rules** (see the context table at the head of [§5](#s-5)).
- **Maximum nesting depth is 8.** A ``Mark`` opener at depth 9 or deeper does not open; it degrades to literal ``Text("::" + name)`` and emits warning CDN-0031.
- An unclosed opener degrades per Class 1 ([§9.4.1](#s-9-4-1)): the opener alone is emitted as ``Text`` and parsing continues immediately after it. No diagnostic.
- Attributes trail the closer, glued to it as for every other inline node: ``::a b::{#x .y}``.
- Whitespace inside the delimiters follows [§12](#s-12), as for any container inline.

**Name lexing.** The name is a maximal ``ID_LITERAL+`` run and is lexed at the opener, before delimiter matching. ``_``, ``-`` and ``.`` are ``ID_LITERAL`` characters, so they are absorbed into the name; ``*``, ``^``, ``$``, ``\``` and space are not, and terminate the name scan. This is a lexical property and is independent of the precedence table ([§11](#s-11)):

```
::a__b__::   → Mark { name: "a__b__" }              `_` is ID_LITERAL — the name swallows it, no Emphasis forms
::a**b**::   → Text("::a") + Strong([Text("b")]) + Text("::")   `*` is not — the name ends at `a`, the opener fails
```

**Colon runs.** A run of three colons in inline position is the ``::`` closer plus a literal ``:``. At block position ``:::name`` is still a ``NamedBlock`` ([§4.13](#s-4-13)) — that classification happens first and never reaches the inline parser.

**Examples:**

```
::a::               → Mark { name: "a", children: [] }
::a b::             → Mark { name: "a", children: [Text("b")] }
::a ::c::::         → Mark { name: "a", children: [Mark { name: "c", children: [] }] }
::a b ::c d:: e::   → Mark { name: "a", children: [Text("b "), Mark { name: "c", children: [Text("d")] }, Text(" e")] }
::a ::a ::a ::::::  → Mark(a, [Mark(a, [Mark(a, [])])])         same-name nesting is legal

::a::b              → Mark { name: "a" } + Text("b")
::a::b ::           → Mark { name: "a" } + Text("b ::")         closer with nothing open is literal
::a::b ::::         → Mark { name: "a" } + Text("b ::::")
:: ::               → Text(":: ::")                             no name
::a b __c__         → Text("::a b ") + Emphasis([Text("c")])    unclosed opener, Class 1
::a b::{#x .y}      → Mark { name: "a", children: [Text("b")], attributes: {id:"x", class:["y"]} }
```

=== 5.11 Variable

**Syntax:** ``{{key}}``

**AST type:**

```typescript
interface Variable {
  type: "Variable"
  key: string
  attributes: Attribute[]
}
```

- ``key`` MUST use ``ID_LITERAL`` characters: ``[a-zA-Z0-9._-]``. A ``{{...}}`` with invalid key characters is emitted as literal text → warning CDN-0015.
- Unclosed ``{{`` → literal text. Empty-key ``{{}}`` → literal text → warning CDN-0015.
- Variables are only parsed in inline contexts where inline rules are active (not inside code/math/meta blocks).
- Brace tie-break: ``{{`` is always matched before ``{`` (longest opener wins).
- May carry trailing attributes: ``{{key}} {#id .class}``.

=== 5.12 Spoiler

**Syntax:** ``^^inline content^^``

**AST type:**

```typescript
interface Spoiler {
  type: "Spoiler"
  children: Inline[]
  attributes: Attribute[]
}
```

- ``^^`` opener and closer. A single ``^`` is always literal text in inline context. (Inside ``[...][^id]`` link/definition target slots, ``^`` retains its reference-marker role per [§5.5](#s-5-5) / [§4.14](#s-4-14) — that context is delimited and never reaches the Spoiler parser.)
- Cutdown emits the AST node ``Spoiler``; consumers choose the rendering (click-to-reveal, blur, redaction, NSFW mask). Semantic variants are conveyed via attributes (e.g. ``{.nsfw}``, ``{.redacted}``).
- Same rules as ``Emphasis``: run of 3 (``^^^`` in inline context = ``^^`` opener/closer + ``^`` literal), greedy close in source order, unclosed ``^^`` → ``Text("^^")``, no same-type nesting.
- Cross-nesting with ``Emphasis``, ``Strong``, ``Highlight``, ``QuoteInline``, and ``Link`` is allowed.
- Leading/trailing whitespace inside delimiters is stripped. See [§12](#s-12) for full whitespace rules.

**Examples:**

```
^^hidden^^         → Spoiler([Text("hidden")])
^^^x^^^            → Spoiler([Text("^x")]) + Text("^")
^^ open            → Text("^^") + Text(" open")            (unclosed)
^ not spoiler ^    → Text("^ not spoiler ^")               (single caret = literal)
__^^x^^__          → Emphasis([Spoiler([Text("x")])])      (cross-nesting)
^^a ^^b^^ c^^      → Spoiler([Text("a")]) + Text("b") + Spoiler([Text("c")])   (greedy)
```

=== 5.13 LineBreak

**Syntax:** ``\<EOL>``, backslash as the last character of a line (before ``\n``). The construct is the **LineBreaker**; it produces the ``LineBreak`` node, as ``PageBreaker`` ([§4.10](#s-4-10)) produces a page boundary.

**AST type:**

```typescript
interface LineBreak {
  type: "LineBreak"
}
```

A ``LineBreak`` asserts an author-intended line break **within** a paragraph — the surrounding text stays one block. It is not a paragraph boundary: a blank line ends the block and starts a new ``Paragraph``, whereas a ``LineBreak`` breaks the line and keeps the block.

Cutdown emits the AST node ``LineBreak``; consumers choose the rendering (a ``<br>``, a newline in plain text, a no-op in a single-line context). See [§16](#s-16) — Cutdown has no canonical rendering.

- The ``\`` and the following newline are consumed. Inline parsing continues on the next line.

  - The ``\`` must be the last non-whitespace character on the line. Trailing whitespace, and a trailing ``##`` comment (which consumes the rest of the line as a reflection entry, [§2.2](#s-2-2)), are ignored when making this test.

**Line-ending summary:**

| Line ending | Result |
|---|---|
| ``word\n`` | Soft break — folded to zero; lines concatenate directly |
| ``word  \n`` | Trailing space collapsed to single space — ``Text("word ")`` emitted |
| ``word\\n`` | ``LineBreak`` segment — explicit line break inside the paragraph |
| ``word\n\n`` | Blank line — ends the block; the next line starts a new ``Paragraph`` |

==== Line Comment (``##``) — not an inline segment

**Syntax:** ``content ## trailing comment``

Line Comment runs till the end of line (EOL). Payload captured as a ``Reflection`` on the enclosing block, omitted by renderers by default ([§2.5](#s-2-5)).

See [§2.2](#s-2-2) for the full normative semantics. Summary:

- Recognized at line-start or mid-line; runs to EOL; opaque to all other delimiters.
- Payload stored as ``Reflection`` entry on the enclosing block — does not appear in the inline stream.
- A single ``#`` is always literal (see [§10.4.4](#s-10-4-4)). ``###`` at inline position → ``##`` (comment opener) + trailing ``#`` in payload.
- Not recognized inside ``CodeInline``, ``MathInline``, or quoted attribute values.
- Escaped with ``\##`` or ``#\#``.

== 6. Segment Attribution (Universal Attributes and Caption)

=== 6.1 Universal Attributes

**AST type:** ``Attribute[]`` — see [§14](#s-14) __Attributes Type__ for the definition.

==== 6.1.1 Syntax

```
{#id .class key=value key="value with spaces"}
```

Token types inside ``{}``:

- ``#identifier`` — the ``#`` (octothorpe) sets the ``id`` attribute. Emits ``{ key: "id", value: "identifier" }``. First ``#`` wins; any subsequent ``#id`` or ``id=`` token is dropped and CDN-0020 is emitted.
- ``.classname`` — appends to the ``class`` entry. All ``.classname`` tokens in a block are collected into a single ``{ key: "class", value: string[] }`` entry. ``.class`` syntax has priority: if ``class=`` also appears, ``class=`` is dropped and CDN-0021 is emitted.
- ``key=value`` — custom attribute. Emits ``{ key: "key", value: "value" }``. Unquoted value: no spaces. Quoted value: spaces allowed. First occurrence wins; duplicate keys are dropped and CDN-0022 is emitted.
- ``key`` (bare, with no ``=`` (equals sign)) — flag token. Emits ``{ key: "key", value: "" }``. Signals a semantic hint with no associated value.
- Token order inside ``{}`` is preserved in the emitted ``Attribute[]``.

> **Philosophy:** Universal Attributes are semantic hints for consumers — they are not one-to-one mappings to HTML attributes. A bare ``{banner}`` does not mean ``<div banner>``; it means "this element has the semantic role 'banner'." The consuming application decides how to expand, map, or ignore any attribute token. Cutdown makes no assumption about the rendering target.

==== 6.1.2 Placement

Attributes MUST appear **after** their target element on the **same line**.

```
== Section {#intro}
**bold text** {.highlight}
:::callout {.warning}
```

Whitespace between a segment and its attaching ``{...}`` is consumed by the attachment and does not appear in the AST. See [§12](#s-12) for the full rule.

===== Block Opening Lines — Last-Attr Rule

On lines that open a block construct (headings, named blocks), the **last ``{...}`` token on the line is claimed by the block**. All earlier ``{...}`` tokens on the same line follow inline attachment rules and bind to their immediately preceding inline element.

An empty ``{}`` as the last token explicitly assigns no attributes to the block, freeing earlier ``{...}`` to bind inward.

```
= Heading **bold**{.b} {#h}    →  Section({id:"h"},  [Text("Heading "), Strong({class:"b"}, "bold")])
= Heading **bold**{.b} {}      →  Section({},        [Text("Heading "), Strong({class:"b"}, "bold")])
= Heading **bold** {#h}        →  Section({id:"h"},  [Text("Heading "), Strong("bold")])
```

This rule applies only in block opening line context. In paragraph context all ``{...}`` follow inline attachment rules exclusively.

===== Per-segment placement rules

- ``Meta``: no attributes supported.
- ``RefDefinition``: has ``attributes``.
- ``Table``: see [§4](#s-4) for row and table attr placement.
- ``List / FileRefGroup / ImageGroup / QuoteBlock / Paragraph``: scope-chain rule (Rule B) — see below.
- ``Cell``, ``Column``, ``Page``, ``Document``: no attributes supported.

===== Scope-chain rule (Rule B)

A sequence of ``{attr}`` blocks at the end of an inline context is distributed **right-to-left** through a scope chain. The **last** ``{}`` in the sequence is claimed by the **highest segment** in the current hierarchy; each preceding ``{}`` claims the next lower segment. Any ``{}`` blocks at the front of the sequence that have no segment to claim are **silently dropped**.

An empty ``{}`` is valid syntax. It claims its slot and assigns nothing to that segment's attributes.

``{...}`` is tokenized **atomically** — the interior is never parsed as inline markup. If the ``{`` (left brace) has no matching ``}`` (right brace) before end of inline context, the entire slice from ``{`` to end of line is emitted as one verbatim ``Text`` run (Class 2 degradation, [§9.4.1](#s-9-4-1); see [§6.1.3](#s-6-1-3)).

**Scope slots by context:**

| Context | Slot 1 (last ``{}``) | Slot 2 | Slot 3 |
|---|---|---|---|
| Standalone Paragraph | Paragraph | last attr-bearing inline | — |
| List item | List | ListItem | last attr-bearing inline |
| Table row — mid-table, cell open | Row | last attr-bearing inline | — |
| Table row — mid-table, cell sealed | Row | — | — |
| Table row — last row, cell open | Table | Row | last attr-bearing inline |
| Table row — last row, cell sealed | Table | Row | — |
| FileRef / ImageBlock in group | FileRefGroup | FileRef / ImageBlock | last attr-bearing inline |
| QuoteBlock nesting (``> >``) | outermost QuoteBlock | … inner levels … | Paragraph → inline |

**Table rows.** ``Cell`` bears no attributes (see __Per-segment placement rules__ above), so the chain walks **past** the cell to the last attr-bearing inline inside it. Whether that slot exists depends on the trailing ``|`` (pipe):

- **Cell open** — the row omits the trailing ``|``, so the chain sits inside the last cell's inline context and the inline slot is available.
- **Cell sealed** — the row writes the trailing ``|``, closing the last cell's inline context before the chain begins. There is no open inline context, so the inline slot does not exist and the chain stops at ``Row``.

**A chain written before the closer belongs to the cell, not the row.** Every cell is its own inline context, so Rule B applies inside it with the cell's own slots — and since ``Cell`` bears no attributes, the only slot is that cell's last attr-bearing inline. This holds for any cell, not just the last.

```
| AA | **BB** {.a}         →  Table({.a}, Row(...))          ← open: row chain, slot 1 = Table
| AA | **BB** | {.a}       →  Table({.a}, Row(...))          ← after the closer: row chain
| AA | **BB** {.a} |       →  Row(Cell(...), Cell(Strong({.a}, "BB")))   ← before the closer: cell chain
| **AA** {.x} | BB |       →  Row(Cell(Strong({.x}, "AA")), Cell("BB"))  ← non-final cell
```

So the closer sets how deep the row chain reaches, and the chain's position relative to the closer selects which chain applies. A cell chain has exactly one slot; a second group orphans (CDN-0011).

The slot searches the **last cell only**. If that cell holds no attr-bearing inline, the slot goes unclaimed and the ``{}`` is dropped with CDN-0011 ([§6.1.3](#s-6-1-3)) — the chain never scans sideways into an earlier cell, exactly as ``- text {.a}{.b}{.c}`` drops ``.a`` rather than hunting leftward.

```
| AA | **BB** {.a}{.b}{.c}       →  Table({.c}, Row({.b}, Cell(...), Cell(Strong({.a}, "BB"))))
| AA | **BB** | {.a}{.b}{.c}     →  Table({.c}, Row({.b}, ...))              ← sealed; {.a} dropped
| AA | **BB** | CC {.a}{.b}{.c}  →  Table({.c}, Row({.b}, ...))              ← last cell is Text; {.a} dropped
```

Single NL does not break the attr chain. A sequence of ``{}`` blocks may span multiple lines (one per line) as long as no blank line appears between them.

```
- ::sp:: {.a}{.b}{.c}  →  List({.c}, ListItem({.b}, Mark({.a})))

- ::sp:: {.a}{.b}      →  List({.b}, ListItem({.a}, Mark()))

- ::sp:: {.a}          →  List({.a}, ListItem(Mark()))

- ::sp:: {}            →  List({},   ListItem(Mark()))     ← {} no-op on List

- ::sp:: {.a}{}        →  List({},   ListItem({.a}, Mark())) ← {} no-op on List; {.a} to ListItem

- text {.a}{.b}      →  List({.b}, ListItem({.a}, Text("text")))

- text {.a}          →  List({.a}, ListItem(Text("text")))

- text {.a}{.b}{.c}  →  List({.c}, ListItem({.b}, Text("text")))   ← {.a} dropped (Text has no attrs)
```

Same rule applies to ``FileRefGroup`` and ``ImageGroup``:

```
/img.png {.a}{.b}   →  FileRefGroup({.b}, FileRef({.a}, ...))
/img.png {.a}       →  FileRefGroup({.a}, FileRef(...))
/img.png {}         →  FileRefGroup({},   FileRef(...))    ← {} no-op on group
/img.png {.a}{}     →  FileRefGroup({},   FileRef({.a}, ...))
```

Multiline equivalents (all produce identical AST):

```
- li item {.a}{.b}

- li item
  {.a}{.b}

- li item
  {.a}
  {.b}
```

===== Trailing attr lines and loose-list detection

A line consisting solely of ``{attrs}`` immediately after a list item with no preceding blank line is a **trailing attr line**, not a blank line. Loose list detection ignores trailing attr lines.

```
- item one
{.attrs}         ← trailing attr line, NOT a blank line → list stays tight
- item two

- item one

{.attrs}         ← blank line precedes → list is loose; {.attrs} is orphaned (literal)
- item two
```

``{attrs}`` may appear at the end of a Paragraph or ListItem either on the same line as the final content line, or on the immediately following line (no blank line between). A line consisting solely of ``{attrs}`` immediately after a paragraph (no blank line) is consumed as part of the paragraph's scope chain and does not start a new block.

==== 6.1.3 Orphan Attributes

An ``{...}`` sequence that cannot be assigned to any segment in the current scope chain is an **orphan**.

Orphan behaviour depends on position:

| Position | Behaviour | Example |
|---|---|---|
| Middle of inline content (no preceding attr-bearing segment, slots exhausted) | ``Text("{...}")`` emitted verbatim | ``price is {high}`` → ``Text("price is ")`` + ``Text("{high}")`` |
| After a double blank line (own block, no following content claims it) | ``Text("{...}")`` emitted verbatim |  |
| End of scope chain, all slots filled, excess ``{}`` at the front | dropped (no AST output), **CDN-0011** emitted | ``{.x}{.a}{.b}`` on a 2-slot context → ``{.x}`` dropped |

``{`` is always consumed as the start of an attribute scan, running to the matching ``}`` or end of line. If the content violates the attribute grammar or the ``}`` never arrives, the **entire slice** — braces included, when present — is emitted as one verbatim ``Text`` run and is never inline-parsed (Class 2 degradation, [§9.4.1](#s-9-4-1)). This is the intentional literal-span idiom: ``{a **b**}`` is the literal text ``{a **b**}``. Consequence: any future extension of the attribute grammar is a breaking change for text relying on this idiom.

Authors who want a literal ``{`` SHOULD escape it with ``\{`` to make intent explicit:

```
price is \{high\}  →  Text("price is {high}")
```

==== 6.1.4 Attribute Inside Link Text

``{attrs}`` appearing inside ``[...]`` follows paragraph rules: it attaches to the preceding inline element, or is dropped if no preceding inline element exists.

```
[**bold** {.foo}](url)  →  Link { children: [Strong({class:"foo"}, "bold")], href: "url" }
[{.foo} text](url)      →  Link { children: [Text("text")], href: "url" }   ({.foo} dropped)
```

=== 6.2 Caption

A caption line enriches the immediately preceding captionable block with a ``caption`` field. It does not produce a separate AST node.

**Syntax:**

```
^ inline-content
```

A line at block start consisting of ``^`` (caret) followed by a single space and then any inline content. The ``^`` and space are consumed; the remainder is parsed as ``Inline[]``.

**Binding rule:** A caption line binds to the immediately preceding block in the current block scope when all three hold:

1. That block is captionable (see table below);
2. That block's caption slot is empty;
3. No blank line separates the block's last line from the ``^ `` line.

**Otherwise the caption line is orphaned:** it becomes a ``Paragraph`` containing the line verbatim, and warning CDN-0008 is emitted.

Lines that emit no block are **transparent** — they are not "the immediately preceding block" and do not break binding. Any number of them may intervene:

- a trailing ``{attrs}`` line (sets the block's attributes; emits no node);
- a standalone ``## comment`` line (attaches to the preceding block's ``reflection``, [§2.2](#s-2-2)).

```
| col |
{#tbl-one}
^ Caption text   →  Table { caption: [...], attributes: [{id:"tbl-one"}] }

| col |
## editorial note
^ Caption text   →  Table { caption: [...], reflection: [{ loc: { start: 11, end: 25 }, text: "editorial note" }] }
```

**Single-line only.** A caption is exactly one line. A second consecutive ``^ `` line fails condition 2 — the slot it would claim is already filled — so it orphans.

The "preceding block" is always resolved within the current block scope ([§1.6](#s-1-6)). A ``^ `` line inside a ``NamedBlock`` binds to the last captionable child of that ``NamedBlock``, not to anything outside it.

**Captionable blocks and their AST fields:**

| Block | Field added |
|---|---|
| ``Table`` | ``caption: Inline[] | null`` |
| ``ImageBlock`` | ``caption: Inline[] | null`` |
| ``CodeBlock`` | ``caption: Inline[] | null`` |
| ``MathBlock`` | ``caption: Inline[] | null`` |
| ``FileRef`` | ``caption: Inline[] | null`` |
| ``FileRefGroup`` | ``caption: Inline[] | null`` |
| ``NamedBlock`` | ``caption: Inline[] | null`` |
| ``SpoilerBlock`` | ``caption: Inline[] | null`` |
| ``QuoteBlock`` | ``caption: Inline[] | null`` |

All captionable blocks default these fields to ``null`` when no caption line is present.

**``FileRefGroup`` mid-run close.** A ``^ `` line immediately after a ``FileRef`` or ``ImageBlock`` that is part of an active group closes the group at that point and binds to it. The next ``FileRef``/``ImageBlock`` starts a new group:

```
/a.png
/b.png
^ Two-image group caption
/c.png
^ Single-image group caption
```

**Inline content.** The caption text is parsed by the full inline rule set:

- ``##`` behaves normally — consumes to EOL; payload stored in the caption's parent block ``reflection`` at the caption line's offset.
- ``{{variable}}`` is allowed — ``Variable`` nodes are valid caption content.
- A trailing ``{attrs}`` sequence at end of the caption line is emitted as literal text and does not attach to anything → warning CDN-0009. Caption text inherits no scope-chain slot; the parent block's attributes are set independently.

**Escaping.** ``\^`` at line start suppresses the caption opener → ``Paragraph([Text("^ ...")])``. See [§8.2](#s-8-2).

**Example:**

```
Input:
  | Name | Score |
  |:-----|------:|
  | Alice |   42 |
  ^ Results from the first cohort

AST:
  Table {
    caption: [Text("Results from the first cohort")],
    rows: [Row(type:"Header",...), Row(type:"Row",...)],
    attributes: []
  }

Input:
  > To be, or not to be.
  ^ William Shakespeare, __Hamlet__, Act 3

AST:
  QuoteBlock {
    caption: [Text("William Shakespeare, "), Emphasis([Text("Hamlet")]), Text(", Act 3")],
    children: [Paragraph([Text("To be, or not to be.")])],
    attributes: []
  }
```

== 7. Input Interpretation

Cutdown does not preprocess the input into a normalized copy. The parser reads the raw file and applies the following **interpretive rules** — the source text is never rewritten, so there is exactly one coordinate system: UTF-16 code-unit offsets into the raw input (see [§14](#s-14), Location Type).

1. **Encoding:** Cutdown receives decoded text from a valid UTF-8 input boundary. Decoding transport bytes, including buffering an incomplete multi-byte sequence, happens before Cutdown parsing. A persisted Cutdown file MUST be valid UTF-8; malformed byte sequences are outside the Cutdown input model.
2. **Unicode normalization:** Identifiers (labels, references, mentions) are **compared under NFC** — two identifiers match if their NFC forms are equal. The source text itself is never rewritten to NFC (a textual normalization pass would shift ``loc`` offsets). Authors SHOULD store files in NFC.
3. **BOM:** A UTF-8 BOM (``U+FEFF``) at the start of the input is skipped — the first content offset is 1 instead of 0. A BOM appearing anywhere else in the document is treated as a regular Unicode character.
4. **Null bytes:** ``U+0000`` is not valid content. The parser MUST emit ``U+FFFD`` (Unicode replacement character) in its place in ``Text`` values and MAY emit a diagnostic. ``U+0000`` and ``U+FFFD`` are both one UTF-16 code unit, so offsets are unaffected.
5. **Line terminators:** ``\r\n``, lone ``\r``, and ``\n`` are all line terminators. This is lexer behavior, not a rewrite: in a ``\r\n`` file the ``\r`` is simply part of the terminator, and line content ends before it. If the input does not end with a line terminator, the lexer treats end-of-input as a virtual terminator; no text is appended.
6. **Document-edge blank lines:** Leading and trailing **blank lines** (lines containing only whitespace characters per [§10.1](#s-10-1)) are skipped by the block phase — they produce no blocks and no diagnostic. A document consisting entirely of blank lines produces an empty AST. The skipped text remains part of the raw input for offset purposes.
7. **Tabs:** A tab character (``\t``) outside fenced blocks is **treated as** a single space (``U+0020``) for all structural and inline purposes. Tab and space are both one UTF-16 code unit, so offsets are unaffected. No diagnostic is emitted.

   - Leading tab on a block line → treated as one leading space, then disregarded by block classification
   - Tab inside inline content → treated as a single space (participates in whitespace collapsing)
   - Tab inside code/meta/math fences → preserved literally in the emitted ``raw`` value
8. **HTML entities:** HTML character references (``&amp;``, ``&lt;``, ``&#160;``, ``&nbsp;``, etc.) are **not decoded**. They are emitted as literal ``Text`` segments. The parser has no HTML entity table. Consumers rendering to HTML are responsible for deciding whether to re-encode or pass through.
9. **BiDi Control Characters:** Unicode bidirectional control characters (e.g., ``U+200E`` LRM, ``U+200F`` RLM, ``U+2066``–``U+2069`` Isolates) MUST be preserved literally in ``Text`` segments. The parser performs no special BiDi-aware reordering; it operates strictly on logical character order.

=== 7.1 Streaming input snapshots

A producer MAY make source available incrementally. After each decoded Unicode scalar value, the available source is an input snapshot and MUST be parsed by the ordinary Cutdown grammar. End of input is a virtual line terminator as defined above, whether it denotes a saved file, a cancelled producer, or the source currently available from a still-open producer.

Cutdown defines neither chunks nor a completion signal. Ordering, retries, replacements, deletion, synchronization, and producer identity are outside this specification.

== 8. Escaping

**Rule:** A ``\`` (backslash) before a special character emits the literal character. The ``\`` is consumed.

A backslash before a **non-special** character emits both the backslash and the character literally. The ``\`` is NOT consumed silently.

```
\*   →  Text("*")
\\   →  Text("\")
\a   →  Text("\a")
```

=== 8.1 Special Characters

The following characters are special and may be escaped:

```
= # * _ ~ ` $ [ ] ( ) ! { } : - > / \ | " ' ^
```

Escaping inside opaque containers (CodeInline, CodeBlock, Meta, MathBlock, CommentBlock) follows narrow per-construct rules — see [§8.3](#s-8-3).

=== 8.2 Block-Opener Escapes

A backslash before any one character of a line-start block marker suppresses the marker. The line becomes a ``Paragraph`` with the marker chars emitted as literal text. The ``\`` is consumed; marker chars are kept verbatim.

Applies at **line start only** (after container-indent stripping, before the first non-whitespace char of the marker). Mid-line occurrences of these chars are governed by [§8](#s-8) inline rules.

| Construct | Marker | Escape forms (all equivalent) | Result |
|---|---|---|---|
| Heading ([§4.2](#s-4-2)) | ``=`` ... ``=========`` | ``\=``, ``\==``, ``\===`` ... | ``Paragraph([Text("= ...")])`` |
| List item ([§4.7](#s-4-7)) | ``- `` | ``\- item`` | ``Paragraph([Text("- item")])`` |
| QuoteBlock ([§4.6](#s-4-6)) | ``> `` | ``\> quoted`` | ``Paragraph([Text("> quoted")])`` |
| PageBreaker ([§4.10](#s-4-10)) | ``---`` | ``\---``, ``-\--``, ``--\-`` | ``Paragraph([Text("---")])`` — top level only; no Page boundary occurs |
| FileRef ([§4.11](#s-4-11)) | ``/path`` | ``\/path`` | ``Paragraph([Text("/path")])`` |
| CodeBlock ([§4.4](#s-4-4)) | ``\`\`\``` | ``\\`\`\```, `` \`\`` ``, `` ``\\` `` | Paragraph; residual backticks still feed inline parsing |
| Meta ([§4.3](#s-4-3)) | ``~~~`` | ``\~~~``, ``~\~~``, ``~~\~`` | ``Paragraph([Text("~~~")])`` |
| MathBlock ([§4.5](#s-4-5)) | ``$$$`` | ``\$$$``, ``$\$$``, ``$$\$`` | ``Paragraph([Text("$$$")])`` |
| CommentBlock ([§4.16](#s-4-16)) | ``###`` (line start) | ``\###``, ``#\##``, ``##\#`` | ``Paragraph([Text("###")])`` |
| NamedBlock ([§4.13](#s-4-13)) | ``:::name`` | ``\:::name``, ``:\::name``, ``::\:name`` | ``Paragraph([Text(":::name")])`` — **no CDN-0013** |
| SpoilerBlock ([§4.15](#s-4-15)) | ``^^^`` | ``\^^^``, ``^\^^``, ``^^\^`` | ``Paragraph([Text("^^^")])`` |
| Caption ([§6.2](#s-6-2)) | ``^ `` | ``\^ text`` | ``Paragraph([Text("^ text")])`` |
| Table row ([§4.8](#s-4-8)) | ``|`` | ``| cell |`` | ``Paragraph([Text("| cell |")])`` |

**Notes:**

- The escape suppresses __the block opener only__. Residual characters re-enter normal processing, regardless of which of the three marker chars carried the escape. Example: ``\\`\`\```, `` \`\`` ``, and `` ``\\` `` all become a Paragraph containing three backtick characters; inline parsing then opens a ``CodeInline`` from the first two and leaves the third as a literal ``\```. To produce three literal backticks, escape each: ``\\`\\`\\```.
- Attributes that follow an escaped marker are literal text — there is no block to attach them to. ``\--- {.cover}`` → ``Paragraph([Text("--- {.cover}")])``.
- The rule applies in every block scope (Page scope, ListItem, TaskItem, QuoteBlock, NamedBlock, SpoilerBlock).
- ``##`` (line comment, mid-line) is escaped with ``\##`` or ``#\#`` per [§2.2](#s-2-2) — that is a separate inline escape, not a block-opener escape.
- No diagnostic is emitted when an opener is escaped. The backslash is a deliberate author signal.

=== 8.3 Opaque-Block Closer Escapes

Opaque containers (whose content is captured verbatim) admit a single **narrow escape**: the fence character itself. All other backslashes inside opaque content are literal.

| Container | Escape | Result | Other ``\X`` inside |
|---|---|---|---|
| CodeInline ([§5.6](#s-5-6)) | ``\\``` | literal ``\``` | literal ``\X`` (including ``\\`` → two chars) |
| CodeBlock ([§4.4](#s-4-4)) | ``\\``` | literal ``\``` | literal ``\X`` |
| Meta ([§4.3](#s-4-3)) | ``\~`` | literal ``~`` | literal ``\X`` |
| CommentBlock ([§4.16](#s-4-16)) | ``\#`` | literal ``#`` | literal ``\X`` |
| MathBlock ([§4.5](#s-4-5)) | — **none** | n/a | fully literal (LaTeX owns ``\``) |

**Notes:**

- The escape applies **uniformly throughout opaque content** — line-start or mid-line. ``\fence`` anywhere produces ``fence``. A closer line ``\~~~``, ``~\~~``, or ``~~\~`` keeps the Meta block open and emits a literal ``~~~`` content line. (Mid-line uniformity is a simplification — closers are only detected at line start, so mid-line escape has no closer-suppression effect; it just affects the captured raw character.)
- ``\\`` inside an opaque (non-Math) block emits two literal characters ``\\``. Backslash is "active" only directly before its fence character.
- ``\#`` in a CommentBlock always escapes to ``#`` (octothorpe), regardless of how many ``#`` characters neighbor it. The rule does not look at run length.
- **MathBlock carve-out:** no escape is processed because LaTeX assigns its own meaning to backslash. The trade-off is that a literal ``$$$`` line inside a MathBlock body is unsupported — wrap such content in a ``CodeBlock`` or split the math.
- **NamedBlock** (``:::``) and **SpoilerBlock** (``^^^``) are not opaque — their children are parsed as blocks. To prevent a content line from being read as the container's closer, use the same escape mechanic as [§8.2](#s-8-2): a ``\`` before any one of the three fence chars at line start (e.g., ``\:::``, ``:\::``, ``::\:`` for NamedBlock; ``\^^^``, ``^\^^``, ``^^\^`` for SpoilerBlock). The line becomes a Paragraph containing the literal fence chars.
- No diagnostic is emitted for closer escapes.

== 9. Parsing Algorithm

Cutdown's parsing model makes three testable guarantees:

1. **Single pass.** Every character of an input snapshot is scanned a bounded number of times; parsing is linear time in snapshot length. Degradation emits verbatim substrings identified by source offset — committed text is never re-lexed or re-inline-parsed. An incremental implementation MAY retain an unresolved suffix between snapshots; its result for each snapshot MUST equal parsing that snapshot afresh ([§16](#s-16)).
2. **Bounded lookahead.** At most one line at block level; at most to end of line at inline level.
3. **Deferred attachment.** A deferred structural decision attaches or regroups already-built nodes only; it never re-parses text. The deferral windows are: one-block emission latency (a caption line or attribute-continuation line may bind to the preceding block) and open-inline buffering until end of line.

=== 9.1 Phase 1 — Input Interpretation

1. Receive decoded text from the UTF-8 input boundary ([§7](#s-7)).
2. Apply the interpretive rules of [§7](#s-7): ``\r\n`` / ``\r`` / ``\n`` all read as line terminators, tabs outside fences read as single spaces, leading BOM skipped. The source text is never rewritten — all offsets index the raw input ([§14](#s-14), Location Type).

=== 9.2 Phase 2 — Block Identification

1. Split input into lines.
2. **Detect ``##`` boundaries and record Reflection payloads.** Walk lines top-to-bottom, maintaining "opaque context" state (inside ``CodeBlock``, ``Meta``, ``MathBlock``, or ``CommentBlock``). Within an opaque context, the opener line and the closer line are scanned; the body lines between them are not. On all other lines, scan in source order for the first un-escaped ``##`` not occurring inside ``CodeInline`` (`` `` \``), ``MathInline`` (``$$``), or a quoted attribute value. If found: characters before ``##`` are the line's structural content; characters from ``##`` to (but not including) ``\n`` are the comment payload. Block classification (Phase 3) operates on the pre-``##`` substring. The payload is later attached to the appropriate block as a ``Reflection`` entry ([§2.2](#s-2-2)) — it does not enter the inline stream.
3. Identify block boundaries: a sequence of non-blank (in pre-``##`` content) lines bounded by blank lines (or document start/end) is a **block candidate**.
4. Apply the fenced constructs that override blank-line boundaries — ``\`\`\```, ``~~~``, ``:::``, ``$$$``, ``^^^`` and ``###``. Each opens until its closer or end of document; see [§10.4.2](#s-10-4-2) for the shared fence pattern and [§4.3](#s-4-3)–[§4.16](#s-4-16) for each construct.

=== 9.3 Phase 3 — Block Classification

Each block candidate is classified by its first line:

| First line matches | Block type |
|---|---|
| ``^(={1,9}) `` | Heading → Section |
| ``^---`` | PageBreaker (top level only; no node — [§9.6](#s-9-6)) |
| ``^`` \`\`` ` | CodeBlock |
| ``^~~~`` | Meta |
| ``^:::[ID_LITERAL]`` | NamedBlock |
| ``^|`` | Table |
| ``^>`` | QuoteBlock |
| ``^- `` or ``^- \[[ x]\] `` | List (unordered / task) |
| ``^[0-9]+\. `` | List (ordered) |
| ``^\[^[ID_LITERAL]`` | RefDefinition |
| ``^\$\$\$`` | MathBlock |
| ``^\^\^\^`` | SpoilerBlock |
| ``^###`` | CommentBlock |
| ``^/`` | FileRef |
| ``^!\[`` | ImageBlock — provisional; falls back to Paragraph if the image is not the only segment on the line ([§4.9](#s-4-9)) |
| (anything else) | Paragraph |

=== 9.4 Phase 4 — Inline Parsing

Inline content is parsed in source order within each block that contains inline content. The parser:

1. Scans for openers (``**``, ``__``, ``~~``, ``^^``, '\``', ``[``, ``![``, ``::``, ``{{``, ``""``, ``''``, ``$$``).
2. On finding an opener, scans forward for a valid closer.
3. If no valid closer is found, the opener degrades. The degradation rule depends on the opener's class ([§9.4.1](#s-9-4-1)).
4. Resolves escape sequences ``\x`` before delimiter matching.
5. Collects trailing ``{attrs}`` after each completed inline element.

==== 9.4.1 Degradation classes

Inline openers fall into two classes with different degradation behavior. In both cases no diagnostic is emitted — degradation to visible literal text is silent by design.

**Class 1 — symmetrical doubled delimiters: opener-as-text.**

| Opener | Construct |
|---|---|
| ``**`` | Strong |
| ``__`` | Emphasis |
| ``~~`` | Highlight |
| ``^^`` | Spoiler |
| `` `` \`` | CodeInline |
| ``$$`` | MathInline |
| ``""`` / ``''`` | QuoteInline |

If no closer is found before the end of the inline context, the opener alone is emitted as ``Text`` and parsing continues immediately after it. Constructs following the dead opener are parsed normally: ``**a __b__ c`` yields ``Text("**a ")``, ``Emphasis(b)``, ``Text(" c")``.

**Class 2 — asymmetrical bracket-like openers: verbatim slice.**

| Opener | Construct |
|---|---|
| ``[`` / ``![`` | Link / ImageInline |
| ``{{`` | Variable |
| ``{`` | attribute scan ([§6](#s-6)) |

An unresolved Class 2 opener causes the source from the opener to its terminator — end of line, or the ``##`` cut ([§2.2](#s-2-2)) — to be emitted as a single verbatim ``Text`` run, copied from the source by offset. The slice is never inline-parsed: closed constructs inside a dead slice are lost (they remain literal). Constructs committed __before__ the opener are retained. ``[a __b__ c`` yields ``Text("[a __b__ c")`` — the ``Emphasis`` inside the dead slice does not exist.

**Attribute braces.** ``{`` (left brace) opens an attribute scan running to the matching ``}`` (right brace) or end of line. If the content violates the attribute grammar ([§6](#s-6)) or the ``}`` never arrives, the entire slice — braces included, when present — is emitted as verbatim ``Text`` and never inline-parsed. This is the intentional **literal-span idiom**: ``{a **b**}`` is the literal text ``{a **b**}``. Consequence: any future extension of the attribute grammar is a breaking change for text relying on this idiom.

**Class 3 — bracket-matched, counted: ``Mark``.**

``::name … ::`` ([§5.10](#s-5-10)) is the only construct whose opener and closer are textually distinct, so the parser matches them by counting rather than by taking the first closer. The matching rule, the depth cap and CDN-0031 are defined in [§5.10](#s-5-10).

An opener that never matches degrades per **Class 1**: the opener alone (``::`` plus the name) is emitted as ``Text`` and parsing continues immediately after it, so following constructs parse normally.

``##`` boundaries are NOT re-scanned during Phase 4 — they were established in Phase 2 ([§9.2](#s-9-2)). The inline parser receives only the pre-``##`` substring of each line. When that substring leaves an inline opener unclosed (e.g. ``[text `` with no ``]`` (right bracket) because ``##`` swallowed it), the opener degrades per its class ([§9.4.1](#s-9-4-1)) — for a Class 2 opener the ``##`` cut acts as the slice terminator. See [§2.2](#s-2-2) for examples.

Reference links (``[text][^ref]``) are emitted as ``Link { kind: "ref" }`` in-place. Resolution against ``RefDefinition`` segments is the consumer's responsibility.

Citation links (``[text][@cite]``, including ``[][@cite]``) are emitted as ``Link { kind: "cite" }`` in-place. Citation resolution is the consumer's responsibility.

=== 9.5 Derived Structure

Parsing (Phases 1–4) produces a **flat block sequence** — one for the document root, and one for the child list of every block container (``ListItem``, ``TaskItem``, ``QuoteBlock``, ``NamedBlock``, ``SpoilerBlock``). ``Section`` nesting and the ``Page[]`` division are not parsed; they are **derived** from these flat sequences by two deterministic folds: the sectionization fold ([§9.5.1](#s-9-5-1)) and the pagination fold ([§9.5.2](#s-9-5-2)).

**Implementation neutrality.** The folds define the resulting tree, not an implementation strategy. A parser MAY interleave fold logic with block classification, run the folds as separate post-passes, or use any other strategy — it conforms as long as it produces the same tree.

==== 9.5.1 Sectionization fold

The sectionization fold applies independently to every flat block sequence (root and each container child list).

**A ``Section`` spans from its heading to the next heading of level ≤ its own within the same sequence, or to the sequence's end.** Equivalently, walking the sequence in document order:

1. On a heading of level ``n``: close all open Sections of level ≥ ``n`` within this sequence, then open a new ``Section(level=n)``.
2. All subsequent non-heading blocks belong to the innermost open Section.
3. All open Sections close at the end of the sequence. Section scope never crosses a container boundary.

**Skipped levels.** A heading whose level is deeper than the innermost open Section by more than one (e.g. ``=`` followed directly by ``===``) nests under the nearest shallower open Section. The written level is preserved in the ``Section`` node; no intermediate Sections are synthesized; no diagnostic is emitted. The written level is the source of truth — tree depth is incidental, and consumers that need a normalized depth derive it themselves.

**Section attributes** are those on the heading line only. Rule B ([§6](#s-6)) never assigns a scope-chain slot to a Section.

==== 9.5.2 Pagination fold

The pagination fold applies **only to the root sequence** — blocks inside containers never affect pagination regardless of their type. Two items drive the fold: ``Meta`` blocks and PageBreakers ([§9.6](#s-9-6)).

1. The document begins with ``Page[0]``, initially empty (``meta: null``, ``children: []``).
2. A **PageBreaker** unconditionally closes the current Page — as a Ghost Page if it is empty — and opens a new empty Page. A PageBreaker also closes all open root-level Sections. Every PageBreaker produces a page boundary: a leading ``---`` at document start yields a leading Ghost Page; consecutive separators yield Ghost Pages.
3. A **``Meta`` block** is handled by the current Page's ``meta`` slot, not by its position in the document.

   - Slot empty → the ``Meta`` fills it. No new Page is created, whatever content the Page already holds.
   - Slot already set → the ``Meta`` closes the current Page and opens a new Page carrying itself as ``meta``.

   Content before a ``Meta`` therefore never creates a Page: a ``Meta`` after paragraphs, comments, or any other block still fills the Page those blocks are on. A ``Meta`` after a PageBreaker fills the Page the PageBreaker opened — the boundary is the PageBreaker's doing, and the ``Meta`` adds none of its own.
4. All other root blocks are appended to the current Page's ``children``.
5. Ghost Pages (``meta: null``, ``children: []``) are valid and emitted as-is. Consumers decide how to handle them.

=== 9.6 PageBreaker

A **PageBreaker** is the pagination signal consumed by the fold in [§9.5.2](#s-9-5-2). It produces no AST node. The construct — its syntax, its diagnostics, and its behaviour inside block containers — is defined in [§4.10](#s-4-10).

=== 9.7 Incremental availability

Incremental availability does not create a second parsing mode. At every decoded Unicode-scalar boundary, an implementation MUST produce the same AST and diagnostics as an ordinary parse of the source available at that boundary. Later source MAY reinterpret an unresolved inline suffix, an open block/container, or derived structure that depends on it. Consumers MAY delay semantic rendering until an end of block ([§1](#s-1), [§16](#s-16)); Cutdown defines no rendering schedule.

== 10. Block Structure and Block Boundaries

=== 10.1 Block Boundaries

Blocks are separated by one or more **blank lines**. A blank line is a line containing only whitespace characters (under the interpretive rules of [§7](#s-7)); consecutive blank lines act as one ([§12.1](#s-12-1)).

A parser identifies block boundaries by scanning for blank line sequences. Each contiguous run of non-blank lines is classified by its first line ([§9.3](#s-9-3)). A run does not always produce one block:

- A **single-line block** — ``Section``, ``FileRef``, ``ImageBlock``, ``RefDefinition``, a table row — consumes its own line. The rest of the run re-enters classification, so ``= Title`` followed directly by ``content`` yields a ``Section`` and a ``Paragraph``.
- A **``Paragraph``** consumes the whole run: it ends at the blank line, never before.
- A **fenced block** consumes from its opener to its closer, which may lie past a blank line ([§10.4.2](#s-10-4-2)).

**Block elements cannot interrupt a paragraph.** Once a run has been classified as a ``Paragraph``, every later line in it is paragraph content, whatever it looks like — this holds for **every** block opener without exception, fences included. See [§4.1](#s-4-1), which defines the rule.

Comments ([§2](#s-2)) are detected in Phase 2 before block boundary analysis. A line starting with ``###`` is a ``CommentBlock`` fence (produces an AST node). A line starting with ``##`` (pre-``##`` content empty) acts as a blank line for block-boundary purposes and stores its payload as a ``Reflection`` entry on the nearest block. See [§2](#s-2) for the full semantics and [§10.4.4](#s-10-4-4) for the symbol-repetition table.

=== 10.2 Leading and Trailing Whitespace

Any number of leading spaces (including none) are stripped before block classification. Indentation has exactly two uses in Cutdown:

1. **Block classification ignores it.** The stripped line determines the block type.
2. **List nesting uses it.** For a list marker, the parser records the marker's **original column** (before stripping) as separate metadata and feeds it to the nesting stack model ([§10.5](#s-10-5)). The column is used during list parsing only.

Indented code blocks (as in CommonMark) are not supported.

Trailing-space handling is defined in [§12.1](#s-12-1).

=== 10.3 Block Classification

Each block candidate is classified by its first line (see [§9.3](#s-9-3) for the full classification table).

=== 10.4 Syntax Primitives

Cutdown uses two structural patterns for delimiters:

> **Philosophy:** because the delimiter is always a __doubled__ symbol, every **single** symbol stays literal. ``snake_case``, an apostrophe in "don't", ``2*3``, and ``a_b`` need no escaping in ordinary prose. A reader who knows this can predict the rest of the grammar instead of memorising it: one symbol is text, two open an inline segment, three open a block.

==== 10.4.1 Doubled-symbol inline delimiter

Any two identical characters form an inline block delimiter:

```
<Symbol><Symbol> content <Symbol><Symbol> {attrs}
```

The opener and closer are the same doubled symbol. Inline blocks delimited this way are composable (nestable with other types). The parser recognizes a built-in exclusive list of doubled symbols (see [§5](#s-5)). Unrecognized doubled symbols are emitted as literal text.

==== 10.4.2 Tripled-symbol block delimiter

Any three identical characters form a block delimiter:

```
<Symbol><Symbol><Symbol>[name] {attrs}
content
<Symbol><Symbol><Symbol>
```

The opener may carry an optional name and attributes. The closer is the bare tripled symbol. The parser recognizes a built-in exclusive list of tripled symbols (see [§4](#s-4)). Unrecognized tripled symbols are emitted as literal text.

==== 10.4.3 Delimiter placement

Doubled-symbol delimiters may appear:

- Surrounded by spaces: ``aa ** bb ** cc``
- Adjacent to literal text on one or both sides: ``aa**bb**cc``
- Adjacent to another delimiter: ``__**text**__``

In all cases the delimiter is recognized. Whether an unmatched opener is treated as literal text follows the same rule as all inline constructs ([§5](#s-5), [§9.4](#s-9-4)).

==== 10.4.4 Symbol repetition

When N identical characters appear at an inline position, the following rules apply:

**Inline delimiter symbols** (inline parsing context):

| Symbol | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|
| ``*`` | literal | ``Strong`` open/close | ``**`` + ``*`` literal | ``Strong([])`` empty | ``Strong([])`` + ``*`` literal |
| ``_`` | literal | ``Emphasis`` open/close | ``__`` + ``_`` literal | ``Emphasis([])`` empty | ``Emphasis([])`` + ``_`` literal |
| ``"`` | literal | ``QuoteInline(double)`` open/close | ``""`` + ``"`` literal | ``QuoteInline([])`` empty | ``QuoteInline([])`` + ``"`` literal |
| ``'`` | literal | ``QuoteInline(single)`` open/close | ``''`` + ``'`` literal | ``QuoteInline([])`` empty | ``QuoteInline([])`` + ``'`` literal |
| ` | literal | ``CodeInline`` open/close | \`` + ` literal¹ | ``CodeInline("")`` empty | ``CodeInline("\``")` |
| ``~`` | literal | ``Highlight`` open/close | ``~~`` + ``~`` literal¹ | ``Highlight([])`` empty | ``Highlight([])`` + ``~`` literal |
| ``$`` | literal | ``MathInline`` open/close | ``$$`` + ``$`` literal¹ | ``MathInline("")`` empty | ``MathInline("$")`` |
| ``^`` | literal² | ``Spoiler`` open/close | ``^^`` + ``^`` literal¹ | ``Spoiler([])`` empty | ``Spoiler([])`` + ``^`` literal |
| ``#`` | literal | ``##`` line comment → Reflection entry (to EOL, no closer) | ``CommentBlock`` fence³ | ``CommentBlock`` fence + ``#`` literal | ``CommentBlock`` fence + ``##`` literal |

¹ When appearing at the **start of a block line**, ``\`\`\```, ``~~~``, ``$$$``, ``^^^`` are block fences (CodeBlock, Meta, MathBlock, SpoilerBlock respectively). In inline context, they parse as 2-delimiter + 1 literal.

² A single ``^`` (caret) is literal in inline context. Inside a ``[...][^id]`` link/definition target slot it retains its reference-marker role ([§4.14](#s-4-14), [§5.5](#s-5-5)); that role is delimited by the surrounding brackets and never reaches the Spoiler parser.

³ ``###`` is a block fence only when it begins a block candidate — that is, when it is the first non-whitespace content of a line after container-indent stripping ([§10.2](#s-10-2), [§10.6](#s-10-6)). In inline position, ``###`` parses as ``##`` (line comment opener — runs to EOL) + ``#`` (collapsed into the payload text).

**Block/structural symbols**:

| Symbol | 1 | 2 | 3 | 4+ |
|---|---|---|---|---|
| ``=`` | Heading L1 (+space) | Heading L2 | Heading L3 | … up to L9; 10+ = literal |
| ``>`` | QuoteBlock L1 | QuoteBlock L2 | QuoteBlock L3 | Level N (no limit) |
| ``-`` | list marker (``- ``+space) or literal | literal ``--`` | PageBreaker (top level; no node — [§9.6](#s-9-6)) | PageBreaker (tail dropped, CDN-0016) |
| ``:`` | literal | Mark ``::name … ::`` ([§5.10](#s-5-10)) | NamedBlock prefix ``:::name`` | literal |
| ``^`` | Caption line when followed by a space (``^ text``, [§6.2](#s-6-2)) | inline ``Spoiler`` opener — not a block | SpoilerBlock fence¹ | — |
| ``|`` | Table row ([§4.8](#s-4-8)) | — | — | — |
| ``/`` | FileRef when followed by a path ([§4.11](#s-4-11)) | — | — | — |

The two halves of this table use different keys. Rows for ``=``, ``>``, ``:`` and ``#`` are keyed by run length. Rows for ``-``, ``^``, ``\|`` and ``/`` are keyed by the character that **follows** the symbol at column 1 — a space, a cell, or a path — not by repetition.

Paired symbols (``{}``/``[]``) follow their own rules and are not covered by this table; this includes the bracket-initiated block openers ``![`` (ImageBlock, [§4.9](#s-4-9)) and ``[^`` (RefDefinition, [§4.14](#s-4-14)).

==== 10.4.5 Delimiter collisions

When N identical characters appear and the parser recognizes a delimiter of length 2 or 3 at that position, the maximum recognized length is consumed as the delimiter. Any remaining characters are treated as literal content.

Known collisions:

| Sequence | Parsed as |
|---|---|
| ``~~~`` at inline position | ``~~`` (Highlight opener) + ``~`` (literal) |
| ``$$$`` at inline position | ``$$`` (MathInline opener) + ``$`` (literal) |
| \`\`` at inline position | \`` (CodeInline opener) + ` (literal inside) |
| ``"""`` at inline position | ``""`` (QuoteInline double opener) + ``"`` (literal) |
| ``'''`` at inline position | ``''`` (QuoteInline single opener) + ``'`` (literal) |
| ``^^^`` at inline position | ``^^`` (Spoiler opener) + ``^`` (literal) |
| ``###`` at inline position | ``##`` (line comment → Reflection entry; trailing ``#`` is part of the payload text) |
| ``---`` non-line-start | literal text (PageBreaker only recognized at top-level line start) |

=== 10.5 List Indentation Model

Cutdown uses a **stack-based, column-relative** model for all list types (unordered, ordered, task). Nesting is determined by comparing marker columns, not by fixed indentation increments.

**Definitions:**

- The **column** of a line is its count of leading spaces before the first non-space character. Recorded from the original source before leading-space stripping ([§10.2](#s-10-2)).
- The parser maintains a **nesting stack** of ``(col, item)`` pairs representing the currently open items from outermost to innermost.

**New marker at column C** (pop-then-push rule):

1. While the stack is non-empty and ``C ≤ top.col``: pop.
2. Push the new item at column C.

A marker with ``C > top.col`` is a nested child (+1 depth). A marker with ``C ≤ top.col`` closes items until a shallower ancestor is found, then opens a sibling.

**Non-marker line (continuation text) at column C:**

1. While the stack depth ≥ 2 and ``C < second-from-top.col``: pop.
2. Continue the now-current item.

Depth-0 items (no parent on the stack) accept any non-blank non-marker line unconditionally (threshold = −∞).

**Blank lines:**

- Blank line followed by content at **col 0** → block boundary. The current ``List`` segment ends. If the next line is a list marker, a new ``List`` node begins.
- Blank line followed by content at **col > 0** → absorbed by the list parser. The stack persists. A list marker continues the list via the pop-then-push rule; a non-marker line becomes block content inside the current item (``ListItem.children`` becomes ``Block[]``). The ``List`` is marked ``loose: true``.

**Style note:** Two spaces of indentation per nesting level is recommended. The parser accepts any positive column delta as a valid nesting step; the stack model resolves all cases unambiguously.

```
Input (standard):
  - item 0.0
  - item 1.0
    - item 1.1
    - item 1.2

AST:
  List { kind: "bullet", loose: false }
  ├── ListItem { Text("item 0.0") }
  └── ListItem { Text("item 1.0") }
      └── List { kind: "bullet", loose: false }
          ├── ListItem { Text("item 1.1") }
          └── ListItem { Text("item 1.2") }
```

```
Input (loose list — absorbed blank line):
  - First item
    continues here
  - Second item

    This starts a new paragraph inside item two.

AST:
  List { kind: "bullet", loose: true }
  ├── ListItem
  │   └── Text("First item continues here")
  └── ListItem
      ├── Paragraph("Second item")
      └── Paragraph("This starts a new paragraph inside item two.")
```

=== 10.6 Container-Edge Blank Lines

Leading and trailing **blank lines** (lines containing only whitespace per [§10.1](#s-10-1)) are stripped from the body of non-opaque block containers before their children are parsed. No diagnostic is emitted.

**Applies to:**

- ``NamedBlock`` ([§4.13](#s-4-13))
- ``SpoilerBlock`` ([§4.15](#s-4-15))
- ``QuoteBlock`` ([§4.6](#s-4-6))
- ``ListItem`` / ``TaskItem`` ([§4.7](#s-4-7))

**Does NOT apply to opaque containers** — their bodies are captured verbatim:

- ``CodeBlock`` ([§4.4](#s-4-4))
- ``Meta`` ([§4.3](#s-4-3))
- ``MathBlock`` ([§4.5](#s-4-5))
- ``CommentBlock`` ([§4.16](#s-4-16))

**Pipeline order:**

1. Document-edge blank-line strip ([§7](#s-7) step 6).
2. Block classification and container body extraction.
3. **Container-edge blank-line strip (this section).**
4. Indent-base detection (NamedBlock / SpoilerBlock — [§4.13](#s-4-13) / [§4.15](#s-4-15)). The "first content line" that establishes the base indentation is the first non-blank content line __after__ edge-trim.
5. Child parsing.

A container whose body is empty after edge-trim produces ``children: []`` with no diagnostic. The container itself is preserved — it is a deliberate author construct.

```
Input:
  :::note

  content

  :::

AST:
  NamedBlock { name: "note", children: [Paragraph([Text("content")])] }
```

```
Input:
  :::note

  :::

AST:
  NamedBlock { name: "note", children: [] }
```

For ``QuoteBlock``, a "blank line in the body" is a line whose ``>``-stripped content is empty (e.g., ``> `` alone, or ``>`` alone). Such lines at the leading or trailing edge of the quoted body are stripped before child parsing.

== 11. Precedence Rules

When multiple constructs compete for the same input, the following priority applies (highest first):

| Priority | Construct | Notes |
|---|---|---|
| 1 | CodeBlock fence ``\`\`\``` | Content always literal |
| 2 | MetaBlock fence ``~~~`` | Content always literal |
| 3 | MathBlock ``$$$`` | Content always literal |
| 4 | CommentBlock ``###`` | Content always literal (opaque). See [§2.3](#s-2-3), [§4.16](#s-4-16) |
| 5 | Inline code \`` ``CodeInline`` | Content literal |
| 6 | Line comment ``##`` | No closer; runs to EOL. Payload stored as ``Reflection`` entry on enclosing block. Acts as the terminator for any open inline constructs, which degrade per their class ([§9.4.1](#s-9-4-1)) — Class 2 openers emit the verbatim slice up to the ``##`` cut. Not recognized inside CodeInline / MathInline / quoted attribute values. See [§2.2](#s-2-2) |
| 7 | Escape ``\x`` | Resolved before delimiter matching |
| 8 | Links and images ``[...](...)`` | Matched before emphasis runs |
| 9 | Inline math ``$$`` | Matched before emphasis; content is literal |
| 10 | Strong ``**``, Emphasis ``__``, Highlight ``~~``, Spoiler ``^^``, QuoteInline ``""`` ``''`` | Source order, greedy |
| 11 | Mark ``::name … ::`` | Matched after emphasis. The name run is lexed before delimiter matching and is independent of this table — see __Name lexing__ in [§5.10](#s-5-10) |
| 12 | Variable ``{{key}}`` / Attributes ``{...}`` | Longest opener wins (``{{`` before ``{``), then source order |

Note: MathInline content is always literal (no inline parsing). MathBlock content is always literal.

== 12. Whitespace Rules

=== 12.1 Whitespaces in Block Segments

| Situation | Rule |
|---|---|
| Line endings | ``\r\n``, lone ``\r``, and ``\n`` all read as line terminators ([§7](#s-7)); the text is not rewritten |
| Encoding | UTF-8 required |
| Trailing spaces | Collapsed to a single space. That space is **preserved before a soft break** — it becomes ``Text(" ")`` in the AST, serving as an explicit word-boundary separator — and **dropped at a block boundary** — a blank line, end of input, or the end of a single-line block's inline context (a heading line, a table cell, a caption line) |
| Leading spaces on block line | Stripped before block classification |
| Leading spaces on paragraph continuation line | Stripped before inline parsing |
| Multiple blank lines | Treated as a single blank line |
| Tabs outside fenced blocks | Treated as a single space for classification ([§7](#s-7)); the text is not rewritten |
| Tabs inside code/meta/math fences | Preserved literally |
| Blank lines inside code/math fence | Preserved literally in ``content`` string |
| Blank lines inside meta fence | Passed through in ``raw`` string |
| Soft break (single newline in paragraph) | Folded to zero — no character emitted, no AST segment; lines concatenate directly |
| ``LineBreak`` (``\`` at line end) | Produces a ``LineBreak`` segment — a line break inside the paragraph, not a block boundary ([§5.13](#s-5-13)) |
| Whitespace immediately preceding a ``{attr}`` block that is **consumed** by an attribute slot (block-opener last-attr, inline attachment, or scope-chain) | Stripped from the preceding text value. Does NOT apply when ``{...}`` falls through to literal ``Text("{...}")`` per [§6.1.3](#s-6-1-3) (orphan). |

=== 12.2 Whitespaces in Inline Segments

Within any inline block (Emphasis, Strong, Highlight, Spoiler, MathInline, QuoteInline, Mark):

| Situation | Rule |
|---|---|
| Whitespace between two adjacent opening delimiters (nesting context) | Consumed |
| Whitespace between opening delimiter and first literal | Consumed |
| Whitespace between last literal and closing delimiter | Consumed |
| Whitespace between closing delimiter and next sibling opening delimiter | Preserved as ``Text(" ")`` |
| Interior whitespace runs | Collapsed to one space |
| Non-breaking space (``\u00A0``) | Always preserved, never collapsed |

``CodeInline`` is **exempt from whitespace collapsing** — boundary stripping and interior run collapsing do not apply. The paragraph-level soft-break rule (single ``\n`` → zero) also applies: a ``CodeInline`` spanning two lines of a paragraph has the newline removed with no replacement in ``value``. For multi-line code, use ``CodeBlock`` ([§4](#s-4)).

**Examples:**

```
__ bb __           → Emphasis([Text("bb")])
__  text  __       → Emphasis([Text("text")])
__  __             → Emphasis([])
aa__bb__cc         → Text("aa") + Emphasis([Text("bb")]) + Text("cc")
** __ bb __**      → Strong([Emphasis([Text("bb")])])   (space between ** and __ = zero)
** bb ** __ cc __  → Strong([Text("bb")]) + Text(" ") + Emphasis([Text("cc")])
```

== 13. Special Character Reference

Escape rules: [§8](#s-8) (general), [§8.2](#s-8-2) (block-opener escapes), [§8.3](#s-8-3) (opaque-block closer escapes).

| Character | Name | Role | Escapable |
|---|---|---|---|
| ``=`` | equals | Heading marker (line start) | Yes |
| ``#`` | octothorpe | ``##`` line comment → Reflection entry (anywhere) / CommentBlock (``###``, line start) | Yes |
| ``*`` | asterisk | Strong delimiter (``**``) | Yes |
| ``_`` | underscore | Emphasis delimiter (``__``) | Yes |
| ``~`` | tilde | Highlight (``~~``) / MetaBlock fence (``~~~``) | Yes |
| ` | backtick | Inline code (\``) / CodeBlock fence (\`\``) — escapable inside CodeInline as ``\\``` | Yes |
| ``[`` | left bracket | Link/image opener | Yes |
| ``]`` | right bracket | Link/image closer | Yes |
| ``(`` | left parenthesis | Link URL opener | Yes |
| ``)`` | right parenthesis | Link URL closer | Yes |
| ``!`` | exclamation mark | Image prefix | Yes |
| ``{`` | left brace | Attribute/variable opener | Yes |
| ``}`` | right brace | Attribute/variable closer | Yes |
| ``:`` | colon | Mark delimiter (``::``) / NamedBlock (``:::``) | Yes |
| ``-`` | hyphen | List marker / PageBreaker (``---``, top level) | Yes |
| ``>`` | greater-than sign | QuoteBlock marker | Yes |
| ``/`` | slash | File reference (line start) | Yes |
| ``\`` | backslash | Escape character / ``LineBreak`` at line end ([§5.13](#s-5-13)) — also processes ``\\``` inside CodeInline | Yes |
| \| | pipe | Table cell separator (pipe row) / header separator row | Yes |
| ``^`` | caret | Reference link/definition marker / Spoiler delimiter (``^^``, ``^^^``) | Yes |
| ``$`` | dollar sign | Inline math (``$$``) / block math (``$$$``) | Yes |
| ``"`` | double quote | Inline quote delimiter (``""``) | Yes |
| ``'`` | single quote | Inline quote delimiter (``''``) | Yes |

== 14. AST Node (Segment) Reference

=== 14.1 Root Segments

| Segment | Fields |
|---|---|
| ``Document`` | ``type: "Document", children: Page[]`` |
| ``Page`` | ``type: "Page", meta: Meta|null, children: Block[]`` |

=== 14.2 Block Segments

All block segments carry ``reflection: Reflection[] | null`` (null when no ``##`` comment is present). See [§2.2](#s-2-2) for attachment rules.

| Segment | Fields |
|---|---|
| ``Paragraph`` | ``type: "Paragraph", children: Inline[], reflection, attributes`` |
| ``Section`` | ``type: "Section", level: 1..9, heading: Inline[], children: Block[], reflection, attributes`` |
| ``CodeBlock`` | ``type: "CodeBlock", language: string = "text", raw: string, caption: Inline[]|null, reflection, attributes`` |
| ``MathBlock`` | ``type: "MathBlock", raw: string, caption: Inline[]|null, reflection, attributes`` |
| ``QuoteBlock`` | ``type: "QuoteBlock", children: Block[], caption: Inline[]|null, reflection, attributes`` |
| ``List`` | ``type: "List", kind: "bullet"|"numbered"|"checklist", start: int|null, loose: bool, children: (ListItem|TaskItem)[], reflection, attributes`` |
| ``Table`` | ``type: "Table", rows: Row[], columns: Column[], caption: Inline[]|null, reflection, attributes`` |
| ``ImageBlock`` | ``type: "ImageBlock", alt: Inline[], src: string, caption: Inline[]|null, reflection, attributes`` |
| ``FileRef`` | ``type: "FileRef", path: string, fragment: string|'', query: string|'', caption: Inline[]|null, reflection, attributes`` |
| ``FileRefGroup`` | ``type: "FileRefGroup", group: "image"|"video"|"audio", children: (FileRef|ImageBlock)[], caption: Inline[]|null, reflection, attributes`` |
| ``NamedBlock`` | ``type: "NamedBlock", name: string, children: Block[], caption: Inline[]|null, reflection, attributes`` |
| ``SpoilerBlock`` | ``type: "SpoilerBlock", children: Block[], caption: Inline[]|null, reflection, attributes`` |
| ``CommentBlock`` | ``type: "CommentBlock", text: string, reflection`` — no ``attributes``. Hidden by default ([§2.5](#s-2-5)). |
| ``RefDefinition`` | ``type: "RefDefinition", ref: string, children: Inline[], reflection, attributes`` |

=== 14.3 Inline Segments

| Segment | Fields |
|---|---|
| ``Text`` | ``type: "Text", value: string`` |
| ``Emphasis`` | ``type: "Emphasis", children: Inline[], attributes`` |
| ``Strong`` | ``type: "Strong", children: Inline[], attributes`` |
| ``Highlight`` | ``type: "Highlight", children: Inline[], attributes`` |
| ``Spoiler`` | ``type: "Spoiler", children: Inline[], attributes`` |
| ``Link`` | ``type: "Link", kind: "external"|"page"|"tag"|"ref"|"cite", children: Inline[], href: string|'', target: string|'', attributes`` |
| ``CodeInline`` | ``type: "CodeInline", value: string, attributes`` |
| ``MathInline`` | ``type: "MathInline", formula: string, attributes`` |
| ``QuoteInline`` | ``type: "QuoteInline", kind: "double"|"single", children: Inline[], attributes`` |
| ``ImageInline`` | ``type: "ImageInline", alt: Inline[], src: string, attributes`` |
| ``Mark`` | ``type: "Mark", name: string, children: Inline[], attributes`` |
| ``LineBreak`` | ``type: "LineBreak"`` |
| ``Variable`` | ``type: "Variable", key: string, attributes`` |

=== 14.4 Special Nodes

| Segment | Fields |
|---|---|
| ``Meta`` | ``type: "Meta", format: "yaml"|"toml"|"json" = "yaml", raw: string`` |
| ``ListItem`` | ``type: "ListItem", children: (Block|Inline)[], attributes`` |
| ``TaskItem`` | ``type: "TaskItem", checked: bool, children: (Block|Inline)[], attributes`` |
| ``Column`` | ``type: "Column", align: "start"|"left"|"right"|"center"|"comma"|"decimal" = "start"`` |
| ``Row`` | ``type: "Row"|"Header", children: Cell[], attributes`` |
| ``Cell`` | ``type: "Cell", children: Inline[], row: number, column: number`` |

=== 14.5 Synthetic Segments

The AST schema admits nodes that are not producible by parsing. **Conforming parsers never emit them; conforming consumers must accept them.**

| Segment | Fields |
|---|---|
| ``Fragment`` | ``type: "Fragment", meta: Meta|null, children: Block[]`` |

``Fragment`` is a container block with no ``name``. Its ``meta`` carries the ``Meta`` of the source Page it was materialized from, or ``null``. A ``Fragment`` is a section-scope boundary and is opaque to the derived-structure folds ([§9.5](#s-9-5)): the sectionization and pagination folds treat it as a single opaque item.

=== 14.6 Location Type

Every segment MAY carry a source location, ``loc?: Loc``:

```typescript
interface Loc {
  file?: string   // source file identifier, when known
  start: number   // offset of the segment's first code unit
  end: number     // offset one past the segment's last code unit (end-exclusive)
}
```

- Offsets index **UTF-16 code units of the raw input file** — the text exactly as read, before any interpretation ([§7](#s-7)). This mirrors the Language Server Protocol's baseline position encoding and is the native indexing of the reference TypeScript implementation.
- Line/column positions are derived from offsets by consumers; they are never stored.
- Conformance AST comparison **ignores ``loc``**. Position correctness is verified by a separate, smaller test set.
- Segments the parser synthesises rather than reads carry the ``loc`` of the causing construct in the containing file, or no ``loc`` at all. A ``Fragment`` has a causing construct — its ``FileRef`` line — and takes that ``loc``. A padded table ``Cell`` ([§4.8](#s-4-8)) has no source range and no causing construct at a position, so it carries no ``loc``. A padded ``Cell`` is an ordinary empty ``Cell``, not a distinct node type; it is not listed under Synthetic Segments.
- Diagnostics (CDN codes) carry a ``loc`` identifying the triggering source range.

=== 14.7 Reflection Type

```typescript
interface Reflection {
  loc: Loc       // source range of the ## payload (raw-file UTF-16 offsets, see Location Type)
  text: string   // ## payload with one leading space stripped, trailing whitespace preserved
}
```

``reflection`` is typed ``Reflection[] | null``, and is ``null`` when no ``##`` comment is present on or adjacent to the block. See [§2.2](#s-2-2) for attachment rules.

=== 14.8 Attributes Type

```typescript
type Attribute =
  | { key: "id",    value: string }
  | { key: "class", value: string[] }
  | { key: string,  value: string }   // value: "" for bare-key tokens
```

``attributes`` is typed ``Attribute[]``. A block or inline node with no attributes carries an empty array, never ``null``. Nodes that admit no attributes at all (``CommentBlock``, ``Meta``, ``Column``, ``Cell``) omit the field entirely.

Ordering: entries appear in **source order**. Deduplication rules (see [§6.1.1](#s-6-1-1)) may drop entries before the array is emitted.

== 15. Name and Compliance

The name "Cutdown" is reserved for implementations that fully comply with the official specification.

Modified or extended versions of this specification must not use the name "Cutdown" without clear qualification (e.g., "Cutdown-derived", "Cutdown-compatible").

== 16. Streaming Conformance Profile

=== 16.1 Scope

The streaming profile is mandatory semantic conformance for every Cutdown parser. It does **not** require a streaming parser API, a renderer, or a transport protocol.

A producer may expose source one Unicode scalar value at a time. After each value, the source available so far is an **input snapshot** ([§1](#s-1)) and MUST parse according to the ordinary Cutdown grammar. Parsing that snapshot incrementally MUST produce exactly the AST and diagnostics produced by parsing it afresh.

=== 16.2 Input boundary

The profile begins after UTF-8 decoding ([§7](#s-7)). A decoder buffers an incomplete multi-byte sequence until it can emit a Unicode scalar value; malformed transport bytes are not Cutdown input. The scalar boundary does not change ``loc``: source locations remain UTF-16 code-unit offsets.

A producer MAY batch scalar values for transport, storage, or implementation. Batching does not change the result required at any scalar prefix.

=== 16.3 Snapshot and EOF semantics

Every snapshot is an ordinary valid Cutdown document. End of input is the virtual line terminator described in [§7](#s-7). It may mean a saved file, the source currently available from an open producer, cancellation, or normal completion.

At end of input, existing rules apply without repair:

- unresolved inline openers degrade according to [§9.4.1](#s-9-4-1);
- an unclosed fence or container consumes its current body and emits its existing diagnostic;
- no closer, marker, whitespace, normalization, or other **source** is synthesized. This constrains source text only; synthesising AST nodes where the rules require them — a padded table ``Cell`` ([§4.8](#s-4-8)) — is not source synthesis and is unaffected.

A later scalar may complete an unresolved construct and therefore reinterpret its unresolved suffix. It may also close an open block/container or alter derived section, page, and reference-resolution structure. This is ordinary reparsing of a later snapshot, not a mutation encoded in Cutdown source.

=== 16.4 Consumers and end of block

Cutdown has no canonical rendering. A consumer that has the complete source snapshot MAY render it immediately. A streaming consumer MAY defer semantic rendering of incomplete content until an **end of block** inferred from ordinary syntax: a completed block boundary, a closing fence, or end of input.

An end of block is not a source token, a chunk boundary, or an external stream event. Deferred rendering MUST NOT change the AST or diagnostics required for any input snapshot.

A table is a worked example. Its column count is fixed by the first content row ([§4.8](#s-4-8), __Table shape__), so ``Table.columns`` is settled the moment that row closes and is never revised as later rows arrive — it is **monotonic**. A consumer may therefore commit to a column count early. Under a ``max()`` rule it could not: a later wider row would retroactively change ``columns``, so consecutive snapshots could legitimately disagree.

=== 16.5 Exclusions

This profile defines no chunk envelope, completion event, retry, ordering, replacement, deletion, collaboration, CRDT identity, synchronization state, producer identity, or persistent provenance. Those concerns belong to the producer and consumer integration.

=== 16.6 Evidence

A conforming implementation MUST run every Unicode-scalar prefix of each fixture in ``tests/016-streaming-conformance-profile/``, including the empty and full prefixes. For every prefix it MUST demonstrate:

1. parsing terminates with an ordinary ``Document`` result;
2. repeated parsing is deterministic in AST and diagnostics; and
3. every listed checkpoint matches the fixture’s expected AST/pages and diagnostics.

The fixture schema is defined in ``tests/README.md``.

== 17. Canonical Form

The grammar accepts more than one spelling for the same meaning at a small number of points. Canonical Cutdown is the subset that uses exactly one of them.

This section binds tools that **write** source — formatters, round-trip editors, generators. Two conforming writers given the same AST produce byte-identical source. It places no obligation on a parser: every non-canonical spelling in the table below remains valid input and MUST continue to parse to the same AST.

Canonical form is not a compliance level ([§15](#s-15)) and carries no diagnostic. A tool MAY report a non-canonical spelling as a lint; a parser MUST NOT.

The serialization of a parse __result__ is a different subject, governed by [``policies/canonical-serialization-policy.md``](/latest/policies/canonical-serialization-policy).

=== 17.1 Registry

The table is closed. Every point at which two distinct inputs produce the same AST appears here. A spelling that carries a different AST value is not an alias and does not appear — ``:---`` and ``----`` differ (``"left"`` versus ``"start"``, [§4.8](#s-4-8)), so neither is canonical for the other.

| # | Point | Canonical | Also valid as input | Defined in |
|---|---|---|---|---|
| 1 | ``Meta`` format tag | lowercase — ``yaml``, ``toml``, ``json`` | any case | [§4.3](#s-4-3) |
| 2 | Nested quote marker | ``> >`` | ``>>`` | [§4.6](#s-4-6) |
| 3 | Ordered list item numbers | consecutive ascending integers from ``List.start`` | any numbers after the first | [§4.7](#s-4-7) |
| 4 | ``TaskItem`` unchecked marker | ``[ ]`` | — | [§4.7.2](#s-4-7-2) |
| 5 | ``TaskItem`` checked marker | ``[+]`` | ``[x]``, ``[X]`` | [§4.7.2](#s-4-7-2) |
| 6 | Trailing ``|`` on a table row | omitted, where omitting it does not move the attribute scope chain | present | [§4.8](#s-4-8) |
| 7 | A table with no rows and no columns | ``|`` | a header-separator-only table | [§4.8](#s-4-8) |
| 8 | Trailing ``{attrs}`` placement | on the block's own line | on following attribute-continuation lines | [§6.1](#s-6-1) |
| 9 | Block-opener escape placement | backslash before the marker — ``\\`\`\``` | `` \`\`` ``, `` ``\\` `` and the equivalents for every other marker | [§8.2](#s-8-2) |
| 10 | Blank lines at the start and end of a document | none | any number | [§12.1](#s-12-1) |
| 11 | Padding inside inline delimiters | none — ``**bb**`` | ``** bb **``, ``**  bb  **`` | [§12.2](#s-12-2) |

=== 17.2 Notes on individual rows

**Row 2 — why the spaced form.** One space after each ``>`` keeps the marker run readable at depth and matches the single space that separates the innermost ``>`` from the content: depth three is ``> > > text``, not ``>>> text``. Both spellings carry the same depth ([§4.6](#s-4-6)).

**Row 5 — why ``[+]``.** The letter ``x`` is a strong left-to-right character, so ``[x]`` opens a directional run inside the brackets and renders incorrectly in right-to-left source. ``[+]`` inherits the surrounding direction and is intact in any script. ``[x]`` and ``[X]`` stay valid input because they are what a Markdown document pasted into Cutdown carries.

**Row 5 — preservation is not available.** ``TaskItem.checked`` is a boolean and carries no spelling. A writer cannot reproduce the input marker from the AST, so it MUST emit the canonical one.

**Row 6 — not always an alias.** A trailing ``|`` after the last cell changes which slot a following ``{attrs}`` fills ([§4.8](#s-4-8)). Where it does, it is load-bearing and the row does not apply.

**Row 7 — a writer must still pick one.** ``Table { rows: [], columns: [] }`` is produced both by a lone ``|`` and by a table whose only line is a header separator. The separator carries alignment for columns that do not exist, so the shorter form is canonical.

=== 17.3 What is not in the registry

Three classes of input variation look like aliases and are not:

- **Recovery output.** An unclosed fence produces the same AST as a closed one plus a diagnostic (CDN-0001 … CDN-0005). The diagnostic is the difference; there is nothing to canonicalize.
- **Diagnosed surplus.** ``-----`` parses as a PageBreaker with its tail dropped and CDN-0016 emitted ([§4.10](#s-4-10)). ``---`` is the only undiagnosed spelling, so it is the only one a writer can emit.
- **Layout.** Line width, blank-line counts, table column padding, and indentation width are a tool's style settings. They are outside this section and outside the spec.
