From 0387343dd0753870f2fbea611af31f3457fa578e Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Wed, 26 Aug 2026 22:14:06 +0200 Subject: [PATCH] Corpus 2e1: the carve-out escapes, the claimed line, and the 2e split --- corpus/README.md | 2 +- .../combinations/carve-out-directive.json | 73 ++++++++++++++++ .../combinations/carve-out-directive.md | 11 +++ .../combinations/carve-out-pipe-table.json | 60 ++++++++++++++ .../combinations/carve-out-pipe-table.md | 9 ++ .../combinations/carve-out-strike.json | 69 +++++++++++++++ .../combinations/carve-out-strike.md | 9 ++ .../combinations/closing-fence-line.json | 83 +++++++++++++++++++ .../combinations/closing-fence-line.md | 13 +++ src/corpus.test.ts | 4 +- todo.md | 65 ++++++++------- 11 files changed, 365 insertions(+), 33 deletions(-) create mode 100644 corpus/round-trip/combinations/carve-out-directive.json create mode 100644 corpus/round-trip/combinations/carve-out-directive.md create mode 100644 corpus/round-trip/combinations/carve-out-pipe-table.json create mode 100644 corpus/round-trip/combinations/carve-out-pipe-table.md create mode 100644 corpus/round-trip/combinations/carve-out-strike.json create mode 100644 corpus/round-trip/combinations/carve-out-strike.md create mode 100644 corpus/round-trip/combinations/closing-fence-line.json create mode 100644 corpus/round-trip/combinations/closing-fence-line.md diff --git a/corpus/README.md b/corpus/README.md index 23ecb71..083985b 100644 --- a/corpus/README.md +++ b/corpus/README.md @@ -4,7 +4,7 @@ One directory per contract kind, each landing with its milestone: - `round-trip/` — `.json` + `.md`: the markdown `adfToMarkdown` must emit for that document, byte for byte, and that `markdownToAdf` must read back to it (AGENTS.md §2). Grouped - by node family. + by node family, `combinations/` holding what crosses them. - `normalization/` — `.md` + `.json`: markdown input, and the document `markdownToAdf` must build from it. One-way; the markdown is not canonical. - `errors/` — `.md`: markdown input that must not convert. A `.error` beside it diff --git a/corpus/round-trip/combinations/carve-out-directive.json b/corpus/round-trip/combinations/carve-out-directive.json new file mode 100644 index 0000000..55135a3 --- /dev/null +++ b/corpus/round-trip/combinations/carve-out-directive.json @@ -0,0 +1,73 @@ +{ + "content": [ + { + "content": [ + { + "text": "Write :mention[@Mikael]{id=5b10a2} to tag someone.", + "type": "text" + } + ], + "type": "paragraph" + }, + { + "content": [ + { + "text": "::syncBlock {localId=x}", + "type": "text" + } + ], + "type": "paragraph" + }, + { + "content": [ + { + "text": ":::panel warning", + "type": "text" + } + ], + "type": "paragraph" + }, + { + "content": [ + { + "text": "Stand-up at 10:30, shipped :tada:, {maxLevel} deep, a ::syncBlock mid-line.", + "type": "text" + } + ], + "type": "paragraph" + }, + { + "content": [ + { + "attrs": { + "text": "In review" + }, + "type": "status" + }, + { + "text": "{color} decides.", + "type": "text" + } + ], + "type": "paragraph" + }, + { + "content": [ + { + "text": ":status[nope]{color=red} vs ", + "type": "text" + }, + { + "attrs": { + "color": "red", + "text": "yep" + }, + "type": "status" + } + ], + "type": "paragraph" + } + ], + "type": "doc", + "version": 1 +} diff --git a/corpus/round-trip/combinations/carve-out-directive.md b/corpus/round-trip/combinations/carve-out-directive.md new file mode 100644 index 0000000..e3c46cf --- /dev/null +++ b/corpus/round-trip/combinations/carve-out-directive.md @@ -0,0 +1,11 @@ +Write \:mention[@Mikael]{id=5b10a2} to tag someone. + +\::syncBlock {localId=x} + +\:::panel warning + +Stand-up at 10:30, shipped :tada:, {maxLevel} deep, a ::syncBlock mid-line. + +:status[In review]\{color} decides. + +\:status[nope]{color=red} vs :status[yep]{color=red} diff --git a/corpus/round-trip/combinations/carve-out-pipe-table.json b/corpus/round-trip/combinations/carve-out-pipe-table.json new file mode 100644 index 0000000..7a69066 --- /dev/null +++ b/corpus/round-trip/combinations/carve-out-pipe-table.json @@ -0,0 +1,60 @@ +{ + "content": [ + { + "content": [ + { + "text": "| Part | Qty |", + "type": "text" + } + ], + "type": "paragraph" + }, + { + "content": [ + { + "text": "Pass a | b to the shell.", + "type": "text" + } + ], + "type": "paragraph" + }, + { + "content": [ + { + "content": [ + { + "content": [ + { + "text": "| --- | --- |", + "type": "text" + } + ], + "type": "paragraph" + } + ], + "type": "listItem" + } + ], + "type": "bulletList" + }, + { + "attrs": { + "panelType": "info" + }, + "content": [ + { + "content": [ + { + "text": "| a | b |", + "type": "text" + } + ], + "type": "paragraph" + } + ], + "type": "panel" + } + ], + "type": "doc", + "version": 1 +} diff --git a/corpus/round-trip/combinations/carve-out-pipe-table.md b/corpus/round-trip/combinations/carve-out-pipe-table.md new file mode 100644 index 0000000..1a49b8b --- /dev/null +++ b/corpus/round-trip/combinations/carve-out-pipe-table.md @@ -0,0 +1,9 @@ +\| Part | Qty | + +Pass a | b to the shell. + +- \| --- | --- | + +:::panel info +\| a | b | +::: diff --git a/corpus/round-trip/combinations/carve-out-strike.json b/corpus/round-trip/combinations/carve-out-strike.json new file mode 100644 index 0000000..adeb0dd --- /dev/null +++ b/corpus/round-trip/combinations/carve-out-strike.json @@ -0,0 +1,69 @@ +{ + "content": [ + { + "content": [ + { + "text": "~~not strike~~", + "type": "text" + } + ], + "type": "paragraph" + }, + { + "content": [ + { + "marks": [ + { + "type": "strike" + } + ], + "text": "gone", + "type": "text" + }, + { + "text": " but ~~kept~~", + "type": "text" + } + ], + "type": "paragraph" + }, + { + "content": [ + { + "text": "A ~ B ~ C", + "type": "text" + } + ], + "type": "paragraph" + }, + { + "content": [ + { + "text": "~", + "type": "text" + }, + { + "marks": [ + { + "type": "strike" + } + ], + "text": "a", + "type": "text" + } + ], + "type": "paragraph" + }, + { + "content": [ + { + "text": "~~~not~~~", + "type": "text" + } + ], + "type": "paragraph" + } + ], + "type": "doc", + "version": 1 +} diff --git a/corpus/round-trip/combinations/carve-out-strike.md b/corpus/round-trip/combinations/carve-out-strike.md new file mode 100644 index 0000000..a5765d7 --- /dev/null +++ b/corpus/round-trip/combinations/carve-out-strike.md @@ -0,0 +1,9 @@ +\~~not strike~~ + +~~gone~~ but \~~kept~~ + +A ~ B ~ C + +\~~~a~~ + +\~\~~not~~~ diff --git a/corpus/round-trip/combinations/closing-fence-line.json b/corpus/round-trip/combinations/closing-fence-line.json new file mode 100644 index 0000000..35f33ee --- /dev/null +++ b/corpus/round-trip/combinations/closing-fence-line.json @@ -0,0 +1,83 @@ +{ + "content": [ + { + "attrs": { + "panelType": "info" + }, + "content": [ + { + "content": [ + { + "text": ":::", + "type": "text" + } + ], + "type": "paragraph" + }, + { + "content": [ + { + "text": "::: x", + "type": "text" + } + ], + "type": "paragraph" + }, + { + "content": [ + { + "text": "::", + "type": "text" + } + ], + "type": "paragraph" + }, + { + "content": [ + { + "content": [ + { + "content": [ + { + "text": ":::", + "type": "text" + } + ], + "type": "paragraph" + } + ], + "type": "listItem" + } + ], + "type": "bulletList" + }, + { + "content": [ + { + "content": [ + { + "text": ":::", + "type": "text" + } + ], + "type": "paragraph" + } + ], + "type": "blockquote" + } + ], + "type": "panel" + }, + { + "content": [ + { + "text": ":::", + "type": "text" + } + ], + "type": "paragraph" + } + ], + "type": "doc", + "version": 1 +} diff --git a/corpus/round-trip/combinations/closing-fence-line.md b/corpus/round-trip/combinations/closing-fence-line.md new file mode 100644 index 0000000..18fbff8 --- /dev/null +++ b/corpus/round-trip/combinations/closing-fence-line.md @@ -0,0 +1,13 @@ +:::panel info +\::: + +\::: x + +\:: + +- \::: + +> \::: +::: + +\::: diff --git a/src/corpus.test.ts b/src/corpus.test.ts index 18c4206..9b8bf72 100644 --- a/src/corpus.test.ts +++ b/src/corpus.test.ts @@ -13,7 +13,7 @@ const corpusRoot = join(dirname(fileURLToPath(import.meta.url)), '..', 'corpus') const roundTripRoot = join(corpusRoot, 'round-trip') const unspellableRoot = join(corpusRoot, 'unspellable') -const emittingDirectories = ['block-nodes', 'commonmark-subset', 'inline-nodes', 'opaque-carry'] +const emittingDirectories = ['block-nodes', 'combinations', 'commonmark-subset', 'inline-nodes', 'opaque-carry'] function directoryNames(root: string): string[] { return readdirSync(root, { withFileTypes: true }) @@ -86,6 +86,7 @@ function fenceNestingFault(markdown: string): string | undefined { const run = colons[1]?.length ?? 0 const enclosing = open[open.length - 1] if (colons[2] === '') { + if (enclosing !== undefined && run < enclosing) return `${JSON.stringify(line)} is shorter than the ${enclosing} colons it would close` open.pop() continue } @@ -99,6 +100,7 @@ test('the fence nesting check catches a fence a container cannot hold', () => { assert.equal(fenceNestingFault(':::panel info\n- :::panel warning\n B\n :::\n:::'), '"- :::panel warning" sits in a container fenced with 3 colons') assert.equal(fenceNestingFault(':::panel info\n- - :::panel warning\n B\n :::\n:::'), '"- - :::panel warning" sits in a container fenced with 3 colons') assert.equal(fenceNestingFault(':::panel info\n10. :::panel warning\n B\n :::\n:::'), '"10. :::panel warning" sits in a container fenced with 3 colons') + assert.equal(fenceNestingFault('::::panel info\n:::\n::::'), '":::" is shorter than the 4 colons it would close') assert.equal(fenceNestingFault('::::panel info\n- - :::panel warning\n B\n :::\n::::'), undefined) assert.equal(fenceNestingFault(':::tableCell\n```text\n:::::::panel warning\n:::\n```\n:::'), undefined) }) diff --git a/todo.md b/todo.md index f3a0a48..51abf10 100644 --- a/todo.md +++ b/todo.md @@ -84,37 +84,40 @@ detail is settled at its own milestone. attribute fallback 2e owes, since the reservation leaves that node no other spelling whatever 1d decides for its `localId`. - [ ] **2e — Carve-outs and combinations.** Fixtures and emitter together, into - `corpus/round-trip/combinations/`: the three carve-outs and their escapes, mark runs — the - longest-run rule, attributes included — and the runs a carry breaks, a mark spelling that - cannot open where it sits (`un**-real**istic`; the spec owes the carry a trigger), - attribute canonicalization, a pipe cell's whitespace edges and `\u007c` for a `|` inside a - quoted attribute value, documents combining nodes rather than isolating one, and a - paragraph line inside a container body shaped like a closing fence (`:::`, `::: x`). - Guard `fenceNestingFault`'s bare-run pop here too — a run shorter than the open fence is a - fault, not a close — which today's emitter cannot reach. - Two moves land before the attribute spelling changes. One mark vocabulary: - `emphasisSpellings`, `linkAttributes` and the `code`/`link` names join - `inline-directives.ts`, which holds four of the nine marks while the rest are branch - literals in the emitter — and the parser (3) needs every name to make `:em[x]` the named - error `spec/flavour.md` promises. And `escaping: 'attribute'` earns its keep at the - `\u007c` rule or collapses into `none`: nothing the escaper does tells the two apart - today, since a carried segment holds only spaces, tabs and newlines. - The carry's fallback triggers land here too: `spec/flavour.md` carries a node its section - cannot spell — an attrs key no section lists, a value that is not the section's type, an - arg slot holding no bare token, marks no nesting spells — where the emitter still refuses, - which leaves the refusals a container's own spelling owns. The `\u007c` rule is wider than - the pipe: a quoted attribute value spells `` ` ``, `&` and `<` raw today, so a backtick - pair in an `inlineCard`'s `data`, a `status`'s `style` or the inline carry's `json` reads - back as a code span, an entity or raw HTML inside the value. One escape settles all four, - and the pick decides whether a directive or a code span wins where they overlap (3). The - carry spells `escaping: 'none'` meanwhile, which is what makes `tryPipeCell` refuse the - pipe form for a carry whose JSON holds a pipe. - The gate gains the collision property here: no two corpus documents may emit the same - bytes — one spelling for two documents is a round-trip break no parser can undo, and it is - provable without one. It also settles the emitter's one known approximation: delimiter - flanking is exact, but CommonMark's *matching* — the multiple-of-3 rule and the way a run - splits across several openers — is not modelled. No reachable violation has been found by - hand; the property test is what decides it. + `corpus/round-trip/combinations/` — the documents that cross what 2a–2d isolated. + - [ ] **2e1 — The carve-outs and the claimed line.** The directory opens: the three carve-outs + and their escapes, and a paragraph line inside a container body shaped like a closing + fence (`:::`, `::: x`). Guard `fenceNestingFault`'s bare-run pop here too — a run shorter + than the open fence is a fault, not a close — which today's emitter cannot reach. + - [ ] **2e2 — Mark runs and the runs a carry breaks.** The longest-run rule, attributes + included, and a mark spelling that cannot open where it sits (`un**-real**istic`; the spec + owes the carry a trigger). One mark vocabulary lands here, before 2e3 changes the + attribute spelling: `emphasisSpellings`, `linkAttributes` and the `code`/`link` names join + `inline-directives.ts`, which holds four of the nine marks while the rest are branch + literals in the emitter — and the parser (3) needs every name to make `:em[x]` the named + error `spec/flavour.md` promises. + - [ ] **2e3 — Attribute canonicalization and the quoted value's escape.** Attribute + canonicalization, a pipe cell's whitespace edges, and `\u007c` for a `|` inside a quoted + attribute value. The rule is wider than the pipe: a quoted value spells `` ` ``, `&` and + `<` raw today, so a backtick pair in an `inlineCard`'s `data`, a `status`'s `style` or the + inline carry's `json` reads back as a code span, an entity or raw HTML inside the value. + One escape settles all four, and the pick decides whether a directive or a code span wins + where they overlap (3). `escaping: 'attribute'` earns its keep at that rule or collapses + into `none`: nothing the escaper does tells the two apart today, since a carried segment + holds only spaces, tabs and newlines. + - [ ] **2e4 — The carry's fallback triggers.** `spec/flavour.md` carries a node its section + cannot spell — an attrs key no section lists, a value that is not the section's type, an + arg slot holding no bare token, marks no nesting spells — where the emitter still refuses, + which leaves the refusals a container's own spelling owns. The carry spells + `escaping: 'none'`, which is what makes `tryPipeCell` refuse the pipe form for a carry + whose JSON holds a pipe. + - [ ] **2e5 — Combined documents and the collision property.** Documents combining nodes rather + than isolating one, and the gate's collision property: no two corpus documents may emit + the same bytes — one spelling for two documents is a round-trip break no parser can undo, + and it is provable without one. It also settles the emitter's one known approximation: + delimiter flanking is exact, but CommonMark's *matching* — the multiple-of-3 rule and the + way a run splits across several openers — is not modelled. No reachable violation has been + found by hand; the property test is what decides it. - [ ] **2f — The attributes CommonMark cannot hold.** 1d's settled answer: the block nodes CommonMark spells — `blockquote`, `bulletList`, `codeBlock`, `heading`, `listItem`, `orderedList`, `paragraph`, `rule` — get directive sections in `spec/flavour.md` carrying