Changelog

Per-release notes live in changelogs/. Change classes are defined in policies/versioning-policy.md.

Notes are written per minor line; patch releases are folded into the minor release above them.

0.10.0

[BREAKING] Span renamed to Mark

Before:  ::a    →  Span { name: "a", … }
After:   ::a::  →  Mark { name: "a", … }

[BREAKING] Mark takes inline content and nests

Span was a leaf with no closer and empty children. Mark takes a closer and real children, with ::name:: retained as the empty form.

Before:  ::a b::   →  Span { name: "a", children: [] } + Text(" b::")
After:   ::a b::   →  Mark { name: "a", children: [Text("b")] }

Mark is the only inline construct permitting same-type nesting, including of the same name — it matches by counting rather than by first closer.

[BREAKING] Multiline tables removed; Table.kind removed

Tables are pipe-only; Cell.children is Inline[].

Before:  +----------+  →  Table { kind: "multiline", … }
After:   +----------+  →  Paragraph([Text("+----------+")])

[BREAKING] + is no longer a special character

[BREAKING] TextBreak renamed to LineBreak

Before:  word\<EOL>  →  TextBreak
After:   word\<EOL>  →  LineBreak

[BREAKING] QuoteBlock.attribution renamed to caption

Before:  > quoted / ^ Source  →  QuoteBlock { attribution: [...] }
After:   > quoted / ^ Source  →  QuoteBlock { caption: [...] }

[BREAKING] QuoteBlock supports lazy continuation

A line without > continues the quote and follows its current child block's continuation rules. The quote ends at a blank line, container end, or end of input.

Before:  > First line   →  QuoteBlock { Paragraph("First line") }
         Continued         Paragraph("Continued")
After:   > First line   →  QuoteBlock { Paragraph("First lineContinued") }
         Continued

The QuoteBlock classification pattern changes from ^> to ^>, admitting >text and >>nested.

[BREAKING] Meta pagination is slot-based, not positional

A Meta fills the current Page's meta slot when empty, and opens a new Page only when it is already set. Content before frontmatter no longer creates a Page.

Before:  Para / --- / ~~~meta~~~  →  Page, GhostPage, Page{meta}
After:                            →  Page, Page{meta}

Before:  Para / ~~~meta~~~        →  Page{Para}, Page{meta}
After:                            →  Page{meta, Para}

[BREAKING] PageBreak renamed to PageBreaker

Terminology only — the construct produces no AST node, so no type, field, or diagnostic code changes.

[BREAKING] Default column alignment is "start", not "left"

Column.align gains "start" and defaults to it. ---- and a missing separator cell yield "start", which follows the text direction; :--- keeps "left", an explicit left edge in either direction.

[BREAKING] - [] is no longer a task marker

The unchecked marker is [ ]. - [] text is an ordinary ListItem whose content begins [].

[BREAKING] ### closes like every other fence

The CommentBlock closer no longer has to match the opener's column; any line whose stripped content is ### closes the block. Escape a ### line to keep it inside the body.

Before:  ###  /  note  /    ###   →  CommentBlock still open
After:                            →  CommentBlock { text: "note\n" }

[ADDITIVE] CDN-0031 — Mark nesting depth exceeded

Maximum Mark depth is 8. An opener at depth 9 emits CDN-0031 (warning).

[ADDITIVE] §17 Canonical Form

A closed registry of the eleven points where the grammar accepts more than one spelling, and the one a writer emits for each. Binds writers only; every listed alias remains valid input. Replaces policies/canonical-source-policy.md, which is deleted. New fixture directory tests/017-canonical-form/ asserts canonical in place of ast.

[CLARIFY] A fixture asserted RefDefinition.id

The field is ref; ref-definition-basic asserted id and no other fixture covered it.

[CLARIFY] Heading content takes paragraph continuation lines

The rule was unwritten: the lines following a heading continue its inline content until a blank line, an attribute line, a standalone ##, or a caption line.

[CLARIFY] Inline union was missing Spoiler and Variable

[CLARIFY] The inline-context table was missing Spoiler

[CLARIFY] Two conformance fixtures did not satisfy the corpus contract

Fixture Was Now
empty-document.yaml id: document-empty id: empty-document
comment-inline-in-meta-literal.yaml both ast: [] and pages: pages: only

