Emitter 2a: the corpus runner, the canonical serializer and the CommonMark subset
CI / gate (push) Successful in 5s

This commit is contained in:
Mikael Göransson
2026-08-24 15:36:05 +02:00
parent 288ee1697c
commit 0728ea1cfb
16 changed files with 976 additions and 35 deletions
+31 -24
View File
@@ -20,7 +20,7 @@ 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 fixtures per spec'd node, in `corpus/`, one
- [x] **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. Per `@atlaskit/adf-schema` 57.1.0 every block node it spells — `blockquote`,
@@ -37,8 +37,8 @@ detail is settled at its own milestone.
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. **Also blocked**: block separation is unstated for a CommonMark block beside a
brackets or a backslash escape, and for titles `'…'` or `(…)` besides — so §8 leaves
the pick here. **Also blocked**: block separation is unstated for a CommonMark block beside a
directive block in a container body — an `expand` whose content is `paragraph` "A" then a
`panel` (`panelType` `warning`) holding "B" spells `A` and `:::panel warning` either on
consecutive lines or with a blank line between. Two defensible spellings, so §8 leaves the
@@ -52,28 +52,35 @@ detail is settled at its own milestone.
- [x] **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 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),
- [ ] **2 — `adfToMarkdown`.** First real code. Each sub-item turns one corpus directory green;
the two that have no fixtures yet write them in the same chunk, tests first (§10).
- [ ] **2a — The runner and the CommonMark subset.** The corpus runner: walk
`corpus/round-trip/`, assert `adfToMarkdown` emits each `.md` byte for byte. 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. `commonmark-subset/` green.
- [ ] **2b — Block nodes.** `block-nodes/` green.
- [ ] **2c — Inline nodes and marks.** `inline-nodes/` green.
- [ ] **2d — The opaque carry** (§3). Fixtures and emitter together, into
`corpus/round-trip/opaque-carry/`: an unknown node in both positions, the reserved `adf`
info string, and the `codeBlock` whose language is `adf`.
- [ ] **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`).
- [ ] **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, 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. The CommonMark spec suite runs against it from here (§10), and
`corpus/errors/` gains the error each fixture must return (1d7).
quoted attribute value, documents combining nodes rather than isolating one, and a
paragraph line inside a container body shaped like a closing fence (`:::`, `::: x`).
- [ ] **3 — `markdownToAdf`.** The CommonMark parser is the largest single component; split it
into sub-items before starting (§15). Fixtures land with the code that reads them:
`corpus/normalization/` (setext, indented code, loose lists, `*`/`+` bullets, entity
references, soft wraps — one-way, the markdown not canonical) and `corpus/errors/` (a
markdown input per named error — malformed directives, the image gap, a claimed pipe-table
line that does not parse, the content slot, raw HTML with no mapping — each with the error
it must return). 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. The CommonMark spec suite runs
against it from here (§10).
- [ ] **4 — Round-trip property tests** over the corpus, both ways — the thing that proves 2 and
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