From 9ce5946173743aab723aefccf9095b75275b5eb1 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Tue, 25 Aug 2026 15:38:16 +0200 Subject: [PATCH] Fold the per-node ambiguity codes into one, and sharpen the container fence rule --- AGENTS.md | 3 ++- corpus/unspellable/code-block-empty-language.error | 2 +- corpus/unspellable/media-empty-alt.error | 2 +- corpus/unspellable/ordered-list-start-one.error | 2 +- spec/flavour.md | 6 +++--- src/adf-to-markdown.test.ts | 8 ++++---- src/adf-to-markdown.ts | 4 ++-- src/markdown-image.ts | 2 +- src/result.ts | 4 +--- 9 files changed, 16 insertions(+), 17 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index f741040..6677d43 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -75,7 +75,8 @@ Pre-1.0, normal 0.x rules. The error surface is a contract too. `ConvertError` is `{ code, message, path }` — the code from a closed list a consumer may switch exhaustively, the message free text, the path the node's position from the document root. Adding, removing or renaming a code is breaking, so a milestone meeting a -new failure cause reuses a code where one fits; the list is complete at `0.1.0`. +new failure cause reuses a code where one fits; the list is complete at `0.1.0`. A code names the +cause, never the node — `path` and `message` carry that. ## 9. Release automation diff --git a/corpus/unspellable/code-block-empty-language.error b/corpus/unspellable/code-block-empty-language.error index a53e1bc..01daa5e 100644 --- a/corpus/unspellable/code-block-empty-language.error +++ b/corpus/unspellable/code-block-empty-language.error @@ -1 +1 @@ -ambiguous-empty-code-block-language +ambiguous-attribute-spelling diff --git a/corpus/unspellable/media-empty-alt.error b/corpus/unspellable/media-empty-alt.error index a7d35ec..01daa5e 100644 --- a/corpus/unspellable/media-empty-alt.error +++ b/corpus/unspellable/media-empty-alt.error @@ -1 +1 @@ -ambiguous-empty-media-alt +ambiguous-attribute-spelling diff --git a/corpus/unspellable/ordered-list-start-one.error b/corpus/unspellable/ordered-list-start-one.error index e7b3b24..01daa5e 100644 --- a/corpus/unspellable/ordered-list-start-one.error +++ b/corpus/unspellable/ordered-list-start-one.error @@ -1 +1 @@ -ambiguous-ordered-list-start +ambiguous-attribute-spelling diff --git a/spec/flavour.md b/spec/flavour.md index 14be46f..7df17af 100644 --- a/spec/flavour.md +++ b/spec/flavour.md @@ -74,9 +74,9 @@ block content The fence is three or more colons. `arg` is one optional bare token whose meaning each node defines (e.g. the panel type). The body is block markdown. The closing fence is a line of at -least the opening's length, and a container's fence is longer than every directive fence line in -its body — counting only lines that parse as directive fences in the body's block structure; a -colon run inside a code fence or opaque carry is content. Canonical form uses minimal lengths. +least the opening's length, and a container's fence is longer than every directive fence line +anywhere in its body, however deeply a list item or blockquote nests it; a colon run inside a code +fence or opaque carry is content. Canonical form uses minimal lengths. Directive fence lines follow code-fence indentation (up to three spaces relative to their container); trailing whitespace on a fence line is tolerated in input, never emitted. diff --git a/src/adf-to-markdown.test.ts b/src/adf-to-markdown.test.ts index 28f3f71..74c4948 100644 --- a/src/adf-to-markdown.test.ts +++ b/src/adf-to-markdown.test.ts @@ -53,13 +53,13 @@ test('refuses marks on a block node', () => { test('refuses an ordered list whose markdown start is ambiguous', () => { const items: AdfNode[] = [{ content: [paragraph({ text: 'x', type: 'text' })], type: 'listItem' }] - assert.equal(code(adfToMarkdown(document({ content: items, type: 'orderedList' }))), 'ambiguous-ordered-list-start') - assert.equal(code(adfToMarkdown(document({ attrs: { order: 1 }, content: items, type: 'orderedList' }))), 'ambiguous-ordered-list-start') + assert.equal(code(adfToMarkdown(document({ content: items, type: 'orderedList' }))), 'ambiguous-attribute-spelling') + assert.equal(code(adfToMarkdown(document({ attrs: { order: 1 }, content: items, type: 'orderedList' }))), 'ambiguous-attribute-spelling') assert.equal(markdown(adfToMarkdown(document({ attrs: { order: 2 }, content: items, type: 'orderedList' }))), '2. x\n') }) test('refuses the code block info strings the fence cannot hold', () => { - assert.equal(code(adfToMarkdown(document({ attrs: { language: '' }, type: 'codeBlock' }))), 'ambiguous-empty-code-block-language') + assert.equal(code(adfToMarkdown(document({ attrs: { language: '' }, type: 'codeBlock' }))), 'ambiguous-attribute-spelling') assert.equal(code(adfToMarkdown(document({ attrs: { language: 'adf' }, type: 'codeBlock' }))), 'reserved-adf-language') assert.equal(code(adfToMarkdown(document({ attrs: { language: 'a`b' }, type: 'codeBlock' }))), 'unspellable-code-block-language') assert.equal(code(adfToMarkdown(document({ attrs: { language: ' sql' }, type: 'codeBlock' }))), 'unspellable-code-block-language') @@ -334,7 +334,7 @@ test('spells the image form for exactly the centered external media shape', () = assert.equal(markdown(adfToMarkdown(single({ alt: 'The moon', type: 'external', url }))), `![The moon](${url})\n`) assert.equal(markdown(adfToMarkdown(single({ type: 'external', url }))), `![](${url})\n`) assert.equal(markdown(adfToMarkdown(single({ alt: 'a [b] c', type: 'external', url }))), `![a \\[b\\] c](${url})\n`) - assert.equal(code(adfToMarkdown(single({ alt: '', type: 'external', url }))), 'ambiguous-empty-media-alt') + assert.equal(code(adfToMarkdown(single({ alt: '', type: 'external', url }))), 'ambiguous-attribute-spelling') assert.equal(code(adfToMarkdown(single({ alt: 'a\nb', type: 'external', url }))), 'unspellable-whitespace') assert.equal(code(adfToMarkdown(single({ alt: 'a\u0000b', type: 'external', url }))), 'unspellable-character') assert.equal(code(adfToMarkdown(single({ type: 'external', url: 'https://example.com/a b>c' }))), 'unspellable-link-destination') diff --git a/src/adf-to-markdown.ts b/src/adf-to-markdown.ts index 54c5b1a..5e23d8c 100644 --- a/src/adf-to-markdown.ts +++ b/src/adf-to-markdown.ts @@ -178,7 +178,7 @@ function spellCodeFenceInfo(language: JsonValue | undefined, path: ConvertErrorP if (language === undefined) return success('') if (typeof language !== 'string') return failure('unsupported-node-shape', 'a codeBlock language is no string', path) if (language === '') { - return failure('ambiguous-empty-code-block-language', 'an empty codeBlock language and an absent one share one markdown spelling', path) + return failure('ambiguous-attribute-spelling', 'an empty codeBlock language and an absent one share one markdown spelling', path) } if (language === 'adf') return failure('reserved-adf-language', 'the adf info string is reserved for the opaque carry', path) if (/[`\n\r]/.test(language) || language !== language.trim()) { @@ -227,7 +227,7 @@ function listStart(node: AdfNode, items: number, path: ConvertErrorPath): Result if (node.type !== 'orderedList') return success(0) const start = node.attrs?.['order'] if (start === undefined || start === 1) { - return failure('ambiguous-ordered-list-start', 'an orderedList starting at 1 and one with no order share one markdown spelling', path) + return failure('ambiguous-attribute-spelling', 'an orderedList starting at 1 and one with no order share one markdown spelling', path) } if (typeof start !== 'number' || !Number.isInteger(start) || start < 0 || start > largestListMarker) { return failure('unsupported-node-shape', `no list marker spells the order ${JSON.stringify(start)}`, path) diff --git a/src/markdown-image.ts b/src/markdown-image.ts index 5778ad8..f7d8317 100644 --- a/src/markdown-image.ts +++ b/src/markdown-image.ts @@ -10,7 +10,7 @@ export function emitImage(node: AdfNode, path: ConvertErrorPath): Result const image = imageShape(node) if (image === undefined) return undefined const mediaPath = [...path, 'content', 0] - if (image.alt === '') return failure('ambiguous-empty-media-alt', 'an empty media alt and an absent one share one image spelling', mediaPath) + if (image.alt === '') return failure('ambiguous-attribute-spelling', 'an empty media alt and an absent one share one image spelling', mediaPath) return emitImageLine(image.alt, image.url, mediaPath) } diff --git a/src/result.ts b/src/result.ts index f2edebd..00a1a9e 100644 --- a/src/result.ts +++ b/src/result.ts @@ -1,7 +1,5 @@ export type ConvertErrorCode = - | 'ambiguous-empty-code-block-language' - | 'ambiguous-empty-media-alt' - | 'ambiguous-ordered-list-start' + | 'ambiguous-attribute-spelling' | 'not-an-adf-document' | 'reserved-adf-language' | 'unspellable-adjacent-lists'