From 6f32c6fc61af310ba128fe8bd132f1d007a83d98 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Wed, 26 Aug 2026 16:54:22 +0200 Subject: [PATCH] Refuse a fence info string that decodes to the reserved carry name --- AGENTS.md | 3 ++- src/adf-to-markdown.test.ts | 1 + src/adf-to-markdown.ts | 5 ++++- src/markdown-inline.ts | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 60544bb..d8748a9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -151,7 +151,8 @@ One-line commit messages and PR titles; short PR summaries. No AI-attribution ma No wiki markup (§1), no network or filesystem I/O, no name→id resolution (§3), no ADF schema validation or exported validator — a refusal that keeps the round-trip is not schema validation, -so the one a node carrying the same mark type twice earns stays, no shipped CSS (§4), no streaming APIs, no performance budget — +so the one a spelled node carrying the same mark type twice earns stays, no shipped CSS (§4), no +streaming APIs, no performance budget — conversions are O(n), real documents are kilobytes. A CLI is a later goal (`todo.md`), not a non-goal. diff --git a/src/adf-to-markdown.test.ts b/src/adf-to-markdown.test.ts index df573fd..ffd318b 100644 --- a/src/adf-to-markdown.test.ts +++ b/src/adf-to-markdown.test.ts @@ -62,6 +62,7 @@ test('refuses the code block info strings the fence cannot hold', () => { assert.equal(code(adfToMarkdown(document({ attrs: { language: '' }, type: 'codeBlock' }))), 'ambiguous-attribute-spelling') 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') + assert.equal(code(adfToMarkdown(document({ attrs: { language: 'adf' }, type: 'codeBlock' }))), 'unspellable-code-block-language') }) test('refuses a link destination CommonMark cannot spell', () => { diff --git a/src/adf-to-markdown.ts b/src/adf-to-markdown.ts index 1e1983d..7c98bb4 100644 --- a/src/adf-to-markdown.ts +++ b/src/adf-to-markdown.ts @@ -7,7 +7,7 @@ import { emitInlineLine } from './markdown-inline.ts' import { tryPipeTable } from './markdown-pipe-table.ts' import { carriedBlock, carryName } from './opaque-carry.ts' import { failure, success, type ConvertErrorPath, type Result } from './result.ts' -import { holdsNullCharacter, isThematicBreak } from './commonmark-grammar.ts' +import { holdsEntityReference, holdsNullCharacter, isThematicBreak } from './commonmark-grammar.ts' import { isAdfDocument } from './adf-document.ts' import { largestNesting } from './nesting.ts' import { fencedCodeBlock } from './backtick-runs.ts' @@ -187,6 +187,9 @@ function spellCodeFenceInfo(language: JsonValue | undefined, path: ConvertErrorP if (/[`\n\r]/.test(language) || language !== language.trim()) { return failure('unspellable-code-block-language', 'a fence info string holds no backtick and no edge whitespace', path) } + if (holdsEntityReference(language)) { + return failure('unspellable-code-block-language', 'a fence info string shaped like an entity reference decodes on the way back', path) + } return success(language) } diff --git a/src/markdown-inline.ts b/src/markdown-inline.ts index 6d77fb1..3e78fcf 100644 --- a/src/markdown-inline.ts +++ b/src/markdown-inline.ts @@ -132,6 +132,7 @@ function inlineRuns(nodes: readonly AdfNode[], depth: number, firstIndex: number const runs: InlineRun[] = [] for (const [offset, node] of nodes.entries()) { const index = firstIndex + offset + // spec/flavour.md, Marks. const mark = carries(node) ? undefined : (node.marks ?? [])[depth] if (mark === undefined) { runs.push({ index, kind: 'plain', node }) @@ -148,7 +149,6 @@ function nodePath(context: InlineContext, index: number): ConvertErrorPath { return [...context.path, 'content', index] } -// spec/flavour.md, Marks. function carries(node: AdfNode): boolean { return node.type !== 'hardBreak' && node.type !== 'text' && inlineDirective(node.type) === undefined }