[CLARIFY] No block opens inside a paragraph

A fence opener inside a paragraph run is paragraph content, not a CodeBlock, with no diagnostic; caption (^ ) and attribute-continuation ({…}) lines are not block openers and still attach to the preceding block.

[CLARIFY] Caption binding takes three conditions

The preceding block must be captionable, its caption slot empty, and no blank line between; any number of {attrs} and ## comment lines may intervene. Anything else orphans to a Paragraph with CDN-0008.

[CLARIFY] A run of non-blank lines may produce several blocks

A single-line block consumes only its own line and returns the remainder to classification, so = Title followed directly by content yields a Section and a Paragraph.

[CLARIFY] Trailing spaces drop at the end of a single-line block

Heading lines, table cells, and caption lines are block boundaries for the trailing-space rule.

[CLARIFY] A cell chain needs an attr-bearing inline

| AA | CC {.a} | drops {.a} with CDN-0011 — Cell bears no attributes and plain text offers no slot.

[CLARIFY] AST field and enumeration drift

Node Was Now
Page meta: Meta | {} meta: Meta | null
List carried caption field removed
FileRef no caption field caption: Inline[] | null
CommentBlock no reflection field reflection: Reflection[] | null
all nodes attributes: Attribute[] | null attributes: Attribute[], empty when absent
Block union omitted SpoilerBlock, CommentBlock both added; RefDefinition confirmed a Block

[CLARIFY] Editorial pass over prose and references

Duplicate definitions reduced to pointers (PageBreaker, the pagination and sectionization folds, Mark matching, the fence list, whitespace handling). Character-naming convention restated with one canonical form. Block Scope defined. Broken and imprecise cross-references corrected.

[CLARIFY] SYNTAX.md and README.md corrections

Was Now
+ listed as a special character removed
"Same-type nesting not allowed" scoped to doubled delimiters; Mark nests
":: has no closer" stale Span-era text, corrected
"GFM pipe tables parse unchanged" replaced with the three actual differences
node FileRefBlock; Page.children: (Section | Block)[] FileRef; Block[]
degradation described as one rule both classes described
"A single character is always plain text — no exceptions" scoped to inline position
"A Meta block is legal anywhere" page scope only

0.9.0

[BREAKING] Trailing whitespace collapses instead of being discarded

spec/012-whitespace-rules.md said trailing spaces are "Ignored, except: a single trailing space immediately before a soft break is preserved", while §5.13's line-ending summary said "word \n — Trailing space collapsed to single spaceText("word ") emitted". Two or more trailing spaces could not be both ignored and collapsed.

Resolved in favour of collapse. Trailing spaces collapse to a single space, preserved before a soft break and dropped at a block boundary.

Before:  "a  \nb\n"  →  Paragraph([Text("ab")])     (per §12: two spaces ignored)
After:   "a  \nb\n"  →  Paragraph([Text("a b")])

A parser that followed §5.13 already behaved the new way; a parser that followed §12 did not.

The ## comment payload is opaque, so trailing whitespace inside the payload is preserved verbatim and this rule does not reach inside it — previously unspecified.

[BREAKING] ImageBlock falls back to Paragraph when it is not the only segment on its line

