From 7195cd9f49704d67c60f022287f5755e5b074b66 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 24 Aug 2026 11:21:57 +0200 Subject: [PATCH 1/4] Corpus 1d1: the canonical-form pairs, and the 1d split --- corpus/README.md | 8 ++ corpus/canonical-form/blockquote.json | 29 +++++ corpus/canonical-form/blockquote.md | 3 + corpus/canonical-form/bullet-list.json | 72 +++++++++++ corpus/canonical-form/bullet-list.md | 4 + .../code-block-backtick-run.json | 15 +++ .../canonical-form/code-block-backtick-run.md | 5 + corpus/canonical-form/code-block.json | 18 +++ corpus/canonical-form/code-block.md | 5 + corpus/canonical-form/code-span.json | 118 ++++++++++++++++++ corpus/canonical-form/code-span.md | 11 ++ corpus/canonical-form/em-strong-strike.json | 76 +++++++++++ corpus/canonical-form/em-strong-strike.md | 3 + corpus/canonical-form/hard-break.json | 53 ++++++++ corpus/canonical-form/hard-break.md | 6 + corpus/canonical-form/heading.json | 78 ++++++++++++ corpus/canonical-form/heading.md | 11 ++ corpus/canonical-form/link.json | 116 +++++++++++++++++ corpus/canonical-form/link.md | 9 ++ corpus/canonical-form/ordered-list-order.json | 65 ++++++++++ corpus/canonical-form/ordered-list-order.md | 5 + corpus/canonical-form/ordered-list.json | 53 ++++++++ corpus/canonical-form/ordered-list.md | 3 + corpus/canonical-form/paragraph-empty.json | 27 ++++ corpus/canonical-form/paragraph-empty.md | 5 + corpus/canonical-form/paragraph.json | 24 ++++ corpus/canonical-form/paragraph.md | 3 + corpus/canonical-form/rule.json | 27 ++++ corpus/canonical-form/rule.md | 5 + corpus/canonical-form/text-escaping.json | 42 +++++++ corpus/canonical-form/text-escaping.md | 7 ++ todo.md | 20 ++- 32 files changed, 925 insertions(+), 1 deletion(-) create mode 100644 corpus/README.md create mode 100644 corpus/canonical-form/blockquote.json create mode 100644 corpus/canonical-form/blockquote.md create mode 100644 corpus/canonical-form/bullet-list.json create mode 100644 corpus/canonical-form/bullet-list.md create mode 100644 corpus/canonical-form/code-block-backtick-run.json create mode 100644 corpus/canonical-form/code-block-backtick-run.md create mode 100644 corpus/canonical-form/code-block.json create mode 100644 corpus/canonical-form/code-block.md create mode 100644 corpus/canonical-form/code-span.json create mode 100644 corpus/canonical-form/code-span.md create mode 100644 corpus/canonical-form/em-strong-strike.json create mode 100644 corpus/canonical-form/em-strong-strike.md create mode 100644 corpus/canonical-form/hard-break.json create mode 100644 corpus/canonical-form/hard-break.md create mode 100644 corpus/canonical-form/heading.json create mode 100644 corpus/canonical-form/heading.md create mode 100644 corpus/canonical-form/link.json create mode 100644 corpus/canonical-form/link.md create mode 100644 corpus/canonical-form/ordered-list-order.json create mode 100644 corpus/canonical-form/ordered-list-order.md create mode 100644 corpus/canonical-form/ordered-list.json create mode 100644 corpus/canonical-form/ordered-list.md create mode 100644 corpus/canonical-form/paragraph-empty.json create mode 100644 corpus/canonical-form/paragraph-empty.md create mode 100644 corpus/canonical-form/paragraph.json create mode 100644 corpus/canonical-form/paragraph.md create mode 100644 corpus/canonical-form/rule.json create mode 100644 corpus/canonical-form/rule.md create mode 100644 corpus/canonical-form/text-escaping.json create mode 100644 corpus/canonical-form/text-escaping.md diff --git a/corpus/README.md b/corpus/README.md new file mode 100644 index 0000000..4122780 --- /dev/null +++ b/corpus/README.md @@ -0,0 +1,8 @@ +# The corpus + +`.json` is an ADF document; `.md` is the markdown `adfToMarkdown` must emit for it, +byte for byte including the trailing newline, and that `markdownToAdf` must read back to that same +document (AGENTS.md §2). Directories mirror `spec/flavour.md`'s sections. + +The JSON is editor-normal — empty `attrs`, `marks` and `content` as the absent key, adjacent +identical-mark text nodes merged — two-space indent, keys sorted. diff --git a/corpus/canonical-form/blockquote.json b/corpus/canonical-form/blockquote.json new file mode 100644 index 0000000..145989c --- /dev/null +++ b/corpus/canonical-form/blockquote.json @@ -0,0 +1,29 @@ +{ + "content": [ + { + "content": [ + { + "content": [ + { + "text": "Ship it.", + "type": "text" + } + ], + "type": "paragraph" + }, + { + "content": [ + { + "text": "Then tell them.", + "type": "text" + } + ], + "type": "paragraph" + } + ], + "type": "blockquote" + } + ], + "type": "doc", + "version": 1 +} diff --git a/corpus/canonical-form/blockquote.md b/corpus/canonical-form/blockquote.md new file mode 100644 index 0000000..f46663f --- /dev/null +++ b/corpus/canonical-form/blockquote.md @@ -0,0 +1,3 @@ +> Ship it. +> +> Then tell them. diff --git a/corpus/canonical-form/bullet-list.json b/corpus/canonical-form/bullet-list.json new file mode 100644 index 0000000..17d1b65 --- /dev/null +++ b/corpus/canonical-form/bullet-list.json @@ -0,0 +1,72 @@ +{ + "content": [ + { + "content": [ + { + "content": [ + { + "content": [ + { + "text": "Bolt M8", + "type": "text" + } + ], + "type": "paragraph" + } + ], + "type": "listItem" + }, + { + "content": [ + { + "content": [ + { + "text": "Nut M8", + "type": "text" + } + ], + "type": "paragraph" + } + ], + "type": "listItem" + }, + { + "content": [ + { + "content": [ + { + "text": "Washer M8", + "type": "text" + } + ], + "type": "paragraph" + }, + { + "content": [ + { + "content": [ + { + "content": [ + { + "text": "Fibre", + "type": "text" + } + ], + "type": "paragraph" + } + ], + "type": "listItem" + } + ], + "type": "bulletList" + } + ], + "type": "listItem" + } + ], + "type": "bulletList" + } + ], + "type": "doc", + "version": 1 +} diff --git a/corpus/canonical-form/bullet-list.md b/corpus/canonical-form/bullet-list.md new file mode 100644 index 0000000..51738d8 --- /dev/null +++ b/corpus/canonical-form/bullet-list.md @@ -0,0 +1,4 @@ +- Bolt M8 +- Nut M8 +- Washer M8 + - Fibre diff --git a/corpus/canonical-form/code-block-backtick-run.json b/corpus/canonical-form/code-block-backtick-run.json new file mode 100644 index 0000000..012ffd4 --- /dev/null +++ b/corpus/canonical-form/code-block-backtick-run.json @@ -0,0 +1,15 @@ +{ + "content": [ + { + "content": [ + { + "text": "```\ncode\n```", + "type": "text" + } + ], + "type": "codeBlock" + } + ], + "type": "doc", + "version": 1 +} diff --git a/corpus/canonical-form/code-block-backtick-run.md b/corpus/canonical-form/code-block-backtick-run.md new file mode 100644 index 0000000..436503d --- /dev/null +++ b/corpus/canonical-form/code-block-backtick-run.md @@ -0,0 +1,5 @@ +```` +``` +code +``` +```` diff --git a/corpus/canonical-form/code-block.json b/corpus/canonical-form/code-block.json new file mode 100644 index 0000000..e22ce1e --- /dev/null +++ b/corpus/canonical-form/code-block.json @@ -0,0 +1,18 @@ +{ + "content": [ + { + "attrs": { + "language": "sql" + }, + "content": [ + { + "text": "SELECT id\nFROM part\nWHERE qty > 0;", + "type": "text" + } + ], + "type": "codeBlock" + } + ], + "type": "doc", + "version": 1 +} diff --git a/corpus/canonical-form/code-block.md b/corpus/canonical-form/code-block.md new file mode 100644 index 0000000..1b22ef2 --- /dev/null +++ b/corpus/canonical-form/code-block.md @@ -0,0 +1,5 @@ +```sql +SELECT id +FROM part +WHERE qty > 0; +``` diff --git a/corpus/canonical-form/code-span.json b/corpus/canonical-form/code-span.json new file mode 100644 index 0000000..dfdda44 --- /dev/null +++ b/corpus/canonical-form/code-span.json @@ -0,0 +1,118 @@ +{ + "content": [ + { + "content": [ + { + "text": "Run ", + "type": "text" + }, + { + "marks": [ + { + "type": "code" + } + ], + "text": "npm ci", + "type": "text" + }, + { + "text": " first.", + "type": "text" + } + ], + "type": "paragraph" + }, + { + "content": [ + { + "text": "Nested backticks: ", + "type": "text" + }, + { + "marks": [ + { + "type": "code" + } + ], + "text": "run `date` twice", + "type": "text" + } + ], + "type": "paragraph" + }, + { + "content": [ + { + "text": "A span holding ", + "type": "text" + }, + { + "marks": [ + { + "type": "code" + } + ], + "text": "`code`", + "type": "text" + } + ], + "type": "paragraph" + }, + { + "content": [ + { + "text": "Held: ", + "type": "text" + }, + { + "marks": [ + { + "type": "code" + } + ], + "text": " spaced ", + "type": "text" + } + ], + "type": "paragraph" + }, + { + "content": [ + { + "text": "Three: ", + "type": "text" + }, + { + "marks": [ + { + "type": "code" + } + ], + "text": " ", + "type": "text" + } + ], + "type": "paragraph" + }, + { + "content": [ + { + "text": "Literal: ", + "type": "text" + }, + { + "marks": [ + { + "type": "code" + } + ], + "text": "~~not strike~~", + "type": "text" + } + ], + "type": "paragraph" + } + ], + "type": "doc", + "version": 1 +} diff --git a/corpus/canonical-form/code-span.md b/corpus/canonical-form/code-span.md new file mode 100644 index 0000000..274490e --- /dev/null +++ b/corpus/canonical-form/code-span.md @@ -0,0 +1,11 @@ +Run `npm ci` first. + +Nested backticks: ``run `date` twice`` + +A span holding `` `code` `` + +Held: ` spaced ` + +Three: ` ` + +Literal: `~~not strike~~` diff --git a/corpus/canonical-form/em-strong-strike.json b/corpus/canonical-form/em-strong-strike.json new file mode 100644 index 0000000..a185740 --- /dev/null +++ b/corpus/canonical-form/em-strong-strike.json @@ -0,0 +1,76 @@ +{ + "content": [ + { + "content": [ + { + "text": "Read the ", + "type": "text" + }, + { + "marks": [ + { + "type": "em" + } + ], + "text": "manual", + "type": "text" + }, + { + "text": " before ", + "type": "text" + }, + { + "marks": [ + { + "type": "strong" + } + ], + "text": "wiring", + "type": "text" + }, + { + "text": " the ", + "type": "text" + }, + { + "marks": [ + { + "type": "strike" + } + ], + "text": "relay", + "type": "text" + }, + { + "text": ".", + "type": "text" + } + ], + "type": "paragraph" + }, + { + "content": [ + { + "text": "An un", + "type": "text" + }, + { + "marks": [ + { + "type": "em" + } + ], + "text": "real", + "type": "text" + }, + { + "text": "istic goal.", + "type": "text" + } + ], + "type": "paragraph" + } + ], + "type": "doc", + "version": 1 +} diff --git a/corpus/canonical-form/em-strong-strike.md b/corpus/canonical-form/em-strong-strike.md new file mode 100644 index 0000000..d14ffda --- /dev/null +++ b/corpus/canonical-form/em-strong-strike.md @@ -0,0 +1,3 @@ +Read the _manual_ before **wiring** the ~~relay~~. + +An un*real*istic goal. diff --git a/corpus/canonical-form/hard-break.json b/corpus/canonical-form/hard-break.json new file mode 100644 index 0000000..85f8cf6 --- /dev/null +++ b/corpus/canonical-form/hard-break.json @@ -0,0 +1,53 @@ +{ + "content": [ + { + "content": [ + { + "text": "Line one", + "type": "text" + }, + { + "type": "hardBreak" + }, + { + "text": "Line two", + "type": "text" + } + ], + "type": "paragraph" + }, + { + "content": [ + { + "text": "Trailing", + "type": "text" + }, + { + "type": "hardBreak" + } + ], + "type": "paragraph" + }, + { + "attrs": { + "level": 2 + }, + "content": [ + { + "text": "Two", + "type": "text" + }, + { + "type": "hardBreak" + }, + { + "text": "lines", + "type": "text" + } + ], + "type": "heading" + } + ], + "type": "doc", + "version": 1 +} diff --git a/corpus/canonical-form/hard-break.md b/corpus/canonical-form/hard-break.md new file mode 100644 index 0000000..ebede47 --- /dev/null +++ b/corpus/canonical-form/hard-break.md @@ -0,0 +1,6 @@ +Line one\ +Line two + +Trailing:hardBreak{} + +## Two:hardBreak{}lines diff --git a/corpus/canonical-form/heading.json b/corpus/canonical-form/heading.json new file mode 100644 index 0000000..589c4b3 --- /dev/null +++ b/corpus/canonical-form/heading.json @@ -0,0 +1,78 @@ +{ + "content": [ + { + "attrs": { + "level": 1 + }, + "content": [ + { + "text": "Assembly", + "type": "text" + } + ], + "type": "heading" + }, + { + "attrs": { + "level": 2 + }, + "content": [ + { + "text": "Parts", + "type": "text" + } + ], + "type": "heading" + }, + { + "attrs": { + "level": 3 + }, + "content": [ + { + "text": "Fasteners", + "type": "text" + } + ], + "type": "heading" + }, + { + "attrs": { + "level": 4 + }, + "content": [ + { + "text": "Bolts", + "type": "text" + } + ], + "type": "heading" + }, + { + "attrs": { + "level": 5 + }, + "content": [ + { + "text": "Sizes", + "type": "text" + } + ], + "type": "heading" + }, + { + "attrs": { + "level": 6 + }, + "content": [ + { + "text": "M8", + "type": "text" + } + ], + "type": "heading" + } + ], + "type": "doc", + "version": 1 +} diff --git a/corpus/canonical-form/heading.md b/corpus/canonical-form/heading.md new file mode 100644 index 0000000..976188c --- /dev/null +++ b/corpus/canonical-form/heading.md @@ -0,0 +1,11 @@ +# Assembly + +## Parts + +### Fasteners + +#### Bolts + +##### Sizes + +###### M8 diff --git a/corpus/canonical-form/link.json b/corpus/canonical-form/link.json new file mode 100644 index 0000000..d50684b --- /dev/null +++ b/corpus/canonical-form/link.json @@ -0,0 +1,116 @@ +{ + "content": [ + { + "content": [ + { + "text": "See ", + "type": "text" + }, + { + "marks": [ + { + "attrs": { + "href": "https://example.com/changelog" + }, + "type": "link" + } + ], + "text": "the changelog", + "type": "text" + }, + { + "text": ".", + "type": "text" + } + ], + "type": "paragraph" + }, + { + "content": [ + { + "marks": [ + { + "attrs": { + "href": "https://example.com/" + }, + "type": "link" + } + ], + "text": "https://example.com/", + "type": "text" + } + ], + "type": "paragraph" + }, + { + "content": [ + { + "text": "Read ", + "type": "text" + }, + { + "marks": [ + { + "attrs": { + "href": "https://example.com/guide", + "title": "Setup guide" + }, + "type": "link" + } + ], + "text": "the guide", + "type": "text" + }, + { + "text": ".", + "type": "text" + } + ], + "type": "paragraph" + }, + { + "content": [ + { + "text": "Open ", + "type": "text" + }, + { + "marks": [ + { + "attrs": { + "href": "https://example.com/the plan.pdf" + }, + "type": "link" + } + ], + "text": "the plan", + "type": "text" + }, + { + "text": ".", + "type": "text" + } + ], + "type": "paragraph" + }, + { + "content": [ + { + "marks": [ + { + "attrs": { + "href": "/parts/m8" + }, + "type": "link" + } + ], + "text": "/parts/m8", + "type": "text" + } + ], + "type": "paragraph" + } + ], + "type": "doc", + "version": 1 +} diff --git a/corpus/canonical-form/link.md b/corpus/canonical-form/link.md new file mode 100644 index 0000000..ab5d9b1 --- /dev/null +++ b/corpus/canonical-form/link.md @@ -0,0 +1,9 @@ +See [the changelog](https://example.com/changelog). + + + +Read [the guide](https://example.com/guide "Setup guide"). + +Open [the plan](). + +[/parts/m8](/parts/m8) diff --git a/corpus/canonical-form/ordered-list-order.json b/corpus/canonical-form/ordered-list-order.json new file mode 100644 index 0000000..c7d1fbe --- /dev/null +++ b/corpus/canonical-form/ordered-list-order.json @@ -0,0 +1,65 @@ +{ + "content": [ + { + "attrs": { + "order": 9 + }, + "content": [ + { + "content": [ + { + "content": [ + { + "text": "Bolt M8", + "type": "text" + } + ], + "type": "paragraph" + } + ], + "type": "listItem" + }, + { + "content": [ + { + "content": [ + { + "text": "Nut M8", + "type": "text" + } + ], + "type": "paragraph" + }, + { + "content": [ + { + "text": "Zinc-plated.", + "type": "text" + } + ], + "type": "paragraph" + } + ], + "type": "listItem" + }, + { + "content": [ + { + "content": [ + { + "text": "Washer M8", + "type": "text" + } + ], + "type": "paragraph" + } + ], + "type": "listItem" + } + ], + "type": "orderedList" + } + ], + "type": "doc", + "version": 1 +} diff --git a/corpus/canonical-form/ordered-list-order.md b/corpus/canonical-form/ordered-list-order.md new file mode 100644 index 0000000..163e5a2 --- /dev/null +++ b/corpus/canonical-form/ordered-list-order.md @@ -0,0 +1,5 @@ +9. Bolt M8 +10. Nut M8 + + Zinc-plated. +11. Washer M8 diff --git a/corpus/canonical-form/ordered-list.json b/corpus/canonical-form/ordered-list.json new file mode 100644 index 0000000..a39528a --- /dev/null +++ b/corpus/canonical-form/ordered-list.json @@ -0,0 +1,53 @@ +{ + "content": [ + { + "content": [ + { + "content": [ + { + "content": [ + { + "text": "Bolt M8", + "type": "text" + } + ], + "type": "paragraph" + } + ], + "type": "listItem" + }, + { + "content": [ + { + "content": [ + { + "text": "Nut M8", + "type": "text" + } + ], + "type": "paragraph" + } + ], + "type": "listItem" + }, + { + "content": [ + { + "content": [ + { + "text": "Washer M8", + "type": "text" + } + ], + "type": "paragraph" + } + ], + "type": "listItem" + } + ], + "type": "orderedList" + } + ], + "type": "doc", + "version": 1 +} diff --git a/corpus/canonical-form/ordered-list.md b/corpus/canonical-form/ordered-list.md new file mode 100644 index 0000000..3e512e7 --- /dev/null +++ b/corpus/canonical-form/ordered-list.md @@ -0,0 +1,3 @@ +1. Bolt M8 +2. Nut M8 +3. Washer M8 diff --git a/corpus/canonical-form/paragraph-empty.json b/corpus/canonical-form/paragraph-empty.json new file mode 100644 index 0000000..f7ebf4b --- /dev/null +++ b/corpus/canonical-form/paragraph-empty.json @@ -0,0 +1,27 @@ +{ + "content": [ + { + "content": [ + { + "text": "First.", + "type": "text" + } + ], + "type": "paragraph" + }, + { + "type": "paragraph" + }, + { + "content": [ + { + "text": "Second.", + "type": "text" + } + ], + "type": "paragraph" + } + ], + "type": "doc", + "version": 1 +} diff --git a/corpus/canonical-form/paragraph-empty.md b/corpus/canonical-form/paragraph-empty.md new file mode 100644 index 0000000..9255212 --- /dev/null +++ b/corpus/canonical-form/paragraph-empty.md @@ -0,0 +1,5 @@ +First. + +::paragraph + +Second. diff --git a/corpus/canonical-form/paragraph.json b/corpus/canonical-form/paragraph.json new file mode 100644 index 0000000..dd8141f --- /dev/null +++ b/corpus/canonical-form/paragraph.json @@ -0,0 +1,24 @@ +{ + "content": [ + { + "content": [ + { + "text": "The converter emits every paragraph on a single line, however long it runs, because a soft line break in input carries no meaning ADF can hold.", + "type": "text" + } + ], + "type": "paragraph" + }, + { + "content": [ + { + "text": "Blocks are separated by exactly one blank line.", + "type": "text" + } + ], + "type": "paragraph" + } + ], + "type": "doc", + "version": 1 +} diff --git a/corpus/canonical-form/paragraph.md b/corpus/canonical-form/paragraph.md new file mode 100644 index 0000000..3dd7081 --- /dev/null +++ b/corpus/canonical-form/paragraph.md @@ -0,0 +1,3 @@ +The converter emits every paragraph on a single line, however long it runs, because a soft line break in input carries no meaning ADF can hold. + +Blocks are separated by exactly one blank line. diff --git a/corpus/canonical-form/rule.json b/corpus/canonical-form/rule.json new file mode 100644 index 0000000..8caf8ab --- /dev/null +++ b/corpus/canonical-form/rule.json @@ -0,0 +1,27 @@ +{ + "content": [ + { + "content": [ + { + "text": "Before.", + "type": "text" + } + ], + "type": "paragraph" + }, + { + "type": "rule" + }, + { + "content": [ + { + "text": "After.", + "type": "text" + } + ], + "type": "paragraph" + } + ], + "type": "doc", + "version": 1 +} diff --git a/corpus/canonical-form/rule.md b/corpus/canonical-form/rule.md new file mode 100644 index 0000000..8c2fd78 --- /dev/null +++ b/corpus/canonical-form/rule.md @@ -0,0 +1,5 @@ +Before. + +--- + +After. diff --git a/corpus/canonical-form/text-escaping.json b/corpus/canonical-form/text-escaping.json new file mode 100644 index 0000000..1465d19 --- /dev/null +++ b/corpus/canonical-form/text-escaping.json @@ -0,0 +1,42 @@ +{ + "content": [ + { + "content": [ + { + "text": "# Not a heading", + "type": "text" + } + ], + "type": "paragraph" + }, + { + "content": [ + { + "text": "- not a bullet", + "type": "text" + } + ], + "type": "paragraph" + }, + { + "content": [ + { + "text": "2 * 3 * 4 = 24", + "type": "text" + } + ], + "type": "paragraph" + }, + { + "content": [ + { + "text": "snake_case_name", + "type": "text" + } + ], + "type": "paragraph" + } + ], + "type": "doc", + "version": 1 +} diff --git a/corpus/canonical-form/text-escaping.md b/corpus/canonical-form/text-escaping.md new file mode 100644 index 0000000..de71c9c --- /dev/null +++ b/corpus/canonical-form/text-escaping.md @@ -0,0 +1,7 @@ +\# Not a heading + +\- not a bullet + +2 * 3 * 4 = 24 + +snake_case_name diff --git a/todo.md b/todo.md index 88c719b..1e57210 100644 --- a/todo.md +++ b/todo.md @@ -21,7 +21,25 @@ detail is settled at its own milestone. payloads for external-URL support — if it exists, revisit the media section's mid-text-image error and its "no slot" ground. - [ ] **1d — Corpus start** (§10): checked-in ADF ↔ canonical-markdown fixture pairs per spec'd - node. + node, in `corpus/`, one directory per `spec/flavour.md` section. + - [ ] **1d1 — Canonical form**: the plain-CommonMark subset — blockquote, bulletList, + codeBlock, heading, orderedList, paragraph, rule, listItem, hardBreak, text, code spans, + and the `code`, `em`, `link`, `strike` and `strong` marks. + - [ ] **1d2 — Block nodes**: panel, expand/nestedExpand, the media family and the CommonMark + image shape, both table forms, task and decision lists, layout, extensions, syncBlock — + with the reserved `marks` attribute and the fence lengths nesting forces. + - [ ] **1d3 — Inline nodes and marks**: date, emoji, inlineCard, mediaInline, mention, status; + border, subsup, textColor, underline; the content slot's `text` attribute and the + `:text{text="…"}` whitespace spelling. + - [ ] **1d4 — Opaque carry** (§3): an unknown node in both positions, the reserved `adf` info + string, and the `codeBlock` whose language is `adf`. + - [ ] **1d5 — Carve-outs and combinations**: the three carve-outs and their escapes, mark + nesting order and the runs a carry breaks, attribute canonicalization, and documents + combining nodes rather than isolating one. + - [ ] **1d6 — Input normalization**: one-way markdown→ADF fixtures, not pairs — setext + headings, indented code, loose lists, `*`/`+` bullets, entity references, soft wraps. + - [ ] **1d7 — Error input**: also one-way, a markdown input per named error. Waits on milestone + 3 naming them; 1d's pairs are valid documents only. - [ ] **2 — `adfToMarkdown`.** First real code — decide here where §10's coverage check lives. - [ ] **3 — `markdownToAdf`.** The CommonMark parser is the largest single component. The raw-HTML element mapping is empty until milestone 6, so at `0.1.0` every raw-HTML construct in input -- 2.52.0 From fc92bf8bf56f5787d622fcec314d24347ad82857 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 24 Aug 2026 11:40:11 +0200 Subject: [PATCH 2/4] Corpus by contract kind: pin the fence and escaping rules, hold two questions --- corpus/README.md | 17 ++++-- corpus/canonical-form/ordered-list.json | 53 ------------------- corpus/canonical-form/ordered-list.md | 3 -- .../commonmark-subset}/blockquote.json | 0 .../commonmark-subset}/blockquote.md | 0 .../commonmark-subset}/bullet-list.json | 0 .../commonmark-subset}/bullet-list.md | 0 .../code-block-backtick-run-mid-line.json | 18 +++++++ .../code-block-backtick-run-mid-line.md | 3 ++ .../code-block-backtick-run.json | 3 ++ .../code-block-backtick-run.md | 2 +- .../commonmark-subset}/code-block.json | 0 .../commonmark-subset}/code-block.md | 0 .../commonmark-subset}/code-span.json | 0 .../commonmark-subset}/code-span.md | 0 .../commonmark-subset/document-empty.json | 4 ++ .../commonmark-subset/document-empty.md | 0 .../commonmark-subset}/em-strong-strike.json | 0 .../commonmark-subset}/em-strong-strike.md | 0 .../commonmark-subset}/hard-break.json | 0 .../commonmark-subset}/hard-break.md | 0 .../commonmark-subset}/heading.json | 0 .../commonmark-subset}/heading.md | 0 .../commonmark-subset}/link.json | 0 .../commonmark-subset}/link.md | 0 .../ordered-list-order.json | 0 .../commonmark-subset}/ordered-list-order.md | 0 .../commonmark-subset}/paragraph-empty.json | 0 .../commonmark-subset}/paragraph-empty.md | 0 .../commonmark-subset}/paragraph.json | 0 .../commonmark-subset}/paragraph.md | 0 .../commonmark-subset}/rule.json | 0 .../commonmark-subset}/rule.md | 0 .../commonmark-subset}/text-escaping.json | 9 ++++ .../commonmark-subset}/text-escaping.md | 2 + spec/flavour.md | 10 ++-- todo.md | 35 ++++++++---- 37 files changed, 84 insertions(+), 75 deletions(-) delete mode 100644 corpus/canonical-form/ordered-list.json delete mode 100644 corpus/canonical-form/ordered-list.md rename corpus/{canonical-form => round-trip/commonmark-subset}/blockquote.json (100%) rename corpus/{canonical-form => round-trip/commonmark-subset}/blockquote.md (100%) rename corpus/{canonical-form => round-trip/commonmark-subset}/bullet-list.json (100%) rename corpus/{canonical-form => round-trip/commonmark-subset}/bullet-list.md (100%) create mode 100644 corpus/round-trip/commonmark-subset/code-block-backtick-run-mid-line.json create mode 100644 corpus/round-trip/commonmark-subset/code-block-backtick-run-mid-line.md rename corpus/{canonical-form => round-trip/commonmark-subset}/code-block-backtick-run.json (78%) rename corpus/{canonical-form => round-trip/commonmark-subset}/code-block-backtick-run.md (58%) rename corpus/{canonical-form => round-trip/commonmark-subset}/code-block.json (100%) rename corpus/{canonical-form => round-trip/commonmark-subset}/code-block.md (100%) rename corpus/{canonical-form => round-trip/commonmark-subset}/code-span.json (100%) rename corpus/{canonical-form => round-trip/commonmark-subset}/code-span.md (100%) create mode 100644 corpus/round-trip/commonmark-subset/document-empty.json create mode 100644 corpus/round-trip/commonmark-subset/document-empty.md rename corpus/{canonical-form => round-trip/commonmark-subset}/em-strong-strike.json (100%) rename corpus/{canonical-form => round-trip/commonmark-subset}/em-strong-strike.md (100%) rename corpus/{canonical-form => round-trip/commonmark-subset}/hard-break.json (100%) rename corpus/{canonical-form => round-trip/commonmark-subset}/hard-break.md (100%) rename corpus/{canonical-form => round-trip/commonmark-subset}/heading.json (100%) rename corpus/{canonical-form => round-trip/commonmark-subset}/heading.md (100%) rename corpus/{canonical-form => round-trip/commonmark-subset}/link.json (100%) rename corpus/{canonical-form => round-trip/commonmark-subset}/link.md (100%) rename corpus/{canonical-form => round-trip/commonmark-subset}/ordered-list-order.json (100%) rename corpus/{canonical-form => round-trip/commonmark-subset}/ordered-list-order.md (100%) rename corpus/{canonical-form => round-trip/commonmark-subset}/paragraph-empty.json (100%) rename corpus/{canonical-form => round-trip/commonmark-subset}/paragraph-empty.md (100%) rename corpus/{canonical-form => round-trip/commonmark-subset}/paragraph.json (100%) rename corpus/{canonical-form => round-trip/commonmark-subset}/paragraph.md (100%) rename corpus/{canonical-form => round-trip/commonmark-subset}/rule.json (100%) rename corpus/{canonical-form => round-trip/commonmark-subset}/rule.md (100%) rename corpus/{canonical-form => round-trip/commonmark-subset}/text-escaping.json (81%) rename corpus/{canonical-form => round-trip/commonmark-subset}/text-escaping.md (79%) diff --git a/corpus/README.md b/corpus/README.md index 4122780..5510e21 100644 --- a/corpus/README.md +++ b/corpus/README.md @@ -1,8 +1,15 @@ # The corpus -`.json` is an ADF document; `.md` is the markdown `adfToMarkdown` must emit for it, -byte for byte including the trailing newline, and that `markdownToAdf` must read back to that same -document (AGENTS.md §2). Directories mirror `spec/flavour.md`'s sections. +One directory per contract kind: -The JSON is editor-normal — empty `attrs`, `marks` and `content` as the absent key, adjacent -identical-mark text nodes merged — two-space indent, keys sorted. +- `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: `commonmark-subset/`, `block-nodes/`, `inline-nodes/`, `opaque-carry/`, + `combinations/`. +- `normalization/` — `.md` + `.json`: markdown input, and the document + `markdownToAdf` must build from it. One-way; the markdown is not canonical. +- `errors/` — `.md` + `.error`: markdown input that must not convert. +- `real-payloads/` — `.json`: sanitized live ADF, round-tripped ADF→markdown→ADF. No + expected markdown. + +JSON is editor-normal (AGENTS.md §2), two-space indent, keys sorted. diff --git a/corpus/canonical-form/ordered-list.json b/corpus/canonical-form/ordered-list.json deleted file mode 100644 index a39528a..0000000 --- a/corpus/canonical-form/ordered-list.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "content": [ - { - "content": [ - { - "content": [ - { - "content": [ - { - "text": "Bolt M8", - "type": "text" - } - ], - "type": "paragraph" - } - ], - "type": "listItem" - }, - { - "content": [ - { - "content": [ - { - "text": "Nut M8", - "type": "text" - } - ], - "type": "paragraph" - } - ], - "type": "listItem" - }, - { - "content": [ - { - "content": [ - { - "text": "Washer M8", - "type": "text" - } - ], - "type": "paragraph" - } - ], - "type": "listItem" - } - ], - "type": "orderedList" - } - ], - "type": "doc", - "version": 1 -} diff --git a/corpus/canonical-form/ordered-list.md b/corpus/canonical-form/ordered-list.md deleted file mode 100644 index 3e512e7..0000000 --- a/corpus/canonical-form/ordered-list.md +++ /dev/null @@ -1,3 +0,0 @@ -1. Bolt M8 -2. Nut M8 -3. Washer M8 diff --git a/corpus/canonical-form/blockquote.json b/corpus/round-trip/commonmark-subset/blockquote.json similarity index 100% rename from corpus/canonical-form/blockquote.json rename to corpus/round-trip/commonmark-subset/blockquote.json diff --git a/corpus/canonical-form/blockquote.md b/corpus/round-trip/commonmark-subset/blockquote.md similarity index 100% rename from corpus/canonical-form/blockquote.md rename to corpus/round-trip/commonmark-subset/blockquote.md diff --git a/corpus/canonical-form/bullet-list.json b/corpus/round-trip/commonmark-subset/bullet-list.json similarity index 100% rename from corpus/canonical-form/bullet-list.json rename to corpus/round-trip/commonmark-subset/bullet-list.json diff --git a/corpus/canonical-form/bullet-list.md b/corpus/round-trip/commonmark-subset/bullet-list.md similarity index 100% rename from corpus/canonical-form/bullet-list.md rename to corpus/round-trip/commonmark-subset/bullet-list.md diff --git a/corpus/round-trip/commonmark-subset/code-block-backtick-run-mid-line.json b/corpus/round-trip/commonmark-subset/code-block-backtick-run-mid-line.json new file mode 100644 index 0000000..82391ee --- /dev/null +++ b/corpus/round-trip/commonmark-subset/code-block-backtick-run-mid-line.json @@ -0,0 +1,18 @@ +{ + "content": [ + { + "attrs": { + "language": "bash" + }, + "content": [ + { + "text": "echo \"```\" >> notes.md", + "type": "text" + } + ], + "type": "codeBlock" + } + ], + "type": "doc", + "version": 1 +} diff --git a/corpus/round-trip/commonmark-subset/code-block-backtick-run-mid-line.md b/corpus/round-trip/commonmark-subset/code-block-backtick-run-mid-line.md new file mode 100644 index 0000000..cd3a3b3 --- /dev/null +++ b/corpus/round-trip/commonmark-subset/code-block-backtick-run-mid-line.md @@ -0,0 +1,3 @@ +````bash +echo "```" >> notes.md +```` diff --git a/corpus/canonical-form/code-block-backtick-run.json b/corpus/round-trip/commonmark-subset/code-block-backtick-run.json similarity index 78% rename from corpus/canonical-form/code-block-backtick-run.json rename to corpus/round-trip/commonmark-subset/code-block-backtick-run.json index 012ffd4..a6452fb 100644 --- a/corpus/canonical-form/code-block-backtick-run.json +++ b/corpus/round-trip/commonmark-subset/code-block-backtick-run.json @@ -1,6 +1,9 @@ { "content": [ { + "attrs": { + "language": "markdown" + }, "content": [ { "text": "```\ncode\n```", diff --git a/corpus/canonical-form/code-block-backtick-run.md b/corpus/round-trip/commonmark-subset/code-block-backtick-run.md similarity index 58% rename from corpus/canonical-form/code-block-backtick-run.md rename to corpus/round-trip/commonmark-subset/code-block-backtick-run.md index 436503d..721c874 100644 --- a/corpus/canonical-form/code-block-backtick-run.md +++ b/corpus/round-trip/commonmark-subset/code-block-backtick-run.md @@ -1,4 +1,4 @@ -```` +````markdown ``` code ``` diff --git a/corpus/canonical-form/code-block.json b/corpus/round-trip/commonmark-subset/code-block.json similarity index 100% rename from corpus/canonical-form/code-block.json rename to corpus/round-trip/commonmark-subset/code-block.json diff --git a/corpus/canonical-form/code-block.md b/corpus/round-trip/commonmark-subset/code-block.md similarity index 100% rename from corpus/canonical-form/code-block.md rename to corpus/round-trip/commonmark-subset/code-block.md diff --git a/corpus/canonical-form/code-span.json b/corpus/round-trip/commonmark-subset/code-span.json similarity index 100% rename from corpus/canonical-form/code-span.json rename to corpus/round-trip/commonmark-subset/code-span.json diff --git a/corpus/canonical-form/code-span.md b/corpus/round-trip/commonmark-subset/code-span.md similarity index 100% rename from corpus/canonical-form/code-span.md rename to corpus/round-trip/commonmark-subset/code-span.md diff --git a/corpus/round-trip/commonmark-subset/document-empty.json b/corpus/round-trip/commonmark-subset/document-empty.json new file mode 100644 index 0000000..6591ef5 --- /dev/null +++ b/corpus/round-trip/commonmark-subset/document-empty.json @@ -0,0 +1,4 @@ +{ + "type": "doc", + "version": 1 +} diff --git a/corpus/round-trip/commonmark-subset/document-empty.md b/corpus/round-trip/commonmark-subset/document-empty.md new file mode 100644 index 0000000..e69de29 diff --git a/corpus/canonical-form/em-strong-strike.json b/corpus/round-trip/commonmark-subset/em-strong-strike.json similarity index 100% rename from corpus/canonical-form/em-strong-strike.json rename to corpus/round-trip/commonmark-subset/em-strong-strike.json diff --git a/corpus/canonical-form/em-strong-strike.md b/corpus/round-trip/commonmark-subset/em-strong-strike.md similarity index 100% rename from corpus/canonical-form/em-strong-strike.md rename to corpus/round-trip/commonmark-subset/em-strong-strike.md diff --git a/corpus/canonical-form/hard-break.json b/corpus/round-trip/commonmark-subset/hard-break.json similarity index 100% rename from corpus/canonical-form/hard-break.json rename to corpus/round-trip/commonmark-subset/hard-break.json diff --git a/corpus/canonical-form/hard-break.md b/corpus/round-trip/commonmark-subset/hard-break.md similarity index 100% rename from corpus/canonical-form/hard-break.md rename to corpus/round-trip/commonmark-subset/hard-break.md diff --git a/corpus/canonical-form/heading.json b/corpus/round-trip/commonmark-subset/heading.json similarity index 100% rename from corpus/canonical-form/heading.json rename to corpus/round-trip/commonmark-subset/heading.json diff --git a/corpus/canonical-form/heading.md b/corpus/round-trip/commonmark-subset/heading.md similarity index 100% rename from corpus/canonical-form/heading.md rename to corpus/round-trip/commonmark-subset/heading.md diff --git a/corpus/canonical-form/link.json b/corpus/round-trip/commonmark-subset/link.json similarity index 100% rename from corpus/canonical-form/link.json rename to corpus/round-trip/commonmark-subset/link.json diff --git a/corpus/canonical-form/link.md b/corpus/round-trip/commonmark-subset/link.md similarity index 100% rename from corpus/canonical-form/link.md rename to corpus/round-trip/commonmark-subset/link.md diff --git a/corpus/canonical-form/ordered-list-order.json b/corpus/round-trip/commonmark-subset/ordered-list-order.json similarity index 100% rename from corpus/canonical-form/ordered-list-order.json rename to corpus/round-trip/commonmark-subset/ordered-list-order.json diff --git a/corpus/canonical-form/ordered-list-order.md b/corpus/round-trip/commonmark-subset/ordered-list-order.md similarity index 100% rename from corpus/canonical-form/ordered-list-order.md rename to corpus/round-trip/commonmark-subset/ordered-list-order.md diff --git a/corpus/canonical-form/paragraph-empty.json b/corpus/round-trip/commonmark-subset/paragraph-empty.json similarity index 100% rename from corpus/canonical-form/paragraph-empty.json rename to corpus/round-trip/commonmark-subset/paragraph-empty.json diff --git a/corpus/canonical-form/paragraph-empty.md b/corpus/round-trip/commonmark-subset/paragraph-empty.md similarity index 100% rename from corpus/canonical-form/paragraph-empty.md rename to corpus/round-trip/commonmark-subset/paragraph-empty.md diff --git a/corpus/canonical-form/paragraph.json b/corpus/round-trip/commonmark-subset/paragraph.json similarity index 100% rename from corpus/canonical-form/paragraph.json rename to corpus/round-trip/commonmark-subset/paragraph.json diff --git a/corpus/canonical-form/paragraph.md b/corpus/round-trip/commonmark-subset/paragraph.md similarity index 100% rename from corpus/canonical-form/paragraph.md rename to corpus/round-trip/commonmark-subset/paragraph.md diff --git a/corpus/canonical-form/rule.json b/corpus/round-trip/commonmark-subset/rule.json similarity index 100% rename from corpus/canonical-form/rule.json rename to corpus/round-trip/commonmark-subset/rule.json diff --git a/corpus/canonical-form/rule.md b/corpus/round-trip/commonmark-subset/rule.md similarity index 100% rename from corpus/canonical-form/rule.md rename to corpus/round-trip/commonmark-subset/rule.md diff --git a/corpus/canonical-form/text-escaping.json b/corpus/round-trip/commonmark-subset/text-escaping.json similarity index 81% rename from corpus/canonical-form/text-escaping.json rename to corpus/round-trip/commonmark-subset/text-escaping.json index 1465d19..5481877 100644 --- a/corpus/canonical-form/text-escaping.json +++ b/corpus/round-trip/commonmark-subset/text-escaping.json @@ -35,6 +35,15 @@ } ], "type": "paragraph" + }, + { + "content": [ + { + "text": "*not emphasis*", + "type": "text" + } + ], + "type": "paragraph" } ], "type": "doc", diff --git a/corpus/canonical-form/text-escaping.md b/corpus/round-trip/commonmark-subset/text-escaping.md similarity index 79% rename from corpus/canonical-form/text-escaping.md rename to corpus/round-trip/commonmark-subset/text-escaping.md index de71c9c..c30602b 100644 --- a/corpus/canonical-form/text-escaping.md +++ b/corpus/round-trip/commonmark-subset/text-escaping.md @@ -5,3 +5,5 @@ 2 * 3 * 4 = 24 snake_case_name + +\*not emphasis* diff --git a/spec/flavour.md b/spec/flavour.md index 0a92971..72ddebd 100644 --- a/spec/flavour.md +++ b/spec/flavour.md @@ -23,7 +23,8 @@ normalizes to it through the round-trip. - Blockquotes prefix lines with `> `; a blank line inside a blockquote is a bare `>`. - ATX headings (`#` … `######`); setext input normalizes to ATX. - Code fences ``` with the node's language as info string, the fence lengthened past any backtick - run in the content; indented-code input normalizes to fences. + run in the content — the longest run anywhere plus one, counting mid-line runs no closing fence + could match; indented-code input normalizes to fences. - Code spans: a backtick string one longer than the longest backtick run in the text, the text padded with one space on each side where it begins or ends with a backtick, or begins and ends with a space without being all spaces. The content is literal — inline parsing does not see @@ -37,8 +38,11 @@ normalizes to it through the round-trip. CommonMark autolink (absolute URI). - Paragraphs on one line — no soft wrapping; a soft line break in input becomes a single space. - Entity references in input decode to their characters; output backslash-escapes only where text - would otherwise parse as syntax. -- Blocks separated by one blank line, no trailing whitespace, single trailing newline. + would otherwise parse as syntax: escape the leading delimiter of a construct that would otherwise + open, re-scan from there, and repeat — with the opener literal the closer parses as text, so + `*not emphasis*` is `\*not emphasis*`, one backslash. +- Blocks separated by one blank line, no trailing whitespace, single trailing newline; a document + with no blocks is the empty string. ## Directives diff --git a/todo.md b/todo.md index 1e57210..6d7fd5b 100644 --- a/todo.md +++ b/todo.md @@ -20,11 +20,18 @@ detail is settled at its own milestone. escape-based, never literal, since pipe cells trim and pad. At `mediaInline`, check real payloads for external-URL support — if it exists, revisit the media section's mid-text-image error and its "no slot" ground. -- [ ] **1d — Corpus start** (§10): checked-in ADF ↔ canonical-markdown fixture pairs per spec'd - node, in `corpus/`, one directory per `spec/flavour.md` section. - - [ ] **1d1 — Canonical form**: the plain-CommonMark subset — blockquote, bulletList, - codeBlock, heading, orderedList, paragraph, rule, listItem, hardBreak, text, code spans, - and the `code`, `em`, `link`, `strike` and `strong` marks. +- [ ] **1d — Corpus start** (§10): checked-in fixtures per spec'd node, in `corpus/`, one + directory per contract kind (`corpus/README.md`). + **Blocked on the maintainer** (§15), not to be guessed: Canonical form has no totality + guard — a `paragraph`, `heading`, `blockquote` or `codeBlock` carrying a `localId`, or a + `blockquote` carrying marks, has no spelling that keeps it, and picking one (directive + sections for the six CommonMark block nodes, or the opaque carry) is a permanent format + decision (§8). Its three collision sites stay out of the corpus until then: an `orderedList` + starting at 1, a `codeBlock` whose info string is empty, and `media` with an empty `alt` — + each a choice between the absent attribute and the empty value. + - [ ] **1d1 — The CommonMark subset**: blockquote, bulletList, codeBlock, heading, orderedList, + paragraph, rule, listItem, hardBreak, text, code spans, and the `code`, `em`, `link`, + `strike` and `strong` marks — one mark per text node; nesting is 1d5's. - [ ] **1d2 — Block nodes**: panel, expand/nestedExpand, the media family and the CommonMark image shape, both table forms, task and decision lists, layout, extensions, syncBlock — with the reserved `marks` attribute and the fence lengths nesting forces. @@ -38,14 +45,22 @@ detail is settled at its own milestone. combining nodes rather than isolating one. - [ ] **1d6 — Input normalization**: one-way markdown→ADF fixtures, not pairs — setext headings, indented code, loose lists, `*`/`+` bullets, entity references, soft wraps. - - [ ] **1d7 — Error input**: also one-way, a markdown input per named error. Waits on milestone - 3 naming them; 1d's pairs are valid documents only. -- [ ] **2 — `adfToMarkdown`.** First real code — decide here where §10's coverage check lives. + - [ ] **1d7 — Error input**: also one-way, a markdown input per named error, asserting only + that conversion fails — malformed directives, the image gap, a claimed pipe-table line + that does not parse, the content slot, raw HTML with no mapping. Which error each returns + is pinned at milestone 3, where they are named. +- [ ] **2 — `adfToMarkdown`.** First real code — decide here where §10's coverage check lives, and + gate that every `corpus/**/*.json` re-serializes to itself under the library's own canonical + serializer: one implementation, keys sorted, two spellings — two-space indent for the corpus + files and the block carry's body, compact for the inline carry. - [ ] **3 — `markdownToAdf`.** The CommonMark parser is the largest single component. The raw-HTML element mapping is empty until milestone 6, so at `0.1.0` every raw-HTML construct in input - is an error result. + is an error result. The CommonMark spec suite runs against it from here (§10), and + `corpus/errors/` gains the error each fixture must return (1d7). - [ ] **4 — Round-trip property tests** over the corpus, both ways — the thing that proves 2 and - 3. Generators emit editor-normal ADF (§2). + 3. Generators emit editor-normal ADF (§2). Real sanitized ADF from live Atlassian APIs lands + here too (§10), in `corpus/real-payloads/`: an ADF→markdown→ADF check with no expected + markdown, the payloads supplied by the maintainer. - [ ] **5 — Release pipeline, ship `0.1.0`.** Publish-on-version-change (§9), `NPM_TOKEN` secret, the repo made public first (§6). `0.1.0` is the markdown round-trip: both markdown directions, the types, `isAdfDocument`. The build lands here: a build tsconfig emitting JS -- 2.52.0 From 346bdcc27d8a5bb257240381540231ab0e798646 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 24 Aug 2026 12:32:53 +0200 Subject: [PATCH 3/4] Declare which errors/ file is required, floor the fence at three --- corpus/README.md | 3 ++- spec/flavour.md | 10 +++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/corpus/README.md b/corpus/README.md index 5510e21..722a3e3 100644 --- a/corpus/README.md +++ b/corpus/README.md @@ -8,7 +8,8 @@ One directory per contract kind: `combinations/`. - `normalization/` — `.md` + `.json`: markdown input, and the document `markdownToAdf` must build from it. One-way; the markdown is not canonical. -- `errors/` — `.md` + `.error`: markdown input that must not convert. +- `errors/` — `.md`: markdown input that must not convert. A `.error` beside it + pins which error. - `real-payloads/` — `.json`: sanitized live ADF, round-tripped ADF→markdown→ADF. No expected markdown. diff --git a/spec/flavour.md b/spec/flavour.md index 72ddebd..40cfbf7 100644 --- a/spec/flavour.md +++ b/spec/flavour.md @@ -23,8 +23,8 @@ normalizes to it through the round-trip. - Blockquotes prefix lines with `> `; a blank line inside a blockquote is a bare `>`. - ATX headings (`#` … `######`); setext input normalizes to ATX. - Code fences ``` with the node's language as info string, the fence lengthened past any backtick - run in the content — the longest run anywhere plus one, counting mid-line runs no closing fence - could match; indented-code input normalizes to fences. + run in the content — the longest run anywhere plus one, at least three, counting mid-line runs + no closing fence could match; indented-code input normalizes to fences. - Code spans: a backtick string one longer than the longest backtick run in the text, the text padded with one space on each side where it begins or ends with a backtick, or begins and ends with a space without being all spaces. The content is literal — inline parsing does not see @@ -38,9 +38,9 @@ normalizes to it through the round-trip. CommonMark autolink (absolute URI). - Paragraphs on one line — no soft wrapping; a soft line break in input becomes a single space. - Entity references in input decode to their characters; output backslash-escapes only where text - would otherwise parse as syntax: escape the leading delimiter of a construct that would otherwise - open, re-scan from there, and repeat — with the opener literal the closer parses as text, so - `*not emphasis*` is `\*not emphasis*`, one backslash. + would otherwise parse as syntax: escape the leading delimiter of a construct that would + otherwise open, re-scan from there, and repeat — with the opener literal the closer parses as + text, so `*not emphasis*` is `\*not emphasis*`, one backslash. - Blocks separated by one blank line, no trailing whitespace, single trailing newline; a document with no blocks is the empty string. -- 2.52.0 From 1804d16ccee7123543b6e6602135157e9fa22a4e Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 24 Aug 2026 12:52:38 +0200 Subject: [PATCH 4/4] Scope the trailing-whitespace and escaping-scan rules, name the totality gap --- corpus/README.md | 3 +-- .../code-block-trailing-whitespace.json | 18 +++++++++++++++ .../code-block-trailing-whitespace.md | 4 ++++ .../commonmark-subset/ordered-list-order.json | 9 ++++++++ .../commonmark-subset/ordered-list-order.md | 2 ++ spec/flavour.md | 11 +++++----- todo.md | 22 ++++++++++++++----- 7 files changed, 56 insertions(+), 13 deletions(-) create mode 100644 corpus/round-trip/commonmark-subset/code-block-trailing-whitespace.json create mode 100644 corpus/round-trip/commonmark-subset/code-block-trailing-whitespace.md diff --git a/corpus/README.md b/corpus/README.md index 722a3e3..2137c61 100644 --- a/corpus/README.md +++ b/corpus/README.md @@ -4,8 +4,7 @@ One directory per contract kind: - `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: `commonmark-subset/`, `block-nodes/`, `inline-nodes/`, `opaque-carry/`, - `combinations/`. + by node family. - `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/commonmark-subset/code-block-trailing-whitespace.json b/corpus/round-trip/commonmark-subset/code-block-trailing-whitespace.json new file mode 100644 index 0000000..9a7cca0 --- /dev/null +++ b/corpus/round-trip/commonmark-subset/code-block-trailing-whitespace.json @@ -0,0 +1,18 @@ +{ + "content": [ + { + "attrs": { + "language": "markdown" + }, + "content": [ + { + "text": "Line one \nLine two", + "type": "text" + } + ], + "type": "codeBlock" + } + ], + "type": "doc", + "version": 1 +} diff --git a/corpus/round-trip/commonmark-subset/code-block-trailing-whitespace.md b/corpus/round-trip/commonmark-subset/code-block-trailing-whitespace.md new file mode 100644 index 0000000..fd992ce --- /dev/null +++ b/corpus/round-trip/commonmark-subset/code-block-trailing-whitespace.md @@ -0,0 +1,4 @@ +```markdown +Line one +Line two +``` diff --git a/corpus/round-trip/commonmark-subset/ordered-list-order.json b/corpus/round-trip/commonmark-subset/ordered-list-order.json index c7d1fbe..1364f28 100644 --- a/corpus/round-trip/commonmark-subset/ordered-list-order.json +++ b/corpus/round-trip/commonmark-subset/ordered-list-order.json @@ -15,6 +15,15 @@ } ], "type": "paragraph" + }, + { + "content": [ + { + "text": "Grade 8.8.", + "type": "text" + } + ], + "type": "paragraph" } ], "type": "listItem" diff --git a/corpus/round-trip/commonmark-subset/ordered-list-order.md b/corpus/round-trip/commonmark-subset/ordered-list-order.md index 163e5a2..c36ddfc 100644 --- a/corpus/round-trip/commonmark-subset/ordered-list-order.md +++ b/corpus/round-trip/commonmark-subset/ordered-list-order.md @@ -1,4 +1,6 @@ 9. Bolt M8 + + Grade 8.8. 10. Nut M8 Zinc-plated. diff --git a/spec/flavour.md b/spec/flavour.md index 40cfbf7..537931e 100644 --- a/spec/flavour.md +++ b/spec/flavour.md @@ -38,11 +38,12 @@ normalizes to it through the round-trip. CommonMark autolink (absolute URI). - Paragraphs on one line — no soft wrapping; a soft line break in input becomes a single space. - Entity references in input decode to their characters; output backslash-escapes only where text - would otherwise parse as syntax: escape the leading delimiter of a construct that would - otherwise open, re-scan from there, and repeat — with the opener literal the closer parses as - text, so `*not emphasis*` is `\*not emphasis*`, one backslash. -- Blocks separated by one blank line, no trailing whitespace, single trailing newline; a document - with no blocks is the empty string. + would otherwise parse as syntax, scanning the assembled line rather than each text node: escape + the leading delimiter of a construct that would otherwise open, re-scan from there, and repeat — + with the opener literal the closer parses as text, so `*not emphasis*` is `\*not emphasis*`, one + backslash. +- Blocks separated by one blank line, no trailing whitespace outside a code block's content, + single trailing newline; a document with no blocks is the empty string. ## Directives diff --git a/todo.md b/todo.md index 6d7fd5b..d5b6a80 100644 --- a/todo.md +++ b/todo.md @@ -23,12 +23,22 @@ detail is settled at its own milestone. - [ ] **1d — Corpus start** (§10): checked-in fixtures per spec'd node, in `corpus/`, one directory per contract kind (`corpus/README.md`). **Blocked on the maintainer** (§15), not to be guessed: Canonical form has no totality - guard — a `paragraph`, `heading`, `blockquote` or `codeBlock` carrying a `localId`, or a - `blockquote` carrying marks, has no spelling that keeps it, and picking one (directive - sections for the six CommonMark block nodes, or the opaque carry) is a permanent format - decision (§8). Its three collision sites stay out of the corpus until then: an `orderedList` - starting at 1, a `codeBlock` whose info string is empty, and `media` with an empty `alt` — - each a choice between the absent attribute and the empty value. + guard. Per `@atlaskit/adf-schema` 57.1.0 every block node it spells — `blockquote`, + `bulletList`, `codeBlock`, `heading`, `listItem`, `orderedList`, `paragraph`, `rule` — + carries a `localId` with no spelling, `codeBlock` also `hideLineNumbers`, `uniqueId` and + `wrap`, `blockquote` also marks, and `hardBreak` `text` and `localId` with no section for + the carry fallback to reach. Picking one (directive sections for those nodes, or the opaque + carry) is a permanent format decision (§8). Three collision sites are held out of the corpus + meanwhile, each a choice between the absent attribute and the empty value: a `codeBlock` + whose info string is empty and `media` with an empty `alt`, which one "exactly that shape" + rule — as the CommonMark image already uses — could settle together, and an `orderedList` + starting at 1, independent of the totality answer since `order: 9` keeps the markdown form + either way. **Also blocked**: the link rule covers destination spaces only, so two shapes + break §2 silently — href `https://example.com/a)b` emits `[t](https://example.com/a)b)`, + read back as href `…/a` plus literal `b)`; title `He said "hi"` emits + `[t](u "He said "hi"")`, which holds no title. Two defensible spellings each — angle + brackets or a backslash escape, and for titles `'…'` or `(…)` besides — so §8 leaves the + pick here. - [ ] **1d1 — The CommonMark subset**: blockquote, bulletList, codeBlock, heading, orderedList, paragraph, rule, listItem, hardBreak, text, code spans, and the `code`, `em`, `link`, `strike` and `strong` marks — one mark per text node; nesting is 1d5's. -- 2.52.0