Spell every document the emitter refused for a carriage return, a null character, a code-span line start or an empty text node #160

Merged
lilleman merged 11 commits from spell-every-refusal into main 2026-10-04 21:13:54 +02:00
3 changed files with 17 additions and 4 deletions
Showing only changes of commit a0e4247cfd - Show all commits
+1 -1
View File
@@ -71,7 +71,7 @@ markdown it named on parse refuses with the code the table gives.
| a link whose `href` or `title` no CommonMark escape spells, on emit | `unspellable-link` | spells `!adf:link[text]{attrs}` |
| a carriage return in text, on emit | `unspellable-character` | spells `
` |
| a null character in text, on emit | `unspellable-character` | spells `!adf:text{text="\u0000"}` |
| a carriage return or null character in a code block or code span, on emit | `unspellable-character` | rides the carry |
| a carriage return or null character in a code block, or a null character in a code span, on emit | `unspellable-character` | rides the carry |
| a paragraph line opening with a code span of three or more backticks, on emit | `unspellable-line-start` | spells the code span |
| a block directive whose CommonMark spelling holds one of the above, on parse | `unspellable-character`, `unspellable-line-start` | `unsupported-node-shape`; write the CommonMark spelling |
| an `emoji`, `mention` or `status` whose `text` holds a line ending or a null character, on emit | `unspellable-whitespace`, `unspellable-character` | rides the inline carry |
+1 -1
View File
@@ -196,7 +196,7 @@ emit refuses:
| Code | Fires when | What you can do |
| --- | --- | --- |
| `unsupported-nesting-depth` | blocks, marks, an attribute's JSON or a carried node's JSON nest past 500 levels | keep the ADF and pass the document over, or show it read-only; flatten the input where you are the one who wrote it |
| `unsupported-node-shape` | parsing: markdown spells a node with an attribute, value, argument or body its type does not take, or without one it needs, writes as a directive a node or mark the lossless flavour spells as CommonMark, or puts a reserved directive out of place: `!adf:textBreak{}` or `!adf:listBreak` parting nothing, `!adf:doc` anywhere but as the whole document. Emitting: a node "Not every document converts back" below names | fix what the message names; `spec/flavour.md` lists every type's attributes and body |
| `unsupported-node-shape` | parsing: markdown spells a node with an attribute, value, argument or body its type does not take, or without one it needs, writes as a directive a node or mark the lossless flavour spells as CommonMark, or puts a reserved directive out of place: `!adf:textBreak{}` or `!adf:listBreak` parting nothing, `!adf:doc` anywhere but as the whole document. Emitting: a node listed under "Not every document converts back" below | fix what the message names; `spec/flavour.md` lists every type's attributes and body |
## The guarantees
+15 -2
View File
@@ -6,7 +6,7 @@
`Bar = 9`
`Next ID = 69`
`Next ID = 70`
| Goal | W |
|---|---|
@@ -51,6 +51,7 @@
| 33 | 0.3.0 | | **Emit a line in time linear in its mark runs, in `adfToMarkdown` and `adfToPlainMarkdown`.** | 4 | 5 | 6 | 9 | 8 | 10.7 |
| 46 | 0.3.0 | | **Publish the bundle size in the README, failing the release pipeline when it drifts.** | 2 | 4 | 5 | 5 | 7, 9 | 10.3 |
| 9 | 0.3.0 | | **Ship an online sandbox: a web page with two textboxes converting between ADF and markdown on the library's browser build.** | 2 | 6 | 6 | 8 | 9 | 10.3 |
| 69 | 0.3.0 | question | **Decide whether a text node's `content` is content `adfToPlainMarkdown` keeps, and keep or document it.** | 2 | 2 | 3 | 5 | 6 | 7.9 |
| 56 | 0.3.0 | principle | **Give each piece of `blocks.ts`'s block-walk state and `inline-content.ts`'s `Scan` one owner that returns what it changes.** | 4 | 5 | 2 | 4 | 1 | 6.8 |
| 57 | 0.3.0 | principle | **Make each `ci.sh` leg build what it reads, so one leg run alone tests the current tree.** | 2 | 3 | 1 | 3 | 7 | 1.2 |
| 58 | 0.3.0 | principle | **Port `ci.sh`, `publish.sh` and `docker-runner.sh` to standalone Python scripts.** | 4 | 6 | 1 | 2 | 7 | -2.2 |
@@ -249,7 +250,9 @@ measured one.
`adfToMarkdown` spends 23 s on one paragraph of 2000 × `un` plus `**-r**`: each run its flanking
cannot spell re-emits the whole line before riding the carry, quadratic in the runs, and the plain
reduction's `spellableLine` drops one mark per re-emit the same way. Make both linear.
reduction's `spellableLine` drops one mark per re-emit the same way. An inline node whose
attributes no spelling writes re-emits the line the same way before riding the carry. Make all
three linear.
### 46. Publish the bundle size in the README, failing the release pipeline when it drifts.
@@ -261,6 +264,16 @@ or report the unminified gzip). The figure lands in README §The package beside
dependencies" claim. Measured today, unminified: tarball 60.4 kB, unpacked 221.5 kB, JS gzipped 45.6
kB.
### 69. Decide whether a text node's `content` is content `adfToPlainMarkdown` keeps, and keep or document it.
`nodeLeaves` (`plain/inline-reduction.ts`) keeps only a text node's `text`, so
`{ text: 'a', content: [{ text: 'kept', type: 'text' }] }` writes `a`, though `toEditorNormal`
keeps that content (`plain/editor-normal.ts`) and an unknown node's children keep their text.
Atlassian's schema allows no `content` on a text node, so no Atlassian reader saw it; an
export/indexing tool fed a looser producer loses searchable text. Goal 6 reads two ways here: keep
the children's text as an unknown node's, or name the drop in the README's plain section. Found by
the product-owner review of item 55, 2026-10-04.
### 56. Give each piece of `blocks.ts`'s block-walk state and `inline-content.ts`'s `Scan` one owner that returns what it changes.
Technical principle "One owner per value": the `Walk` record passes through twelve functions that