§9.3 classified a line matching ^!\[ as an ImageBlock; §4.9 required an ImageBlock to be "the only one segment on the line". What happened when both applied was unspecified.

Phase 3 classification is now provisional. A line carrying content after the image falls back to a Paragraph containing an ImageInline followed by the remaining inline content. No content is dropped.

Before:  "![a](b) trailing text"  →  unspecified (parsers may have emitted ImageBlock)
After:   "![a](b) trailing text"  →  Paragraph([ImageInline(...), Text(" trailing text")])

Paragraph is not captionable, so a ^ line following the fallback is orphaned: it becomes a Paragraph of its own and emits CDN-0008. One stray word after an image demotes the image and converts its caption to prose.

[BREAKING] Table attr chains reach the last cell's inline

§6's scope-chain rule (Rule B) runs a trailing {} sequence down the containment hierarchy, ending at the last attr-bearing inline. §6's slot table truncated the two table contexts short of that inline slot, and §4.8 restated the truncated version a third time.

Cell bears no attributes, so the chain now walks past it into the cell.

Before:  "| AA | **BB** {.a}{.b}{.c}"  →  Table({.c}, Row({.b}, ...))   {.a}, {.b} dropped
After:   "| AA | **BB** {.a}{.b}{.c}"  →  Table({.c}, Row({.b}, Cell(...), Cell(Strong({.a}, "BB"))))

The trailing | selects the chain depth. Omitted, the last cell's inline context is open and the inline slot exists; written, the cell is sealed and the chain stops at Row. The slot searches the last cell only — it never scans sideways into an earlier cell.

Multiline tables are unchanged and stop at Row: a multiline cell holds Block[], so there is no inline context to bind to.

[BREAKING] CDN-0011 fires on orphaned scope-chain attributes

§6.1.3 said an excess {} at the front of a full chain is "silently dropped (no AST output)". CDN-0011 — Orphaned scope-chain attributes defines a warning for exactly that event. The code was therefore dead: no input could emit it, and cdn-0011-orphaned-scope-chain.yaml asserted so in its own description — "no warning (CDN-0011 should not emitted)".

Resolved in favour of the registry. The drop is unchanged; it is now reported.

Before:  "- text {.a}{.b}{.c}"  →  diagnostics: []
After:   "- text {.a}{.b}{.c}"  →  diagnostics: [CDN-0011]

One diagnostic per chain, however many blocks are orphaned. An empty {} still claims its slot, so it is never an orphan.

[BREAKING] Attributes written before a table row's closer bind inside the cell

| AA | **BB** {.a} | lost {.a} entirely and silently. The chain was extracted at line level, which sees the | at the end and finds nothing; the groups were then re-parsed inside the cell and discarded. The same loss applied to any non-final cell — | **AA** {.x} | BB |.

Every cell is an inline context, so Rule B (§6) now applies inside it. Cell bears no attributes, so the cell chain has exactly one slot: that cell's last attr-bearing inline.

Before:  "| AA | **BB** {.a} |"  →  Cell(Strong("BB"))                 {.a} lost
After:   "| AA | **BB** {.a} |"  →  Cell(Strong({.a}, "BB"))

The closer now has a consistent meaning in both directions: it sets how deep the row chain reaches, and a chain's position relative to it selects which chain applies. The three spellings stay distinct —

| AA | **BB** {.a}      →  Table({.a})    row chain, 3 slots, last {} → Table
| AA | **BB** | {.a}    →  Table({.a})    row chain, 2 slots
| AA | **BB** {.a} |    →  Strong({.a})   cell chain, 1 slot

A second group on a cell chain orphans and emits CDN-0011.

[BREAKING] Table column count is fixed by the first content row

spec/004-block-segments.md set the multiline column count to "max() of cell count across all logical rows" with "no diagnostic ... for row/column count mismatches". The pipe section stated no rule at all, yet behaved the same way, so max() was normative for one kind and silent folklore for the other.

Both kinds now take the column count from the first content row.

Before:  "| A | B |\n| C | D | E |"  →  Table(columns: 3), rows of 2 and 3 cells
After:   "| A | B |\n| C | D | E |"  →  Table(columns: 2), "E" dropped + CDN-0018

Before:  "| A | B |\n| C |"          →  Table(columns: 2), rows of 2 and 1 cells
After:   "| A | B |\n| C |"          →  Table(columns: 2), row 2 padded to 2 cells

Short rows are padded with empty cells and emit nothing — nothing is lost. Wide rows drop their surplus and emit CDN-0018, on the CDN-0016 precedent that dropping author content is diagnosable, not silent. A header separator is never a content row, so it never defines the count; wider, it drops surplus with the same diagnostic; narrower, its uncovered columns default to "left" as before.

Padded cells carry no loc. §14's synthetic-loc sentence, which named only Fragment, now covers both cases. A padded Cell is an ordinary empty Cell, not a new node type.

In multiline tables + rows no longer count columns or position boundaries — the first content row's | positions supply both. A +---+---+ drawn wider or narrower than its first content row is inert decoration and emits nothing.

All 16 existing table fixtures are unaffected: every one is rectangular, and every multiline + row already agrees with its first content row.

[ADDITIVE] Trailing | is optional on table rows

§4.8 required leading and trailing | on every pipe row while saying the trailing | was optional for multiline rows — the two kinds disagreed with no reason given. Resolved toward the multiline reading: the last | on a line is always the closer, never a cell separator.

| A | B |      →  2 cells   (unchanged)
| A | B        →  2 cells   (newly legal)
| A | B | |    →  3 cells, the third empty, then the closer

The leading | stays required — §9.3 classifies a pipe table by ^\|, so it is the detection anchor.

The reference parser (cutdown-parser 0.8.0, splitCells) already accepted the omitted closer and already produced these counts; the required-trailing-| sentence was simply wrong. No existing parse changes.

[ADDITIVE] Streaming Conformance Profile (§16)

Every Unicode-scalar prefix of a document is itself an ordinary Cutdown document and must parse to the same AST and diagnostics as parsing that prefix afresh. Adds no syntax and changes no existing parse; a parser that passed 0.8.0 may not pass this release.

[ADDITIVE] Bidi-neutral TaskItem marker - [+]

- [+] joins - [x] as a checked task marker.

[CLARIFY] Two worked examples corrected against the corpus

Six delimiters leave exactly one leftover — opener (2) + literal (1) + closer (2).

Location Was Now
§5.12 ^^^x^^^ → Spoiler([Text("^x")]) + Text("^^") + Text("^")
§5.6 ```text``` → CodeInline { value: "text" }` + Text("")`

[CLARIFY] §6 renumbered to Segment Attribution

Was Now
§6 Universal Attributes and Captions §6 Segment Attribution (Universal Attributes and Caption)
§6.1 Syntax §6.1.1
§6.2 Placement §6.1.2
§6.3 Orphan Attributes §6.1.3
§6.4 Attribute Inside Link Text §6.1.4
§6.5 Caption Line §6.2 Caption

External references to §6.2 (Placement) now resolve to Caption.

[CLARIFY] Loc is defined once, in §14

It was declared in §2.2 and re-declared anonymously in §14, which used the bare name it never defined. §14 now declares interface Loc; §2.2's copy is deleted.

[CLARIFY] "Left-to-right" replaced with source order

Nine occurrences. Every one described source order, not visual order — in an RTL document these diverge. 009:109, which walks a list of blocks, reads "document order".

[CLARIFY] TextBreak had three names

§5.13 said TextBreak, §12 said "Hard break", §13 said "hard break". All now use the AST type name.

[CLARIFY] Characters are named

§13 gained a Name column for all 24 characters. §1 states the convention and defines .

[CLARIFY] §10.5 list examples use kind

Three AST examples still wrote List { ordered: false }, a field renamed to kind in 0.3.6. §14 and the corpus have carried kind throughout.

[CLARIFY] policies/ambiguity-matrix.md dropped

A prose checklist of inline-precedence cases, written before the conformance corpus existed and never updated after it did. It still described ** as the Emphasis delimiter, inverted since 0.8.0, and referenced a mention parser that lives in the extensions list rather than the language.

conformance-policy.md and changes-gate.md kept their obligation to cover conflict, escape, malformed, and neighbouring-precedence cases for every new inline token — they now point at tests/, which is executable and versioned with the spec. Normative sources drop from 21 policies to 20.

[CLARIFY] Added Changelog

0.8.0

[BREAKING] ** and __ swapped

** now marks Strong and __ marks Emphasis, following the dominant Markdown-family convention. Every document using either delimiter parses to a different node type.

Before:  "**text**"  →  Emphasis([Text("text")])
After:   "**text**"  →  Strong([Text("text")])

[BREAKING] ThematicBreak segment removed

--- at top level still closes the current Page and opens a new one, but emits no segment. Inside a block container it no longer emits a segment either, and now emits CDN-0017. CDN-0010 is retired, superseded by CDN-0016 (PageBreak tail dropped).

[BREAKING] Table header separators moved from + rows to | rows

A header separator is now a | row whose every cell is an alignment pattern. + rows never mark headers, and colons inside them are inert.

Before:  "+:---+:---+"  →  header separator
After:   "+----+----+"  →  inert (multiline: row delimiter; pipe: ignored)
         "|:---|:---|"  →  header separator

In a multiline table a header separator also closes the logical row and defines column boundaries.

[BREAKING] Table.kind value "gfm" renamed to "pipe"

Syntax is unchanged — GFM pipe tables still parse as before — but the AST value differs.

[ADDITIVE] Loc

Segments carry a source location. Adds no syntax.

0.7.0

No syntax or AST change. License update; residual CommentInline references removed from the corpus following 0.6.0.

0.6.0

[BREAKING] ## no longer produces an AST segment

The CommentInline segment was removed. A ## payload is now stored as a Reflection entry on the nearest enclosing block; every block type gained reflection: Reflection[] | null.

Before:  "text ## note"  →  Paragraph([Text("text "), CommentInline("note")])
After:   "text ## note"  →  Paragraph([Text("text")], reflection: [{ text: "note" }])

A standalone ## line acts as a blank line for block-boundary purposes.

[ADDITIVE] Multiline tables

A second table variant opened by +- / +:, with cells containing block content rather than inline content. Cell.children becomes Block[] for this kind.

[ADDITIVE] Caption line

^ text following a captionable block attaches as its caption. An orphaned caption emits CDN-0008; attributes on a caption line emit CDN-0009.

0.5.0

[ADDITIVE] Comment syntax

Three-way split on #, following the doubled/tripled-delimiter rule:

Run Meaning
# literal
## line comment, runs to EOL
### CommentBlock fence

Both comment forms are hidden from rendering by default. Unclosed ### emits CDN-0006.

[BREAKING] Escaping strategy defined

spec/008-escaping.md fixed which characters are escapable and what a backslash before a non-escapable character produces. Input previously carrying a bare backslash can parse differently.

[BREAKING] Trimming strategy defined

Where leading and trailing whitespace is stripped was made explicit across block and inline contexts. Attribute blocks are stripped from paragraph tails and NamedBlock openers.

0.4.0

[ADDITIVE] Spoiler

^^text^^ inline and ^^^ fenced block. A single ^ stays literal. Unclosed ^^^ emits CDN-0005.

[BREAKING] Backtick escaping inside code segments

Escape handling within CodeInline and CodeBlock was redefined; a backtick sequence inside a code span can now produce a different value.

0.3.0

[BREAKING] List.ordered replaced by List.kind

ordered: bool became kind: "bullet" | "numbered" | "checklist", determined by the first item's marker. start is non-null only for "numbered".

A "checklist" list has children: TaskItem[] exclusively; "bullet" and "numbered" have children: ListItem[] exclusively. A numbered list meeting a task marker now closes and opens a new "checklist" list — previously the item stayed in place.

[BREAKING] AST field renames

Segment Was Now
CodeBlock content raw
MathBlock formula raw
FileRef, FileRefGroup src path

[BREAKING] QuoteBlock parsing rules

New rules for lazy continuation, nesting, and blank-line handling inside quotes. Existing quoted input can parse to a different block structure.

[ADDITIVE] Inline boundary crossing specified

Behaviour of an inline construct whose opener and closer fall in different containers was previously unspecified.

Also in this line: GRAMMAR.peg was deleted — the prose spec plus the YAML corpus are the sole source of truth. @mention is out of scope.

[CLARIFY] Meta and Variable dropped from the §1 union listings

Neither node was deleted. Meta (§4.3) and Variable (§5.11) keep their definitions and their §14 rows; only their entries in §1's Block and Inline union listings were removed.

Block  =  ... | Section | Meta | CodeBlock | ...      →  Meta dropped
Inline =  ... | Span | Variable | TextBreak           →  Variable dropped

No conforming parser changes behaviour: the unions in §1 are descriptive, and §14 remains the node contract. The two have disagreed ever since.

0.2.0

Baseline. The first published spec; nothing precedes it, so no entry is classifiable as breaking or additive.

Established: the document model, block and inline segment sets, universal attributes, the parsing algorithm, precedence and whitespace rules, the special character reference, and the AST node reference. Golden YAML fixtures became the conformance corpus.

Extensions were cut from the spec in this line; Mention moved to the extensions list rather than the core segment set.

[ADDITIVE] BIDI conformance

Rules for bidirectional text added to input interpretation.