Compare commits

...

6 Commits

7 changed files with 65 additions and 51 deletions
+27 -23
View File
@@ -42,8 +42,8 @@ adjacent text nodes, an empty `attrs`, `content` or `marks`, and `-0`. Neither s
CommonMark's spelling stays wherever a document holds none of those shapes. The plain reader builds CommonMark's spelling stays wherever a document holds none of those shapes. The plain reader builds
what is written, as `markdownToAdf` does. Only the plain writer is lossy: its reduction reads and what is written, as `markdownToAdf` does. Only the plain writer is lossy: its reduction reads and
writes editor-normal ADF — adjacent text nodes of identical marks and no attributes merged, `-0` as writes editor-normal ADF — adjacent text nodes of identical marks and no attributes merged, `-0` as
`0`, and an empty `attrs`, `content` or `marks` the absent key, but the doc's `content` — so two `0`, and an empty `attrs`, `content` or `marks` the absent key, except the doc's `content`, which
documents the editor holds equal write the same plain markdown. ADF's schema requires — so two documents the editor holds equal write the same plain markdown.
## `!adf:textBreak{}` parts text CommonMark would join ## `!adf:textBreak{}` parts text CommonMark would join
@@ -60,16 +60,16 @@ around the leaf. The grammar: `spec/flavour.md` §Inline nodes, **Adjacent text
spells an empty object or array. spells an empty object or array.
An `attrs`, `content` or `marks` key holding an empty object or array is the reserved key with the An `attrs`, `content` or `marks` key holding an empty object or array is the reserved key with the
bare value `empty`, so an empty pair stays the node holding no content key. A writer panel chose bare value `empty`, so a container opener and closer with nothing between them stays the node
the spelling, 5 of 7. The grammar: `spec/flavour.md` §Directives, **Attributes**, and §Marks. holding no `content` key. A writer panel chose the spelling, 5 of 7. The grammar: `spec/flavour.md`
§Directives, **Attributes**, and §Marks.
## `-0` is spelled `-0` ## `-0` is spelled `-0`
2026-10-03, the maintainer. Goal 1. Valid while JSON's own serialization writes `-0` as `0`. 2026-10-03, the maintainer. Goal 1. Valid while JSON's own serialization writes `-0` as `0`.
`-0` is spelled `-0` wherever the flavour writes a number or a JSON value, since JSON's grammar `-0` is spelled `-0` wherever the flavour writes a number or a JSON value, since JSON's grammar
reads it back as `-0` and no list marker spells the sign. The grammar: `spec/flavour.md` §Block reads it back as `-0`. The grammar: `spec/flavour.md` §Block nodes and §The CommonMark blocks.
nodes and §The CommonMark blocks.
## Empty markdown is a document of no blocks ## Empty markdown is a document of no blocks
@@ -495,15 +495,18 @@ exits 0 on.
2026-10-03, the maintainer. KISS, a technical principle, and its comprehension floor of 7. Valid 2026-10-03, the maintainer. KISS, a technical principle, and its comprehension floor of 7. Valid
while `todo.md` items 59, 60, 61 and 62 are open. while `todo.md` items 59, 60, 61 and 62 are open.
A four-seat comprehension panel scored the project 6 against the floor of 7. The scores are the A four-seat comprehension panel scored the project under the floor of 7. Its round-5 scores
baseline: a later panel may not score lower on any dimension. (2026-10-03) are the baseline: a later panel may not score lower on any dimension.
| Seat | Navigation | Locality | Shape | Self-sufficiency | Overall | | Seat | Navigation | Locality | Shape | Self-sufficiency | Overall |
|---|---|---|---|---|---| |---|---|---|---|---|---|
| Junior | 6.5 | 5.5 | 6 | 5.5 | 6 | | Junior | 6 | 5 | 6 | 5 | 5.5 |
| Mid | 7 | 5.5 | 6 | 5 | 5.5 | | Mid | 6.5 | 5 | 6 | 4.5 | 5.5 |
| Senior | 7 | 5.5 | 6.5 | 5.5 | 6 | | Senior | 6.5 | 5.5 | 6.5 | 5.5 | 6 |
| Architect | 6 | 5.5 | 5.5 | 6 | 6 | | Architect | 6.5 | 5.5 | 6 | 6 | 6 |
Three panels on near-identical code scored overall means of 5.88, 5.75 and 5.75, and a seat moves
±0.5 between runs.
## Properties on a fixed seed ## Properties on a fixed seed
@@ -665,14 +668,15 @@ them. A primitive knowing neither ADF nor a format stays at `src/` root. A const
`spec/flavour.md` draws, and a placement nothing here settles goes beside its only reader, or in `spec/flavour.md` draws, and a placement nothing here settles goes beside its only reader, or in
what both read where there are two. what both read where there are two.
A flavour's own code, both directions included, sits in its own directory: `markdown/plain/`. A flavour's own code, both directions included, sits in its own directory: `markdown/plain/`;
Otherwise each format directory parts into `emit/` (ADF→format) and `parse/` (format→ADF), the rest `todo.md` item 62 moves what still sits in `parse/` and `emit/`. Otherwise each format directory
of it holding what both directions read. A construct's reader lives there beside the regex the parts into `emit/` (ADF→format) and `parse/` (format→ADF), the rest of it holding what both
emitter escapes against, so the two cannot drift; a reader with no emit counterpart goes in directions read. A construct's reader lives there beside the regex the emitter escapes against, so
`parse/`, unless it is part of a construct that side already holds — a grammar stays in one file the two cannot drift; a reader with no emit counterpart goes in `parse/`, unless it is part of a
rather than splitting across the seam. A rule both directions must answer alike — whether a list construct that side already holds — a grammar stays in one file rather than splitting across the
marker interrupts a paragraph — is one function there too, never a copy per direction, however seam. A rule both directions must answer alike — whether a list marker interrupts a paragraph — is
conservative the copy would be. Where the rule is the emitter's own choice, input consults it rather one function there too, never a copy per direction, however conservative the copy would be. Where
than restating it, and that is the only value import `parse/` takes from `emit/` — the rule is the emitter's own choice, input consults it rather than restating it, and that is the
`commonMarkSpelling` and `openingLinkTakesDirective` — so no fixture the emitter writes can be only value import `parse/` takes from `emit/` — `commonMarkSpelling` and `openingLinkTakesDirective`
refused, and a spelling the emitter refuses gives its own error rather than a second name for it. — so no fixture the emitter writes can be refused, and a spelling the emitter refuses gives its own
error rather than a second name for it.
+1 -1
View File
@@ -64,7 +64,7 @@ export function emptyKeys(held: { attrs?: AdfAttributes; content?: AdfNode[]; ma
return keys return keys
} }
// A text node holding no attrs or content key and no empty marks: a format spells it as text, where anything more rides a carry. // A format spells this node as text; anything more rides a carry, save content, which is refused.
export function isBareText(node: AdfNode): boolean { export function isBareText(node: AdfNode): boolean {
return node.type === 'text' && node.attrs === undefined && node.content === undefined && node.marks?.length !== 0 return node.type === 'text' && node.attrs === undefined && node.content === undefined && node.marks?.length !== 0
} }
+1 -1
View File
@@ -327,7 +327,7 @@ function isTextBreak(item: Inline): item is TextBreak {
return 'kind' in item && item.kind === 'textBreak' return 'kind' in item && item.kind === 'textBreak'
} }
// The nodes the items hold, own-marks ones unwrapped and text breaks dropped. // The nodes the items hold, with text breaks dropped.
function adfNodes(items: readonly Inline[]): AdfNode[] { function adfNodes(items: readonly Inline[]): AdfNode[] {
const nodes: AdfNode[] = [] const nodes: AdfNode[] = []
for (const item of items) { for (const item of items) {
@@ -344,3 +344,9 @@ test('writes what the editor-normal form of the document writes', () => {
assert.equal(plain({ content: [], text: 'hi', type: 'futureInline' }), 'hi\n') assert.equal(plain({ content: [], text: 'hi', type: 'futureInline' }), 'hi\n')
assert.equal(plain(paragraph({ marks: [{ attrs: {}, type: 'code' }], text: 'a', type: 'text' }, { marks: [{ type: 'code' }], text: '|b', type: 'text' })), '`a|b`\n') assert.equal(plain(paragraph({ marks: [{ attrs: {}, type: 'code' }], text: 'a', type: 'text' }, { marks: [{ type: 'code' }], text: '|b', type: 'text' })), '`a|b`\n')
}) })
test('writes a text node holding content the same in either order beside its neighbour', () => {
const holding: AdfNode = { content: [text('inner')], text: 'a', type: 'text' }
assert.equal(plain(paragraph(holding, text('b'))), 'ab\n')
assert.equal(plain(paragraph(text('b'), holding)), 'ba\n')
})
+6 -4
View File
@@ -50,7 +50,7 @@ test('reads negative zero as zero, as JSON does', () => {
) )
}) })
test('reads an empty attrs object, marks array or content array as the absent key, but on doc', () => { test('reads an empty attrs object, marks array or content array as the absent key, except on doc', () => {
const paragraph: AdfNode = { attrs: {}, content: [{ attrs: {}, marks: [], text: 'a', type: 'text' }, { marks: [{ attrs: {}, type: 'em' }], text: 'b', type: 'text' }], marks: [], type: 'paragraph' } const paragraph: AdfNode = { attrs: {}, content: [{ attrs: {}, marks: [], text: 'a', type: 'text' }, { marks: [{ attrs: {}, type: 'em' }], text: 'b', type: 'text' }], marks: [], type: 'paragraph' }
assert.deepEqual(toEditorNormal({ content: [paragraph, { content: [], type: 'rule' }], type: 'doc', version: 1 }), { assert.deepEqual(toEditorNormal({ content: [paragraph, { content: [], type: 'rule' }], type: 'doc', version: 1 }), {
content: [{ content: [{ text: 'a', type: 'text' }, { marks: [{ type: 'em' }], text: 'b', type: 'text' }], type: 'paragraph' }, { type: 'rule' }], content: [{ content: [{ text: 'a', type: 'text' }, { marks: [{ type: 'em' }], text: 'b', type: 'text' }], type: 'paragraph' }, { type: 'rule' }],
@@ -77,7 +77,9 @@ test('normalizes blocks and mark attributes nesting far past the levels a recurs
assert.deepEqual(merged.content?.[0]?.content?.map((text) => text.text), ['ab']) assert.deepEqual(merged.content?.[0]?.content?.map((text) => text.text), ['ab'])
}) })
test('joins a text node holding content to its neighbour, as editor-normal forms hold no content to part them', () => { test('joins a text node holding content to no neighbour, so its content is kept', () => {
const paragraph: AdfNode = { content: [{ content: [{ text: 'lost', type: 'text' }], text: 'a', type: 'text' }, { text: 'b', type: 'text' }], type: 'paragraph' } const holding: AdfNode = { content: [{ text: 'kept', type: 'text' }], text: 'a', type: 'text' }
assert.deepEqual(toEditorNormal({ content: [paragraph], type: 'doc', version: 1 }).content, [{ content: [{ content: [{ text: 'lost', type: 'text' }], text: 'ab', type: 'text' }], type: 'paragraph' }]) for (const content of [[holding, { text: 'b', type: 'text' }], [{ text: 'b', type: 'text' }, holding]]) {
assert.deepEqual(toEditorNormal({ content: [{ content, type: 'paragraph' }], type: 'doc', version: 1 }).content, [{ content, type: 'paragraph' }])
}
}) })
+2 -1
View File
@@ -7,8 +7,9 @@ type JsonContainer = JsonValue[] | { [key: string]: JsonValue }
type NodeHolder = { content?: AdfNode[] } type NodeHolder = { content?: AdfNode[] }
// The editor's rule is the reader's over the pair's editor-normal forms, which hold no content: a text node's content never parts it. // The editor's rule is the reader's over the pair's editor-normal forms; a text node holding content never joins, so its content is kept.
export function joinsWhenEditorNormal(previous: AdfNode, node: AdfNode): boolean { export function joinsWhenEditorNormal(previous: AdfNode, node: AdfNode): boolean {
if (previous.type !== 'text' || node.type !== 'text' || nodeContent(previous).length > 0 || nodeContent(node).length > 0) return false
return joinsWhenRead(normalNode(previous), normalNode(node)) return joinsWhenRead(normalNode(previous), normalNode(node))
} }
+22 -21
View File
@@ -24,14 +24,14 @@
| ID | Release | Exempt | Item | R | S | A | G | Goals | Score | | ID | Release | Exempt | Item | R | S | A | G | Goals | Score |
|---|---|---|---|---|---|---|---|---|---| |---|---|---|---|---|---|---|---|---|---|
| 63 | 0.2.0 | defect | **Refuse a cyclic input as `not-an-adf-document` in place of looping forever.** | 2 | 2 | 6 | 9 | 1 | 27.5 | | 63 | 0.2.0 | defect | **Refuse a cyclic input as `not-an-adf-document`.** | 2 | 2 | 6 | 9 | 1 | 27.5 |
| 7 | 0.2.0 | | **Ship HTML: `adfToHtml`, `htmlToAdf`, and `markdownToHtml` / `htmlToMarkdown` composed through ADF.** | 6 | 9 | 9 | 9 | 2, 3 | 25.8 | | 7 | 0.2.0 | | **Ship HTML: `adfToHtml`, `htmlToAdf`, and `markdownToHtml` / `htmlToMarkdown` composed through ADF.** | 6 | 9 | 9 | 9 | 2, 3 | 25.8 |
| 55 | 0.2.0 | defect | **Spell through the carry every document the emitter refuses today for a carriage return, a NUL, a code-span line start, a newline inside an emoji, mention or status, or a text node with empty, missing or content-holding text.** | 5 | 5 | 7 | 9 | 1 | 25.8 | | 55 | 0.2.0 | defect | **Spell through the carry every document the emitter refuses today for a carriage return, a NUL, a code-span line start, a newline inside an emoji, mention or status, or a text node whose text is empty or missing, or which holds content.** | 5 | 5 | 7 | 9 | 1 | 25.8 |
| 45 | 0.2.0 | | **Replace `isAdfDocument` with a reader returning `Result<AdfDocument>`.** | 2 | 3 | 6 | 8 | 1 | 25.2 | | 45 | 0.2.0 | | **Replace `isAdfDocument` with a reader returning `Result<AdfDocument>`.** | 2 | 3 | 6 | 8 | 1 | 25.2 |
| 6 | 0.2.0 | decision | **Specify the HTML dialect.** | 2 | 6 | 7 | 8 | 2, 3 | 24.7 | | 6 | 0.2.0 | decision | **Specify the HTML dialect.** | 2 | 6 | 7 | 8 | 2, 3 | 24.7 |
| 64 | 0.2.0 | defect | **Read every JSON key the emitter writes back unchanged on V8, with a fixture whose key holds `\`, `"` or a control character.** | 4 | 4 | 6 | 8 | 1, 7 | 23.0 | | 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, 7 | 23.0 |
| 43 | 0.2.0 | decision | **Give each markdown input its own reader, strict to its own standard.** | 6 | 7 | 8 | 9 | 3, 4 | 22.3 | | 43 | 0.2.0 | decision | **Give each markdown input its own reader, strict to its own standard.** | 6 | 7 | 8 | 9 | 3, 4 | 22.3 |
| 65 | 0.2.0 | defect | **Read a mid-text or titled CommonMark image as Goal 6 allows, in place of refusing it with `unmappable-image`.** | 5 | 5 | 7 | 7 | 3, 4, 6 | 18.7 | | 65 | 0.2.0 | defect | **Read a mid-text or titled CommonMark image as Goal 6 allows.** | 5 | 5 | 7 | 7 | 3, 4, 6 | 18.7 |
| 49 | 0.2.0 | | **Read a list whose bullet or ordered delimiter changes as two lists in the CommonMark reader.** | 4 | 5 | 5 | 8 | 3, 4 | 17.2 | | 49 | 0.2.0 | | **Read a list whose bullet or ordered delimiter changes as two lists in the CommonMark reader.** | 4 | 5 | 5 | 8 | 3, 4 | 17.2 |
| 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 | | 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 |
| 52 | 0.2.0 | | **Spell `colwidth` as a comma list, `colwidth="340,420"`.** | 3 | 3 | 5 | 6 | 5 | 13.0 | | 52 | 0.2.0 | | **Spell `colwidth` as a comma list, `colwidth="340,420"`.** | 3 | 3 | 5 | 6 | 5 | 13.0 |
@@ -40,7 +40,7 @@
| 60 | 0.2.0 | decision | **Collect the questions `parse/` asks `emit/` into one named module.** | 3 | 4 | 2 | 6 | 2 | 10.7 | | 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<T>` to `result.ts`, and move `Flavour` to `markdown/flavour.ts`.** | 3 | 5 | 2 | 6 | 2 | 10.4 | | 59 | 0.2.0 | decision | **Group the directive grammar into `src/markdown/directive/`, move `Read<T>` to `result.ts`, and move `Flavour` to `markdown/flavour.ts`.** | 3 | 5 | 2 | 6 | 2 | 10.4 |
| 50 | 0.2.0 | | **Read `[](/url)` and `[]()` as CommonMark's empty link.** | 4 | 4 | 3 | 6 | 3, 4, 6 | 10.4 | | 50 | 0.2.0 | | **Read `[](/url)` and `[]()` as CommonMark's empty link.** | 4 | 4 | 3 | 6 | 3, 4, 6 | 10.4 |
| 62 | 0.2.0 | decision | **Move the plain flavour's reading out of `parse/` and its writing out of `emit/` into `markdown/plain/`, so `plain/` and `emit/` no longer import each other.** | 4 | 5 | 2 | 6 | 2 | 9.4 | | 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 |
| 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 | | 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 |
| 47 | 0.3.0 | | **Open the README with what the package is, what it does and for whom.** | 1 | 4 | 7 | 9 | 9 | 14.0 | | 47 | 0.3.0 | | **Open the README with what the package is, what it does and for whom.** | 1 | 4 | 7 | 9 | 9 | 14.0 |
| 34 | 0.3.0 | | **Read emphasis flanking by the whole character beside an astral symbol.** | 2 | 3 | 3 | 6 | 3, 4 | 12.6 | | 34 | 0.3.0 | | **Read emphasis flanking by the whole character beside an astral symbol.** | 2 | 3 | 3 | 6 | 3, 4 | 12.6 |
@@ -57,7 +57,7 @@
## Details ## Details
### 63. Refuse a cyclic input as `not-an-adf-document` in place of looping forever. ### 63. Refuse a cyclic input as `not-an-adf-document`.
`adfDocumentFault` walks with `isNodeArray` (`adf/document.ts`) and `isJsonValue` (`json-value.ts`), `adfDocumentFault` walks with `isNodeArray` (`adf/document.ts`) and `isJsonValue` (`json-value.ts`),
worklists that record no visited object, so a node whose `content` holds itself hangs worklists that record no visited object, so a node whose `content` holds itself hangs
@@ -70,7 +70,7 @@ Lands after items 6, 59, 60, 61 and 62. The CommonMark spec suite also runs agai
`markdownToHtml`. The README documents HTML as it documents markdown, and its tagline and `markdownToHtml`. The README documents HTML as it documents markdown, and its tagline and
`package.json`'s `description` regain HTML. `package.json`'s `description` regain HTML.
### 55. Spell through the carry every document the emitter refuses today for a carriage return, a NUL, a code-span line start, a newline inside an emoji, mention or status, or a text node with empty, missing or content-holding text. ### 55. Spell through the carry every document the emitter refuses today for a carriage return, a NUL, a code-span line start, a newline inside an emoji, mention or status, or a text node whose text is empty or missing, or which holds content.
`inline-line.ts` refuses a carriage return or NUL in text (`unspellable-character`) and a paragraph `inline-line.ts` refuses a carriage return or NUL in text (`unspellable-character`) and a paragraph
opening with a code-span run (`unspellable-line-start`); `fencedTexts` refuses the same characters opening with a code-span run (`unspellable-line-start`); `fencedTexts` refuses the same characters
@@ -78,8 +78,8 @@ in a code block; `directive-syntax.ts` refuses a newline in an emoji, mention or
(`unspellable-whitespace`). The inline carry's JSON escapes all of them, so a lossless spelling (`unspellable-whitespace`). The inline carry's JSON escapes all of them, so a lossless spelling
exists, and §The code list says a cause the carry answers gets no code. Fixing it revises §Which exists, and §The code list says a cause the carry answers gets no code. Fixing it revises §Which
code a cause takes and §Markdown in is a canonical fixpoint, which the maintainer decides. Also a code a cause takes and §Markdown in is a canonical fixpoint, which the maintainer decides. Also a
text node whose `text` is empty or missing, or which holds `content`: `inline-line.ts` refuses it text node whose `text` is empty or missing, which `inline-line.ts` and `fencedTexts` refuse, or
inline and `fencedTexts` in a code block. Found by the README-goals audit, 2026-10-03. which holds `content`, which `inline-line.ts` refuses. Found by the README-goals audit, 2026-10-03.
### 45. Replace `isAdfDocument` with a reader returning `Result<AdfDocument>`. ### 45. Replace `isAdfDocument` with a reader returning `Result<AdfDocument>`.
@@ -93,14 +93,14 @@ 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 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. HTML in input). The set sorts per `docs/decisions.md` §Foreign HTML sorts three ways.
### 64. Read every JSON key the emitter writes back unchanged on V8, with a fixture whose key holds `\`, `"` or a control character. ### 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 `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 `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, (`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. Confirm with a fixture first; then read Deno and Chrome the round-trip would refuse its own output or read back a different key; the fixture
JSON with our own parser or record the gap with an ending item. Found by the README-goals audit, tells which. Confirm with a fixture first; then read JSON with our own parser or record the gap with
2026-10-03. an ending item. Found by the README-goals audit, 2026-10-03.
### 43. Give each markdown input its own reader, strict to its own standard. ### 43. Give each markdown input its own reader, strict to its own standard.
@@ -110,11 +110,11 @@ a code fence whose info string opens `adf:` becomes the block carry where Common
caller names the markdown it hands in: CommonMark, read as its spec says, or the lossless flavour, caller names the markdown it hands in: CommonMark, read as its spec says, or the lossless flavour,
read as `spec/flavour.md` says. Breaking: `MIGRATION.md` says which call a caller takes. read as `spec/flavour.md` says. Breaking: `MIGRATION.md` says which call a caller takes.
### 65. Read a mid-text or titled CommonMark image as Goal 6 allows, in place of refusing it with `unmappable-image`. ### 65. Read a mid-text or titled CommonMark image as Goal 6 allows.
§CommonMark is a subset keeps the image gap and no item ends it: a bot's `See ![diagram](url) here` A mid-text or titled image, such as a bot's `See ![diagram](url) here`, is refused: 14 examples in
or a titled image is refused, 14 examples in `corpus/commonmark-spec/refusals.json`. Settle what such `corpus/commonmark-spec/refusals.json`. Settle what such an image builds. Goal 6 drops form and
an image builds — Goal 6 drops form and keeps the target — and have the decision cite this item. keeps the target.
### 49. Read a list whose bullet or ordered delimiter changes as two lists in the CommonMark reader. ### 49. Read a list whose bullet or ordered delimiter changes as two lists in the CommonMark reader.
@@ -173,12 +173,13 @@ input. Breaking, so it ships beside item 43: `MIGRATION.md`'s Readings table gai
`pending` exceptions go, and its spelling leaves the README's "Four CommonMark spellings" bullet, `pending` exceptions go, and its spelling leaves the README's "Four CommonMark spellings" bullet,
which counts one fewer. which counts one fewer.
### 62. Move the plain flavour's reading out of `parse/` and its writing out of `emit/` into `markdown/plain/`, so `plain/` and `emit/` no longer import each other. ### 62. Move the plain flavour's reading out of `parse/` and its writing out of `emit/` into `markdown/plain/`, so `emit/` no longer imports `plain/`.
Reading: the alert and task-marker reads in `parse/markdown-to-adf.ts`, the `mintTaskIds` call and Reading: the alert and task-marker reads in `parse/markdown-to-adf.ts`, the `mintTaskIds` call and
the `inlineLeaves` use. Writing: `spellPlainBlock`, `quotedUnder`, `tryTaskList` and `taskBlocks` the `inlineLeaves` use. Writing: `spellPlainBlock`, `quotedUnder`, `tryTaskList` and `taskBlocks` in
in `emit/adf-to-markdown.ts`. Every comprehension seat on 2026-10-03 named the plain flavour's `emit/adf-to-markdown.ts`. And `highlightDelimiter` and `highlightFlanking` move out of
spread across three directories. `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.
### 38. Spell a lone surrogate in a text node so it survives a UTF-8 encode. ### 38. Spell a lone surrogate in a text node so it survives a UTF-8 encode.