3
0
mirror of https://github.com/larvit/adf-codec.git synced 2026-11-12 19:58:28 +01:00

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

This commit is contained in:
2026-10-09 15:23:05 +02:00
parent 994c450490
commit ca3d703061
10 changed files with 35 additions and 40 deletions
+1 -1
View File
@@ -13,7 +13,7 @@ In `docs/decisions.md`:
- Markdown in is a canonical fixpoint - Markdown in is a canonical fixpoint
- Equality is deep - Equality is deep
- `!adf:textBreak{}` parts text CommonMark would join - `!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` - An empty key spells `empty`
- `-0` is spelled `-0` - `-0` is spelled `-0`
- Empty markdown is a document of no blocks - Empty markdown is a document of no blocks
+1 -1
View File
@@ -5,7 +5,7 @@
- **Breaking:** a list whose bullet character or ordered delimiter changes reads as two lists, as - **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 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` 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` - **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 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 in `portableMarkdownToAdf`, and markdown known to be strict CommonMark in the new
+1 -1
View File
@@ -65,7 +65,7 @@ function migrateMarkdown(stored: string) {
| the `adf` code fence and `:adf{json="…"}` | the `adf:<type>` code fence, its JSON without `type`, and `!adf:carry{json="…"}` | | the `adf` code fence and `:adf{json="…"}` | the `adf:<type>` code fence, its JSON without `type`, and `!adf:carry{json="…"}` |
| `\:` keeps a directive literal | `\!adf:` keeps a directive literal | | `\:` keeps a directive literal | `\!adf:` keeps a directive literal |
| `:adf{json="…"}` carrying a link for its `collection`, `id` or `occurrenceKey` | `!adf:link[text]{attrs}` | | `: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 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. holding an unescaped `!adf:` and a code fence whose info string opens `adf:` are claimed instead.
+9 -8
View File
@@ -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: - A task list holding a block other than a task is a bullet list keeping its states as text:
`- \[x] Done`. `- \[x] Done`.
- Text that would read as a marker takes a backslash: `==` wherever it could open or close a - 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, 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. (`* \**`), 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 - 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 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 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`). 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 - A document any reader built, written back with `adfToLosslessMarkdown`, takes the library's
canonical spelling, which round-trips byte-identically. 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: 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 `[]()` - 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. 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 - 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. 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 And three more: a shortcut reference matching its definition only under Unicode case folding
unresolved, and a link whose text holds an autolink keeps the inner link and leaves the outer stays unresolved, two lists a link reference definition parts stay one list against CommonMark's
brackets literal text, which the spec requires and the reference itself breaks, nesting one `<a>` two, and a link whose text holds an autolink keeps the inner link and leaves the outer brackets
in the other. `corpus/commonmark-spec/exceptions.json` pins the first four `node-model` where the literal text, which the spec requires and the reference itself breaks, nesting one `<a>` in the
suite reaches them, and the next `pending`; the suite holds no example of the last. 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 - 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 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 document read-only; saving markdown you could not produce is the loss the round-trip exists to
+6 -9
View File
@@ -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: 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**. `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 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. bullet character or the ordered delimiter changes.
Every reader parts lists where the marker changes, as CommonMark does. Both writers give a bullet or Every reader parts lists where the marker changes, as CommonMark does, so both writers alternate the
task list right after a bullet or task list `* `, and an ordered list right after an ordered one marker between adjacent lists of a kind and no directive parts them. `*` is the maintainer's pick.
`1)`; the list after that takes `- ` or `1.` again. CommonMark already spells two adjacent lists, so The grammar: `spec/flavour.md` §Canonical form.
no directive parts them. The grammar: `spec/flavour.md` §Canonical form.
## An empty key spells `empty` ## 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 (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, 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 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 notes (3 of 3), and a list's numbering overflowing into bullets (3 of 3).
adjacent ordered lists whose numbering breaks into one such list lapsed on 2026-10-09, when a
delimiter change began parting them.
An omission note reads as the converter's, never as the author's. 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. 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 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. per level above it otherwise.
The parse and the portable reduction keep each node's readable spelling in a memo, so the 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 `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 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 replaces runs and an ordered list past the marker cap gives way, spending two emitter levels where
+10 -12
View File
@@ -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 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 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. inline, so a `~~~` line opens a CommonMark tilde code fence.
- Bullet lists `- `; ordered lists incrementing `1.` `2.` `3.`, the first number the node's - Bullet lists `- `; ordered lists incrementing `1.` `2.` `3.`, the first number the node's `order`
`order` attribute. A bullet list right after a bullet list takes `* `, and an ordered list attribute. CommonMark starts a new list where the bullet character or the delimiter changes, so
right after an ordered one `1)`, since CommonMark starts a new list where the adjacent lists of a kind alternate markers, `- ` `* ` `- ` and `1.` `1)` `1.`. An item whose first
bullet character or the delimiter changes; the list after that takes `- ` or `1.` again. An item line would read as a rule after `* ` opens with a backslash, `* \**`. Continuation lines align
text of asterisks and spaces that would read as a rule after `* ` opens with a backslash, with the first character after the marker (two spaces for `- `, three for `1. `); blank lines
`* \**`. Continuation lines align with the first character inside an item are empty lines, none between a nested list and a CommonMark block above it — one
after the marker (two spaces for `- `, three for `1. `); blank lines inside an item are empty wherever the nested list's own marker cannot interrupt a paragraph (an ordered list whose first
lines, none between a nested list and a CommonMark block above it — one wherever the nested number is not 1, or a list whose first item is empty), whatever block sits above it. Blank lines
list's own marker cannot interrupt a paragraph (an ordered list whose first number is not 1, or between items normalize away. A list whose item holds a line of spaces or tabs alone, which a list
a list whose first item is empty), whatever block sits above it. Blank lines between items item reads back empty, takes the directive form.
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 `>`. - Blockquotes prefix lines with `> `; a blank line inside a blockquote is a bare `>`.
- ATX headings (`#` … `######`); setext input normalizes to ATX. - ATX headings (`#` … `######`); setext input normalizes to ATX.
- Code fences ``` with the node's language as info string, the fence lengthened past any backtick - Code fences ``` with the node's language as info string, the fence lengthened past any backtick
+2 -3
View File
@@ -18,14 +18,13 @@ import { tryImage } from './image.ts'
import { tryPipeTable } from './pipe-table.ts' import { tryPipeTable } from './pipe-table.ts'
type BlockContainer = 'directive' | 'document' | 'list-item' 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] type Delimiters = readonly [string, string]
// headroom: the nesting levels the deepest node below may still spend before `largestNesting` refuses it. // headroom: the nesting levels the deepest node below may still spend before `largestNesting` refuses it.
type EmittedBlock = SpelledBlock & { headroom: number } type EmittedBlock = SpelledBlock & { headroom: number }
type KeptSpelling = { block: EmittedBlock | undefined; depth: number } type KeptSpelling = { block: EmittedBlock | undefined; depth: number }
type PlacedBlock = SpelledBlock & { node: AdfNode } type PlacedBlock = SpelledBlock & { node: AdfNode }
type PlacedBlocks = { blocks: readonly PlacedBlock[]; headroom: number } 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 } 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. // 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<AdfNode, KeptSpelling> export type SpellingMemo = Map<AdfNode, KeptSpelling>
@@ -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 lines = body.map((line, index) => (index === 0 ? `${marker}${line}` : line === '' ? '' : `${indent}${line}`))
const first = lines[0] ?? '' const first = lines[0] ?? ''
if (!isThematicBreak(first)) return lines.join('\n') 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 return inner.startsWith('*') ? [`${marker}\\${first.slice(marker.length)}`, ...lines.slice(1)].join('\n') : undefined
} }
+1 -1
View File
@@ -246,7 +246,7 @@ function openContainer(walk: Walk, start: ContainerStart): void {
walk.stack.push({ blocks, indentation: start.indentation, kind: 'item', list }) 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<ContainerStart, { kind: 'item' }>): ListBlock { function openedList(walk: Walk, start: Extract<ContainerStart, { kind: 'item' }>): ListBlock {
if (!start.fresh) return start.list if (!start.fresh) return start.list
const blocks = currentBlocks(walk) const blocks = currentBlocks(walk)
@@ -125,7 +125,7 @@ function portableSequence(blocks: readonly AdfNode[], reduction: Reduction): Res
sequence.push(block) sequence.push(block)
continue 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] const replacement = block === next ? nodeContent(block).flatMap(nodeContent) : [block]
for (const node of replacement.reverse()) pending.push(node) for (const node of replacement.reverse()) pending.push(node)
} }
+3 -3
View File
@@ -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, 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 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 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. ### 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 `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 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 gains its row. Its `pending` exceptions go, and the README's guarantees drop the case-folding
the CommonMark spellings, which counts one fewer. exception and count one fewer.
### 53. Put a block's `marks` spelling to the writer panel and adopt its pick. ### 53. Put a block's `marks` spelling to the writer panel and adopt its pick.