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 space — Text("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: " trailing text" → unspecified (parsers may have emitted ImageBlock)
After: " 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.