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

Answer architecture round 1: a * marker before a first line of * alone takes a backslash, the marker choice is a decision, the spec names which lists alternate, and the stale listBreak test goes

This commit is contained in:
2026-10-09 14:57:52 +02:00
parent 4aa55e9ee1
commit 2b211e9e4b
8 changed files with 48 additions and 12 deletions
+1
View File
@@ -13,6 +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
- 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
@@ -175,6 +175,25 @@
} }
], ],
"type": "bulletList" "type": "bulletList"
},
{
"content": [
{
"content": [
{
"content": [
{
"text": "**",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "listItem"
}
],
"type": "bulletList"
} }
], ],
"type": "doc", "type": "doc",
@@ -14,3 +14,5 @@
- - Head - - Head
* Tail * Tail
* \**
+15 -3
View File
@@ -71,6 +71,16 @@ Two adjacent text nodes CommonMark would read back as one are parted by the rese
`!adf:textBreak{}`: a leaf building no node keeps both nodes and asks nothing of the text around `!adf:textBreak{}`: a leaf building no node keeps both nodes and asks nothing of the text around
it. 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**. it. 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
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.
## An empty key spells `empty` ## An empty key spells `empty`
2026-10-03, a writer panel and the maintainer. Goals 1 and 6. Valid while no attribute value 2026-10-03, a writer panel and the maintainer. Goals 1 and 6. Valid while no attribute value
@@ -278,7 +288,9 @@ 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, 5 of 7). 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.
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
@@ -689,8 +701,8 @@ 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. `text` and `spelling` carry `commonMarkSpelling` ask stops spelling a node once per level above it. The spelled texts and
no depth and `headroom` is affine in it, so a read at or above the depth that filled the entry `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 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 the parser spent an ordered list past the marker cap gives way, spending two emitter levels where the parser spent
one. Only what succeeded is kept, so no path minted at another position is ever read. one. Only what succeeded is kept, so no path minted at another position is ever read.
+4 -3
View File
@@ -19,9 +19,10 @@ normalizes to it through the round-trip.
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` attribute. A list right after a list of its kind takes the other marker, `* ` or `1)`, `order` attribute. A bullet or task list right after a bullet or task list takes `* `, and an
since CommonMark starts a new list where the bullet character or the delimiter changes; the ordered list right after an ordered one `1)`, since CommonMark starts a new list where the
list after that takes `- ` or `1.` again. Continuation lines align with the first character bullet character or the delimiter changes; the list after that takes `- ` or `1.` again, and a
first line of `*` alone 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 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 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 list's own marker cannot interrupt a paragraph (an ordered list whose first number is not 1, or
@@ -177,6 +177,9 @@ test('alternates the marker between adjacent lists of a kind, the marker change
assert.equal(markdown(adfToLosslessMarkdown(document(nested))), '- - x\n * x\n') assert.equal(markdown(adfToLosslessMarkdown(document(nested))), '- - x\n * x\n')
const stars: AdfNode = { content: [{ content: [paragraph({ text: '* *', type: 'text' })], type: 'listItem' }], type: 'bulletList' } const stars: AdfNode = { content: [{ content: [paragraph({ text: '* *', type: 'text' })], type: 'listItem' }], type: 'bulletList' }
assert.equal(markdown(adfToLosslessMarkdown(document(list, stars))), '- x\n\n* \\* *\n') assert.equal(markdown(adfToLosslessMarkdown(document(list, stars))), '- x\n\n* \\* *\n')
const strong: AdfNode = { content: [{ content: [paragraph({ text: '**', type: 'text' }), paragraph({ text: 'y', type: 'text' })], type: 'listItem' }], type: 'bulletList' }
assert.equal(markdown(adfToLosslessMarkdown(document(strong))), '- **\n\n y\n')
assert.equal(markdown(adfToLosslessMarkdown(document(list, strong))), '- x\n\n* \\**\n\n y\n')
const carried: AdfNode = { ...list, attrs: { unknown: 'x' } } const carried: AdfNode = { ...list, attrs: { unknown: 'x' } }
assert.ok(markdown(adfToLosslessMarkdown(document(carried, carried))).includes('```\n\n```adf:bulletList\n')) assert.ok(markdown(adfToLosslessMarkdown(document(carried, carried))).includes('```\n\n```adf:bulletList\n'))
assert.ok(markdown(adfToLosslessMarkdown(document(carried, list))).endsWith('```\n\n- x\n')) assert.ok(markdown(adfToLosslessMarkdown(document(carried, list))).endsWith('```\n\n- x\n'))
+4 -2
View File
@@ -364,8 +364,10 @@ function tryListItemLines(inner: string, marker: string): string | undefined {
if (body.some((line) => line !== '' && isBlankLine(line))) return undefined if (body.some((line) => line !== '' && isBlankLine(line))) return undefined
const indent = ' '.repeat(marker.length) const indent = ' '.repeat(marker.length)
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}`))
if (isThematicBreak(lines[0] ?? '')) return undefined const first = lines[0] ?? ''
return lines.join('\n') if (!isThematicBreak(first)) return lines.join('\n')
// A first line of `*` alone beside the `* ` marker is text, since a rule is spelled `---`: a backslash keeps it from reading as one.
return inner.startsWith('*') ? [`${marker}\\${first.slice(marker.length)}`, ...lines.slice(1)].join('\n') : undefined
} }
function tryParagraph(node: AdfNode, path: ConvertErrorPath, flavour: WrittenFlavour): Result<EmittedBlock> | undefined { function tryParagraph(node: AdfNode, path: ConvertErrorPath, flavour: WrittenFlavour): Result<EmittedBlock> | undefined {
@@ -284,10 +284,6 @@ test('reads a change of bullet character or ordered delimiter as a second list,
assert.deepEqual(path(losslessMarkdownToAdf('- a\n\n* b\n\n| x |\n')), ['content', 2]) assert.deepEqual(path(losslessMarkdownToAdf('- a\n\n* b\n\n| x |\n')), ['content', 2])
}) })
test('reads the retired list separator as a directive no node spells', () => {
assert.equal(content(losslessMarkdownToAdf('- a\n\n!adf:listBreak\n\n- b\n')), 'unknown-directive-name: the directive name listBreak reads back to no node; \\!adf: keeps the prefix literal')
})
test('reads the image a pipe cell holds as its linked alt text', () => { test('reads the image a pipe cell holds as its linked alt text', () => {
assert.deepEqual(content(losslessMarkdownToAdf('| a |\n| --- |\n| ![x](/u) |\n')), [ assert.deepEqual(content(losslessMarkdownToAdf('| a |\n| --- |\n| ![x](/u) |\n')), [
{ content: [{ content: [cell('tableHeader', text('a'))], type: 'tableRow' }, { content: [cell('tableCell', marked('x', link('/u')))], type: 'tableRow' }], type: 'table' }, { content: [{ content: [cell('tableHeader', text('a'))], type: 'tableRow' }, { content: [cell('tableCell', marked('x', link('/u')))], type: 'tableRow' }], type: 'table' },