From ca3d70306141b13e206d387f1c31c94a85643242 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Fri, 9 Oct 2026 15:23:05 +0200 Subject: [PATCH] Answer prose round 1: the decision says right after and keeps only its reason, the README counts the link-definition exception, and the comments and spec say which list alternates --- AGENTS.md | 2 +- CHANGELOG.md | 2 +- MIGRATION.md | 2 +- README.md | 17 +++++++------- docs/decisions.md | 15 +++++-------- spec/flavour.md | 22 +++++++++---------- src/markdown/emit/adf-to-markdown.ts | 5 ++--- src/markdown/parse/blocks.ts | 2 +- .../portable/adf-to-portable-markdown.ts | 2 +- todo.md | 6 ++--- 10 files changed, 35 insertions(+), 40 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index cb7ba62..9522e2c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,7 +13,7 @@ In `docs/decisions.md`: - Markdown in is a canonical fixpoint - Equality is deep - `!adf:textBreak{}` parts text CommonMark would join -- A list beside one of its kind takes the other marker +- A list right after one of its kind takes the other marker - An empty key spells `empty` - `-0` is spelled `-0` - Empty markdown is a document of no blocks diff --git a/CHANGELOG.md b/CHANGELOG.md index fd96c2a..4f94826 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ - **Breaking:** a list whose bullet character or ordered delimiter changes reads as two lists, as CommonMark reads it: `- a` then `* b`, `1. a` then `1) b`. `adfToLosslessMarkdown` gives a list right after one of its kind the other marker, `* ` or `1)`, in place of the `::listBreak` - separator, and `adfToPortableMarkdown` does the same. Convert stored markdown per `MIGRATION.md`. + separator. Convert stored markdown per `MIGRATION.md`. - **Breaking:** each call names the markdown it reads or writes. `adfToMarkdown` and `markdownToAdf` are now `adfToLosslessMarkdown` and `losslessMarkdownToAdf`. A bot's or a model's markdown reads in `portableMarkdownToAdf`, and markdown known to be strict CommonMark in the new diff --git a/MIGRATION.md b/MIGRATION.md index fde60fb..0c1a2fc 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -65,7 +65,7 @@ function migrateMarkdown(stored: string) { | the `adf` code fence and `:adf{json="…"}` | the `adf:` code fence, its JSON without `type`, and `!adf:carry{json="…"}` | | `\:` keeps a directive literal | `\!adf:` keeps a directive literal | | `:adf{json="…"}` carrying a link for its `collection`, `id` or `occurrenceKey` | `!adf:link[text]{attrs}` | -| `::listBreak` between two lists of a kind | no separator, the second list's marker changed: `* ` for `- `, `1)` for `1.`, alternating | +| `::listBreak` between two lists of a kind | no separator: adjacent lists of a kind alternate `- ` and `* `, or `1.` and `1)` | A colon run and `:name[` are plain text now, and `adf` an ordinary code block language; text holding an unescaped `!adf:` and a code fence whose info string opens `adf:` are claimed instead. diff --git a/README.md b/README.md index 90c408b..3b808ea 100644 --- a/README.md +++ b/README.md @@ -155,8 +155,8 @@ saving what this pair read replaces mentions, attachments and macros with text. - A task list holding a block other than a task is a bullet list keeping its states as text: `- \[x] Done`. - Text that would read as a marker takes a backslash: `==` wherever it could open or close a - highlight, `[!…]` opening a quote, asterisks that would read as a rule after a `* ` marker, - `* \**`, and `[x]` or `[ ]` opening any list item, since GitHub reads that marker per item. + highlight, `[!…]` opening a quote, asterisks that would read as a rule after a `* ` marker + (`* \**`), and `[x]` or `[ ]` opening any list item, since GitHub reads that marker per item. - A node read back carries no `localId` except a `taskList`, `taskItem` or `blockTaskItem`, which Atlassian's schema requires one on: each gets a UUID v4 hashed from the whole markdown and its position, the same on every read. Join markdown bound for one document and read it once: the same @@ -239,7 +239,7 @@ Serves Goals 1, 4 and 5. without a delimiter row, is an error result; each can be kept literal (`spec/flavour.md`). - A document any reader built, written back with `adfToLosslessMarkdown`, takes the library's canonical spelling, which round-trips byte-identically. -- Six CommonMark spellings parse without an error and build a document the reference +- Seven CommonMark spellings parse without an error and build a document the reference implementation renders differently. Four because ADF holds no node for what it renders: - An empty link text reads as its destination, `[](/url)` as `/url` linked to `/url`, and `[]()` as nothing. A link text holding only hard breaks reads the same, followed by its hard breaks. @@ -253,11 +253,12 @@ Serves Goals 1, 4 and 5. - An image's title becomes its caption when the image stays an image, and its link's title when it becomes linked alt text. Inside another link, the title is dropped. - And two more: a shortcut reference matching its definition only under Unicode case folding stays - unresolved, and a link whose text holds an autolink keeps the inner link and leaves the outer - brackets literal text, which the spec requires and the reference itself breaks, nesting one `` - in the other. `corpus/commonmark-spec/exceptions.json` pins the first four `node-model` where the - suite reaches them, and the next `pending`; the suite holds no example of the last. + And three more: a shortcut reference matching its definition only under Unicode case folding + stays unresolved, two lists a link reference definition parts stay one list against CommonMark's + two, and a link whose text holds an autolink keeps the inner link and leaves the outer brackets + literal text, which the spec requires and the reference itself breaks, nesting one `` in the + other. `corpus/commonmark-spec/exceptions.json` pins the first four `node-model` where the suite + reaches them and the fifth `pending`; the suite holds no example of the last two. - Not every document converts back: `adfToLosslessMarkdown` refuses a document nesting past 500 levels, and the node shapes the `unsupported-node-shape` row lists. Show the refusal and keep the document read-only; saving markdown you could not produce is the loss the round-trip exists to diff --git a/docs/decisions.md b/docs/decisions.md index cc21b74..efd1022 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -72,15 +72,14 @@ Two adjacent text nodes CommonMark would read back as one are parted by the rese A code span holds no directive, so the spans close and reopen around the leaf. The grammar: `spec/flavour.md` §Inline nodes, **Adjacent text nodes**. -## A list beside one of its kind takes the other marker +## A list right after one of its kind takes the other marker 2026-10-04, the maintainer. Goals 4 and 5. Valid while CommonMark starts a new list where the bullet character or the ordered delimiter changes. -Every reader parts lists where the marker changes, as CommonMark does. Both writers give a bullet or -task list right after a bullet or task list `* `, and an ordered list right after an ordered one -`1)`; the list after that takes `- ` or `1.` again. CommonMark already spells two adjacent lists, so -no directive parts them. The grammar: `spec/flavour.md` §Canonical form. +Every reader parts lists where the marker changes, as CommonMark does, so both writers alternate the +marker between adjacent lists of a kind and no directive parts them. `*` is the maintainer's pick. +The grammar: `spec/flavour.md` §Canonical form. ## An empty key spells `empty` @@ -289,9 +288,7 @@ renderer confirming each shape. Reader panels settled `error` as an error panel, (3 of 3) and a Han, Hangul, kana, Thai, Lao, Khmer or Myanmar character on either side bounding a delimiter, so `は==日本語==で` (3 of 3), `==한국어==에서만` and `iPhone==専用==` (6 of 7) highlight, the external image's two forms (6 of 7), a rule opening a list item dropping and the omission -notes (3 of 3), and a list's numbering overflowing into bullets (3 of 3). A 5 of 7 verdict joining -adjacent ordered lists whose numbering breaks into one such list lapsed on 2026-10-09, when a -delimiter change began parting them. +notes (3 of 3), and a list's numbering overflowing into bullets (3 of 3). An omission note reads as the converter's, never as the author's. Reading takes other tools' spellings, since it reads their output and writes none of them. Rejected: `~sub~` and `^sup^` (`~2~` is a strike on GitHub, so `subsup` drops), underline and colour @@ -702,7 +699,7 @@ two cannot disagree — which is what makes the kept value a memo rather than a per level above it otherwise. The parse and the portable reduction keep each node's readable spelling in a memo, so the -`commonMarkSpelling` ask stops spelling a node once per level above it. The spelled texts and +`commonMarkSpelling` ask stops spelling a node once per level above it. `text`, `alternateText` and `spelling` carry no depth and `headroom` is affine in it, so a read at or above the depth that filled the entry rebases; a read below re-spells, because a hit skips the depth guards the walk it replaces runs and an ordered list past the marker cap gives way, spending two emitter levels where diff --git a/spec/flavour.md b/spec/flavour.md index 084c42e..b1133f5 100644 --- a/spec/flavour.md +++ b/spec/flavour.md @@ -18,18 +18,16 @@ normalizes to it through the round-trip. cannot parse (intra-word). Strike is GFM strikethrough narrowed to exactly two tildes — a single tilde or a run of three or more is literal text — and block structure resolves before inline, so a `~~~` line opens a CommonMark tilde code fence. -- Bullet lists `- `; ordered lists incrementing `1.` `2.` `3.`, the first number the node's - `order` attribute. A bullet list right after a bullet list takes `* `, and an ordered list - right after an ordered one `1)`, since CommonMark starts a new list where the - bullet character or the delimiter changes; the list after that takes `- ` or `1.` again. An item - text of asterisks and spaces that would read as a rule after `* ` opens with a backslash, - `* \**`. Continuation lines align with the first character - after the marker (two spaces for `- `, three for `1. `); blank lines inside an item are empty - lines, none between a nested list and a CommonMark block above it — one wherever the nested - list's own marker cannot interrupt a paragraph (an ordered list whose first number is not 1, or - a list whose first item is empty), whatever block sits above it. Blank lines between items - normalize away. A list whose item holds a line of spaces or tabs alone, which a list item reads - back empty, takes the directive form. +- Bullet lists `- `; ordered lists incrementing `1.` `2.` `3.`, the first number the node's `order` + attribute. CommonMark starts a new list where the bullet character or the delimiter changes, so + adjacent lists of a kind alternate markers, `- ` `* ` `- ` and `1.` `1)` `1.`. An item whose first + line would read as a rule after `* ` opens with a backslash, `* \**`. Continuation lines align + with the first character after the marker (two spaces for `- `, three for `1. `); blank lines + inside an item are empty lines, none between a nested list and a CommonMark block above it — one + wherever the nested list's own marker cannot interrupt a paragraph (an ordered list whose first + number is not 1, or a list whose first item is empty), whatever block sits above it. Blank lines + between items normalize away. A list whose item holds a line of spaces or tabs alone, which a list + item reads back empty, takes the directive form. - 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 diff --git a/src/markdown/emit/adf-to-markdown.ts b/src/markdown/emit/adf-to-markdown.ts index b04db45..b775500 100644 --- a/src/markdown/emit/adf-to-markdown.ts +++ b/src/markdown/emit/adf-to-markdown.ts @@ -18,14 +18,13 @@ import { tryImage } from './image.ts' import { tryPipeTable } from './pipe-table.ts' type BlockContainer = 'directive' | 'document' | 'list-item' -// The usual delimiter, then the one a list beside another of its kind takes. type Delimiters = readonly [string, string] // headroom: the nesting levels the deepest node below may still spend before `largestNesting` refuses it. type EmittedBlock = SpelledBlock & { headroom: number } type KeptSpelling = { block: EmittedBlock | undefined; depth: number } type PlacedBlock = SpelledBlock & { node: AdfNode } type PlacedBlocks = { blocks: readonly PlacedBlock[]; headroom: number } -// CommonMark starts a new list where the marker changes, so a list beside one of its kind takes `alternateText`. +// CommonMark starts a new list where the marker changes, so every second list in a run of one kind takes `alternateText`. type SpelledBlock = { spelling: 'commonmark' | 'directive'; text: string } | { alternateText: string; spelling: 'list'; text: string } // Keyed by reference: only a caller building one object per position (the parse, the portable reduction) passes one; a consumer's document may share a node. export type SpellingMemo = Map @@ -367,7 +366,7 @@ function tryListItemLines(inner: string, marker: string): string | undefined { const lines = body.map((line, index) => (index === 0 ? `${marker}${line}` : line === '' ? '' : `${indent}${line}`)) const first = lines[0] ?? '' if (!isThematicBreak(first)) return lines.join('\n') - // Only text opens an item with `*`, a rule being spelled `---`; where `* ` before it reads as a rule, a backslash keeps it text. + // An item opens with `*` only as text, a list there taking `- ` and a rule `---`; a backslash keeps a first line reading as a rule text. return inner.startsWith('*') ? [`${marker}\\${first.slice(marker.length)}`, ...lines.slice(1)].join('\n') : undefined } diff --git a/src/markdown/parse/blocks.ts b/src/markdown/parse/blocks.ts index 9198748..61f16cf 100644 --- a/src/markdown/parse/blocks.ts +++ b/src/markdown/parse/blocks.ts @@ -246,7 +246,7 @@ function openContainer(walk: Walk, start: ContainerStart): void { walk.stack.push({ blocks, indentation: start.indentation, kind: 'item', list }) } -// An item after a closed one, such as an empty item a blank line ends, continues the list beside it. +// An item opening after its list's last item closed, as an empty item a blank line ends does, continues that list. function openedList(walk: Walk, start: Extract): ListBlock { if (!start.fresh) return start.list const blocks = currentBlocks(walk) diff --git a/src/markdown/portable/adf-to-portable-markdown.ts b/src/markdown/portable/adf-to-portable-markdown.ts index 82e4238..15bbf78 100644 --- a/src/markdown/portable/adf-to-portable-markdown.ts +++ b/src/markdown/portable/adf-to-portable-markdown.ts @@ -125,7 +125,7 @@ function portableSequence(blocks: readonly AdfNode[], reduction: Reduction): Res sequence.push(block) continue } - // Read next, since a replacement may take the directive form itself. + // Back on `pending`: a replacement may take the directive form itself. const replacement = block === next ? nodeContent(block).flatMap(nodeContent) : [block] for (const node of replacement.reverse()) pending.push(node) } diff --git a/todo.md b/todo.md index 1af7fac..137d382 100644 --- a/todo.md +++ b/todo.md @@ -83,7 +83,7 @@ of one character from each of its characters. no block stands between, so `- a\n\n[r]: /u\n\n- b` reads as one list; CommonMark reads two, the definition closing the first. The join exists for an empty item a blank line ends, which continues its list. `markdown-to-adf.test.ts` asserts the one list. Breaking: `MIGRATION.md`'s -Readings table gains its row. +Readings table gains its row, and the README's guarantees drop the exception and count one fewer. ### 93. Report a refusal's `path` where the node would stand after `portableMarkdownToAdf` moves it out of a task item. @@ -275,8 +275,8 @@ A writer panel chose it on 2026-10-03, 5 of 7, over today's `colwidth="[340,420] `link-syntax.ts` normalizes a label with `toLowerCase`, so `[ẞ]` misses its `[SS]` definition (spec example 540); lowercasing and then uppercasing folds it. Breaking: `MIGRATION.md`'s Readings table -gains its row. Its `pending` exceptions go, and its spelling leaves the README's bullet counting -the CommonMark spellings, which counts one fewer. +gains its row. Its `pending` exceptions go, and the README's guarantees drop the case-folding +exception and count one fewer. ### 53. Put a block's `marks` spelling to the writer panel and adopt its pick.