diff --git a/README.md b/README.md index 41f2117..4fa92bc 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ products store as ADF. lossless pair. - **No throws:** every call returns a result; a refusal carries a code from a closed list. - **Runs anywhere:** pure functions with no runtime dependencies, in Node, Deno, Bun and browsers. -- **HTML at `0.2.0`:** `adfToHtml`, `htmlToAdf`, `markdownToHtml`, `htmlToMarkdown`. +- **HTML at `0.3.0`:** `adfToHtml`, `htmlToAdf`, `markdownToHtml`, `htmlToMarkdown`. [The shape](#the-shape) · [Goals](#goals) · [Audience](#audience) · [Plain markdown](#plain-markdown) · [The errors](#the-errors) · [The guarantees](#the-guarantees) · @@ -60,10 +60,10 @@ isAdfDocument(v: unknown): v is AdfDocument adfToPlainMarkdown(doc: AdfDocument): Result plainMarkdownToAdf(markdown: string): Result -adfToHtml(doc: AdfDocument): Result // 0.2.0 -htmlToAdf(html: string): Result // 0.2.0 -markdownToHtml(markdown: string): Result // 0.2.0 -htmlToMarkdown(html: string): Result // 0.2.0 +adfToHtml(doc: AdfDocument): Result // 0.3.0 +htmlToAdf(html: string): Result // 0.3.0 +markdownToHtml(markdown: string): Result // 0.3.0 +htmlToMarkdown(html: string): Result // 0.3.0 ``` `Result` is `{ ok: true; value: T } | { ok: false; error: ConvertError }` — nothing throws. @@ -180,7 +180,7 @@ UTF-16 code unit, a JavaScript string index rather than a codepoint or a byte of or before the refusal — currently the start of the line the enclosing block begins on; a later minor may narrow that, never widen it. For `not-a-string` it is `{ line: 1, offset: 0 }`. -Parsing — `markdownToAdf` and `plainMarkdownToAdf`, and `htmlToAdf` at `0.2.0`: +Parsing — `markdownToAdf` and `plainMarkdownToAdf`, and `htmlToAdf` at `0.3.0`: | Code | Fires when | What you can do | | --- | --- | --- | @@ -188,11 +188,11 @@ Parsing — `markdownToAdf` and `plainMarkdownToAdf`, and `htmlToAdf` at `0.2.0` | `malformed-pipe-table` | a pipe row that is no pipe table — a missing or ragged `---` delimiter row, an alignment colon in it, or a row not opening with a pipe | open every row with a pipe and give the delimiter row the header's cell count; to keep the lines literal text instead, escape the leading pipe of every one — escaping a single row leaves the next to open a fresh table and fail the same way | | `not-a-string` | the value handed in is not a string, such as `null`, a number or a `String` object | pass the input as a string, `String(value)` for a `String` object; the message names the value's type | | `unknown-directive-name` | a directive whose name is no node or mark this version spells | check the name in `spec/flavour.md`, or escape the prefix as `\!adf:`; the spelling itself is well formed, so a later minor may give the name meaning | -| `unmappable-html` | the input holds an HTML construct the documented element set does not map, a comment and a processing instruction among them — at this version that is every raw HTML construct in markdown, the element set landing at `0.2.0` | remove the construct, or write what it holds in the lossless flavour | +| `unmappable-html` | the input holds an HTML construct the documented element set does not map, a comment and a processing instruction among them — at this version that is every raw HTML construct in markdown, the element set landing at `0.3.0` | remove the construct, or write what it holds in the lossless flavour | | `unmappable-image` | an image sits inside other content that is not another image's description, or carries a title | give the image a paragraph of its own and drop the title | | `unspellable-whitespace` | an `emoji`, `mention` or `status` directive's content slot spells a newline or a carriage return — ` `, ` `, `!adf:text{text="\n"}` | replace it with a space, or write the node as `!adf:carry` — an inline directive never spans lines | -Emitting — `adfToMarkdown`, and `adfToHtml` at `0.2.0`: +Emitting — `adfToMarkdown`, and `adfToHtml` at `0.3.0`: | Code | Fires when | What you can do | | --- | --- | --- | @@ -217,7 +217,7 @@ Serves Goals 1, 4 and 5. empty `attrs`, `content` or `marks` and `-0` included, and unknown node types carried opaquely ([`docs/decisions.md`](https://github.com/larvit/adf-codec/blob/main/docs/decisions.md#unknown-nodes-ride-the-carry)). - Markdown this library reads, and markdown it writes, means what the CommonMark spec says; from - `0.2.0`, well-formed HTML means what the HTML standard says, read or written. The bullets below + `0.3.0`, well-formed HTML means what the HTML standard says, read or written. The bullets below name every exception. - Plain CommonMark is valid input to `markdownToAdf` apart from the raw HTML `unmappable-html` names, with four carve-outs — literal text matching directive, pipe-table or strikethrough syntax, @@ -247,7 +247,7 @@ Serves Goals 1, 4 and 5. loops forever on a string, or on a value `JSON.parse` or `structuredClone` builds. - The emitted formats are semver surface ([`docs/decisions.md`](https://github.com/larvit/adf-codec/blob/main/docs/decisions.md#the-formats-are-api)). -- **`0.2.0`** — `htmlToAdf(adfToHtml(doc))` deep-equals `doc`; fidelity HTML cannot express rides +- **`0.3.0`** — `htmlToAdf(adfToHtml(doc))` deep-equals `doc`; fidelity HTML cannot express rides `data-*` attributes. Foreign HTML maps a documented element set, which markdown's raw HTML reads through as well, and a construct outside it is an error; well-formed HTML only — no tag-soup recovery. diff --git a/docs/decisions.md b/docs/decisions.md index 2373936..5641cfa 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -133,7 +133,7 @@ drop of content: `
Title…
` is an `expand` titled by its summary, a `nestedExpand` inside another; an empty one is refused, since `expand` requires content. A `style` -attribute is not read at `0.2.0`: the `textColor` and `backgroundColor` it could reach cost more +attribute is not read at `0.3.0`: the `textColor` and `backgroundColor` it could reach cost more than they buy. `plainMarkdownToAdf` reads through `markdownToAdf`'s parser, so it takes the same set. diff --git a/todo.md b/todo.md index 326dee5..f63ea5e 100644 --- a/todo.md +++ b/todo.md @@ -24,46 +24,88 @@ | ID | Release | Exempt | Item | R | S | A | G | Goals | Score | |---|---|---|---|---|---|---|---|---|---| -| 7 | 0.2.0 | | **Ship HTML: `adfToHtml`, `htmlToAdf`, and `markdownToHtml` / `htmlToMarkdown` composed through ADF.** | 6 | 9 | 9 | 9 | 2, 4 | 25.8 | -| 45 | 0.2.0 | | **Replace `isAdfDocument` with a reader returning `Result`.** | 2 | 3 | 6 | 8 | 1 | 25.2 | -| 6 | 0.2.0 | decision | **Specify the HTML dialect.** | 2 | 6 | 7 | 8 | 2, 4 | 24.7 | | 64 | 0.2.0 | defect | **Read back unchanged on V8 every JSON key the emitter writes, with a fixture whose key holds `\`, `"` or a control character.** | 4 | 4 | 6 | 8 | 1, 8 | 23.0 | -| 77 | 0.2.0 | decision | **Read the input once into a plain copy, so an accessor property or a throwing Proxy yields `not-an-adf-document` and every call returns a result.** | 4 | 4 | 5 | 9 | 1 | 23.0 | -| 71 | 0.2.0 | | **Add a README table comparing the package with the other ADF converters, each cell checked against that package's own docs or behaviour.** | 1 | 3 | 6 | 7 | 3 | 21.2 | -| 43 | 0.2.0 | decision | **Give each markdown input its own reader, strict to its own standard.** | 6 | 7 | 8 | 9 | 4, 5 | 20.3 | -| 80 | 0.2.0 | | **Name the node's path and the attribute key when `not-an-adf-document` refuses a node inside the document.** | 2 | 3 | 5 | 6 | 1 | 19.2 | -| 68 | 0.2.0 | | **Carry a non-text inline node holding `content` or `text`, a block holding `text`, and a leaf block holding `content`.** | 3 | 3 | 4 | 7 | 1 | 18.2 | -| 75 | 0.2.0 | | **Make the generated-markdown property write block carries as `adf:` fences, canonical and hostile.** | 2 | 2 | 3 | 7 | 1 | 17.5 | | 65 | 0.2.0 | defect | **Read an image not alone in its paragraph as its alt text linked to its URL, and a titled image alone in its paragraph as the image captioned with its title.** | 5 | 5 | 7 | 7 | 4, 5, 7 | 17.1 | -| 81 | 0.2.0 | | **Fail the gate unless the README's error tables list exactly the codes `ConvertErrorCode` holds.** | 1 | 2 | 5 | 5 | 3 | 16.3 | | 49 | 0.2.0 | | **Read a list whose bullet or ordered delimiter changes as two lists, in both readers, and retire `!adf:listBreak`.** | 4 | 5 | 5 | 8 | 4, 5 | 15.5 | -| 46 | 0.2.0 | | **Publish the bundle size in the README, failing the release pipeline when it drifts.** | 2 | 4 | 5 | 5 | 3, 8 | 14.8 | -| 61 | 0.2.0 | decision | **Have the emitter ask the inline reader how a line reads back, in place of `line-escaping.ts` predicting it.** | 6 | 8 | 3 | 8 | 1 | 14.0 | -| 72 | 0.2.0 | decision | **Re-create `gitea.larvit.se/larvit/adf-codec` as a read-only pull mirror of GitHub, its issues and Actions off and its description pointing at GitHub.** | 2 | 1 | 3 | 6 | 3 | 13.1 | -| 52 | 0.2.0 | | **Spell `colwidth` as a comma list, `colwidth="340,420"`.** | 3 | 3 | 5 | 6 | 6 | 11.5 | -| 51 | 0.2.0 | | **Match a reference label to its definition under Unicode case folding.** | 2 | 2 | 2 | 7 | 4, 5 | 10.9 | -| 60 | 0.2.0 | decision | **Collect the questions `parse/` asks `emit/` into one named module.** | 3 | 4 | 2 | 6 | 2 | 10.7 | -| 59 | 0.2.0 | decision | **Group the directive grammar into `src/markdown/directive/`, move `Read` to `result.ts`, and move `Flavour` to `markdown/flavour.ts`.** | 3 | 5 | 2 | 6 | 2 | 10.4 | -| 53 | 0.2.0 | | **Put a block's `marks` spelling to the writer panel and adopt its pick.** | 4 | 5 | 5 | 6 | 6 | 10.0 | -| 62 | 0.2.0 | decision | **Move the plain flavour's reading out of `parse/` and its writing out of `emit/` into `markdown/plain/`, so `emit/` no longer imports `plain/`.** | 4 | 5 | 2 | 6 | 2 | 9.4 | | 50 | 0.2.0 | | **Read `[](/url)` and `[]()` as CommonMark's empty link.** | 4 | 4 | 3 | 6 | 4, 5, 7 | 9.0 | -| 73 | 0.3.0 | | **Announce the package where someone needing an ADF converter already reads: JRACLOUD-77436, the Atlassian developer community and Stack Overflow's ADF-to-markdown questions.** | 1 | 2 | 6 | 8 | 3 | 23.0 | -| 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 | 3 | 21.0 | -| 38 | 0.3.0 | | **Spell a lone surrogate in a text node so it survives a UTF-8 encode.** | 2 | 2 | 4 | 7 | 1 | 19.5 | -| 74 | 0.3.0 | | **Keep the release path publishing after npm retires bypass-2FA tokens.** | 4 | 4 | 8 | 3 | 3 | 15.7 | -| 34 | 0.3.0 | | **Read emphasis flanking by the whole character beside an astral symbol.** | 2 | 3 | 3 | 6 | 4, 5 | 11.3 | -| 31 | 0.3.0 | | **Make the branch-coverage figure repeat across runs of an unchanged tree.** | 2 | 3 | 3 | 4 | 1 | 11.2 | -| 42 | 0.3.0 | defect | **Trim a text leaf's trailing blanks in linear time.** | 1 | 2 | 5 | 9 | 9 | 10.5 | -| 33 | 0.3.0 | defect | **Emit a line in time linear in its mark runs, in `adfToMarkdown` and `adfToPlainMarkdown`.** | 4 | 5 | 6 | 9 | 9 | 8.7 | -| 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 | -| 76 | 0.3.0 | principle | **State the files the package does not ship once, so the build, the lint ceiling and the coverage exclusions cannot drift apart.** | 2 | 2 | 1 | 3 | 1 | 5.5 | -| 82 | 0.3.0 | principle | **Give the directive tests and fixtures random UUID v7 ids in place of `a-1`, `a`, `x` and `h`.** | 1 | 3 | 1 | 2 | 1 | 4.2 | -| 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 | 8 | 0.6 | -| 58 | 0.3.0 | principle | **Port `ci.sh`, `publish.sh` and `docker-runner.sh` to standalone Python scripts.** | 4 | 6 | 1 | 2 | 8 | -2.6 | -| 8 | 0.4.0 | | **Ship a CLI.** | 3 | 7 | 7 | 6 | 3 | 18.6 | +| 7 | 0.3.0 | | **Ship HTML: `adfToHtml`, `htmlToAdf`, and `markdownToHtml` / `htmlToMarkdown` composed through ADF.** | 6 | 9 | 9 | 9 | 2, 4 | 25.8 | +| 45 | 0.3.0 | | **Replace `isAdfDocument` with a reader returning `Result`.** | 2 | 3 | 6 | 8 | 1 | 25.2 | +| 6 | 0.3.0 | decision | **Specify the HTML dialect.** | 2 | 6 | 7 | 8 | 2, 4 | 24.7 | +| 77 | 0.3.0 | decision | **Read the input once into a plain copy, so an accessor property or a throwing Proxy yields `not-an-adf-document` and every call returns a result.** | 4 | 4 | 5 | 9 | 1 | 23.0 | +| 71 | 0.3.0 | | **Add a README table comparing the package with the other ADF converters, each cell checked against that package's own docs or behaviour.** | 1 | 3 | 6 | 7 | 3 | 21.2 | +| 43 | 0.3.0 | decision | **Give each markdown input its own reader, strict to its own standard.** | 6 | 7 | 8 | 9 | 4, 5 | 20.3 | +| 80 | 0.3.0 | | **Name the node's path and the attribute key when `not-an-adf-document` refuses a node inside the document.** | 2 | 3 | 5 | 6 | 1 | 19.2 | +| 68 | 0.3.0 | | **Carry a non-text inline node holding `content` or `text`, a block holding `text`, and a leaf block holding `content`.** | 3 | 3 | 4 | 7 | 1 | 18.2 | +| 75 | 0.3.0 | | **Make the generated-markdown property write block carries as `adf:` fences, canonical and hostile.** | 2 | 2 | 3 | 7 | 1 | 17.5 | +| 81 | 0.3.0 | | **Fail the gate unless the README's error tables list exactly the codes `ConvertErrorCode` holds.** | 1 | 2 | 5 | 5 | 3 | 16.3 | +| 46 | 0.3.0 | | **Publish the bundle size in the README, failing the release pipeline when it drifts.** | 2 | 4 | 5 | 5 | 3, 8 | 14.8 | +| 61 | 0.3.0 | decision | **Have the emitter ask the inline reader how a line reads back, in place of `line-escaping.ts` predicting it.** | 6 | 8 | 3 | 8 | 1 | 14.0 | +| 72 | 0.3.0 | decision | **Re-create `gitea.larvit.se/larvit/adf-codec` as a read-only pull mirror of GitHub, its issues and Actions off and its description pointing at GitHub.** | 2 | 1 | 3 | 6 | 3 | 13.1 | +| 52 | 0.3.0 | | **Spell `colwidth` as a comma list, `colwidth="340,420"`.** | 3 | 3 | 5 | 6 | 6 | 11.5 | +| 51 | 0.3.0 | | **Match a reference label to its definition under Unicode case folding.** | 2 | 2 | 2 | 7 | 4, 5 | 10.9 | +| 60 | 0.3.0 | decision | **Collect the questions `parse/` asks `emit/` into one named module.** | 3 | 4 | 2 | 6 | 2 | 10.7 | +| 59 | 0.3.0 | decision | **Group the directive grammar into `src/markdown/directive/`, move `Read` to `result.ts`, and move `Flavour` to `markdown/flavour.ts`.** | 3 | 5 | 2 | 6 | 2 | 10.4 | +| 53 | 0.3.0 | | **Put a block's `marks` spelling to the writer panel and adopt its pick.** | 4 | 5 | 5 | 6 | 6 | 10.0 | +| 62 | 0.3.0 | decision | **Move the plain flavour's reading out of `parse/` and its writing out of `emit/` into `markdown/plain/`, so `emit/` no longer imports `plain/`.** | 4 | 5 | 2 | 6 | 2 | 9.4 | +| 73 | 0.4.0 | | **Announce the package where someone needing an ADF converter already reads: JRACLOUD-77436, the Atlassian developer community and Stack Overflow's ADF-to-markdown questions.** | 1 | 2 | 6 | 8 | 3 | 23.0 | +| 9 | 0.4.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 | 3 | 21.0 | +| 38 | 0.4.0 | | **Spell a lone surrogate in a text node so it survives a UTF-8 encode.** | 2 | 2 | 4 | 7 | 1 | 19.5 | +| 74 | 0.4.0 | | **Keep the release path publishing after npm retires bypass-2FA tokens.** | 4 | 4 | 8 | 3 | 3 | 15.7 | +| 34 | 0.4.0 | | **Read emphasis flanking by the whole character beside an astral symbol.** | 2 | 3 | 3 | 6 | 4, 5 | 11.3 | +| 31 | 0.4.0 | | **Make the branch-coverage figure repeat across runs of an unchanged tree.** | 2 | 3 | 3 | 4 | 1 | 11.2 | +| 42 | 0.4.0 | defect | **Trim a text leaf's trailing blanks in linear time.** | 1 | 2 | 5 | 9 | 9 | 10.5 | +| 33 | 0.4.0 | defect | **Emit a line in time linear in its mark runs, in `adfToMarkdown` and `adfToPlainMarkdown`.** | 4 | 5 | 6 | 9 | 9 | 8.7 | +| 56 | 0.4.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 | +| 76 | 0.4.0 | principle | **State the files the package does not ship once, so the build, the lint ceiling and the coverage exclusions cannot drift apart.** | 2 | 2 | 1 | 3 | 1 | 5.5 | +| 82 | 0.4.0 | principle | **Give the directive tests and fixtures random UUID v7 ids in place of `a-1`, `a`, `x` and `h`.** | 1 | 3 | 1 | 2 | 1 | 4.2 | +| 57 | 0.4.0 | principle | **Make each `ci.sh` leg build what it reads, so one leg run alone tests the current tree.** | 2 | 3 | 1 | 3 | 8 | 0.6 | +| 58 | 0.4.0 | principle | **Port `ci.sh`, `publish.sh` and `docker-runner.sh` to standalone Python scripts.** | 4 | 6 | 1 | 2 | 8 | -2.6 | +| 8 | 0.5.0 | | **Ship a CLI.** | 3 | 7 | 7 | 6 | 3 | 18.6 | ## Details +### 64. Read back unchanged on V8 every JSON key the emitter writes, with a fixture whose key holds `\`, `"` or a control character. + +`property-harness.ts` strips `\`, `"` and control characters from generated JSON keys, citing V8's +`JSON.parse` returning a wrong key for an escaped backslash. The library reads `json` attributes +(`directive-syntax.ts`) and both carries (`opaque-carry.ts`) through that `JSON.parse`, so on Node, +Deno and Chrome the round-trip would refuse its own output or read back a different key; the fixture +tells which. Confirm with a fixture first; then read JSON with our own parser or record the gap with +an ending item. Found by the README-goals audit, 2026-10-03. + +### 65. Read an image not alone in its paragraph as its alt text linked to its URL, and a titled image alone in its paragraph as the image captioned with its title. + +Both are refused today with `unmappable-image`: 14 examples in +`corpus/commonmark-spec/refusals.json`, a bot's `See ![diagram](url) here` among them. `mediaInline` +takes only Atlassian media ids, never a URL. An image not alone in its paragraph reads as its alt +text, linked to its URL. Its title becomes the link's `title`. An empty alt text reads as the URL, +as item 50 reads an empty link. A titled image alone in its paragraph reads as the image, with its +title as a `caption` (the maintainer, 2026-10-04). By Goal 5, the lossless flavour reads both as +CommonMark does. An image inside a link: `[![moon](moon.jpg)](/uri)` (spec example 517). Alone in +its paragraph, it reads as the image with a `link` mark to `/uri`, which ADF's `media` takes. Not +alone in its paragraph, the image reads as its alt text linked to `/uri`, and its URL drops. A +writer panel chose this reading 3 of 3 (2026-10-04), by Goal 6, which outranks Goal 7. + +### 49. Read a list whose bullet or ordered delimiter changes as two lists, in both readers, and retire `!adf:listBreak`. + +Lands after item 43. Today `- a` then `+ b`, or `1.` then `1)`, reads as one list; CommonMark reads +two (spec examples 301 and 302). Goal 5 settles it for the lossless flavour too: CommonMark spells +adjacent lists by changing the marker, so `adfToMarkdown` and `adfToPlainMarkdown` alternate `-` and +`*` between adjacent bullet lists and `.` and `)` between adjacent ordered lists, and +`!adf:listBreak` retires. `*` is the maintainer's pick (2026-10-04). Breaking, so it ships beside +item 43: `MIGRATION.md`'s Readings and Spellings tables gain their rows. Examples 301 and 302 lose +their `pending` exceptions, and the spelling leaves the README's "Four CommonMark spellings" bullet, +which counts one fewer. + +### 50. Read `[](/url)` and `[]()` as CommonMark's empty link. + +Both stay literal text today (spec examples 484 and 487). ADF holds no empty text node to carry a +link mark, so a link whose text is empty takes its URL as its text (the maintainer, 2026-10-04): +`[](/url)` reads as `/url` linked to `/url`. `[]()` has no URL to show; the chunk settles it by Goals +4 and 7. Breaking, so it ships beside item 43: `MIGRATION.md`'s Readings table gains its row. Its +`pending` exceptions go, and its spelling leaves the README's "Four CommonMark spellings" bullet, +which counts one fewer. + ### 7. Ship HTML: `adfToHtml`, `htmlToAdf`, and `markdownToHtml` / `htmlToMarkdown` composed through ADF. Lands after items 6, 59, 60, 61 and 62. The CommonMark spec suite also runs against @@ -83,15 +125,6 @@ Element-by-element mapping, the `data-*` fidelity scheme, the opaque-carry form, foreign-element set `htmlToAdf` accepts — the set `markdownToAdf` shares (`spec/flavour.md` §Raw HTML in input). The set sorts per `docs/decisions.md` §Foreign HTML sorts three ways. -### 64. Read back unchanged on V8 every JSON key the emitter writes, with a fixture whose key holds `\`, `"` or a control character. - -`property-harness.ts` strips `\`, `"` and control characters from generated JSON keys, citing V8's -`JSON.parse` returning a wrong key for an escaped backslash. The library reads `json` attributes -(`directive-syntax.ts`) and both carries (`opaque-carry.ts`) through that `JSON.parse`, so on Node, -Deno and Chrome the round-trip would refuse its own output or read back a different key; the fixture -tells which. Confirm with a fixture first; then read JSON with our own parser or record the gap with -an ending item. Found by the README-goals audit, 2026-10-03. - ### 77. Read the input once into a plain copy, so an accessor property or a throwing Proxy yields `not-an-adf-document` and every call returns a result. Lands with item 45, in one chunk: item 45's reader returns the copy. The maintainer's call, @@ -153,36 +186,12 @@ type", that is an ordinary code language, so the property reaches `readCarriedBl where a random edit lands in a canonical document. Write `adf:` and `adf:` fences, canonical and hostile. Found by the technical-principles audit, 2026-10-04. -### 65. Read an image not alone in its paragraph as its alt text linked to its URL, and a titled image alone in its paragraph as the image captioned with its title. - -Both are refused today with `unmappable-image`: 14 examples in -`corpus/commonmark-spec/refusals.json`, a bot's `See ![diagram](url) here` among them. `mediaInline` -takes only Atlassian media ids, never a URL. An image not alone in its paragraph reads as its alt -text, linked to its URL. Its title becomes the link's `title`. An empty alt text reads as the URL, -as item 50 reads an empty link. A titled image alone in its paragraph reads as the image, with its -title as a `caption` (the maintainer, 2026-10-04). By Goal 5, the lossless flavour reads both as -CommonMark does. An image inside a link: `[![moon](moon.jpg)](/uri)` (spec example 517). Alone in -its paragraph, it reads as the image with a `link` mark to `/uri`, which ADF's `media` takes. Not -alone in its paragraph, the image reads as its alt text linked to `/uri`, and its URL drops. A -writer panel chose this reading 3 of 3 (2026-10-04), by Goal 6, which outranks Goal 7. - ### 81. Fail the gate unless the README's error tables list exactly the codes `ConvertErrorCode` holds. `src/result.test.ts` checks the union against the call sites only, so a code added or removed leaves the README's tables a false contract under a green gate. Assert that the tables list exactly the union. Found by the technical-principles audit of item 78, 2026-10-05. -### 49. Read a list whose bullet or ordered delimiter changes as two lists, in both readers, and retire `!adf:listBreak`. - -Lands after item 43. Today `- a` then `+ b`, or `1.` then `1)`, reads as one list; CommonMark reads -two (spec examples 301 and 302). Goal 5 settles it for the lossless flavour too: CommonMark spells -adjacent lists by changing the marker, so `adfToMarkdown` and `adfToPlainMarkdown` alternate `-` and -`*` between adjacent bullet lists and `.` and `)` between adjacent ordered lists, and -`!adf:listBreak` retires. `*` is the maintainer's pick (2026-10-04). Breaking, so it ships beside -item 43: `MIGRATION.md`'s Readings and Spellings tables gain their rows. Examples 301 and 302 lose -their `pending` exceptions, and the spelling leaves the README's "Four CommonMark spellings" bullet, -which counts one fewer. - ### 46. Publish the bundle size in the README, failing the release pipeline when it drifts. Lands after item 7, which changes it. The quantity is what a consumer downloads and loads: the @@ -249,15 +258,6 @@ the `inlineLeaves` use. Writing: `spellPlainBlock`, `quotedUnder`, `tryTaskList` `plain/conventions.ts`, which `emit/` imports them from; item 59 moves `Flavour`. Every comprehension reader on 2026-10-03 named the plain flavour's spread across three directories. -### 50. Read `[](/url)` and `[]()` as CommonMark's empty link. - -Both stay literal text today (spec examples 484 and 487). ADF holds no empty text node to carry a -link mark, so a link whose text is empty takes its URL as its text (the maintainer, 2026-10-04): -`[](/url)` reads as `/url` linked to `/url`. `[]()` has no URL to show; the chunk settles it by Goals -4 and 7. Breaking, so it ships beside item 43: `MIGRATION.md`'s Readings table gains its row. Its -`pending` exceptions go, and its spelling leaves the README's "Four CommonMark spellings" bullet, -which counts one fewer. - ### 73. Announce the package where someone needing an ADF converter already reads: JRACLOUD-77436, the Atlassian developer community and Stack Overflow's ADF-to-markdown questions. Lands after `0.2.0` is published with item 71, so each post links the GitHub repository, the