From deae3609692b48e56c6f9db0b89821cbbeb55d2a Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Wed, 4 Nov 2026 20:30:08 +0100 Subject: [PATCH] =?UTF-8?q?Answer=20stability=20round=201:=202=20?= =?UTF-8?q?=F0=9F=94=B4,=201=20=F0=9F=9F=A1=20(0=20misread);=20refuse=20a?= =?UTF-8?q?=20link-marked=20card=20in=20the=20carry=20readers,=20and=20pin?= =?UTF-8?q?=20the=20order=20of=20two?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/adf/carried-json.ts | 6 ++++-- src/adf/document.test.ts | 9 ++++++++- src/adf/document.ts | 10 +++++----- src/html/parse/html-to-adf.test.ts | 7 +++++++ src/markdown/parse/markdown-to-adf.test.ts | 8 ++++++++ 5 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/adf/carried-json.ts b/src/adf/carried-json.ts index 309070d..0c3f8b9 100644 --- a/src/adf/carried-json.ts +++ b/src/adf/carried-json.ts @@ -3,7 +3,7 @@ import type { JsonSpelling } from '../canonical-json.ts' import type { JsonValue } from '../json-value.ts' import { canonicalJsonPieces } from '../canonical-json.ts' import { faulted, success, type ConvertErrorPath, type ConvertFault, type Read, type Result } from '../result.ts' -import { isAdfNode } from './document.ts' +import { isAdfNode, refusedShape } from './document.ts' import { isJsonValue, nestingDepth, overNested } from '../json-value.ts' import { joinedWithin } from '../length-bound.ts' import { parseJsonText } from '../json-text.ts' @@ -32,7 +32,9 @@ export function carriedNesting(held: JsonValue, levels: number): Read { - return isAdfNode(bounded) ? { value: bounded } : { fault: { code: 'unsupported-node-shape', message: `${holder} holds one ADF node's JSON: this JSON is no ADF node` } } + if (!isAdfNode(bounded)) return { fault: { code: 'unsupported-node-shape', message: `${holder} holds one ADF node's JSON: this JSON is no ADF node` } } + const refused = refusedShape([bounded]) + return refused === undefined ? { value: bounded } : { fault: { code: refused.code, message: `${holder} holds a node every writer refuses: ${refused.message}` } } } function nestedPast(levels: number): ConvertFault { diff --git a/src/adf/document.test.ts b/src/adf/document.test.ts index a5d2d7d..ec3058c 100644 --- a/src/adf/document.test.ts +++ b/src/adf/document.test.ts @@ -152,6 +152,13 @@ test('refuses an inline card a link marks, wherever the document holds it and wh } }) +test('names the first link-marked card in document order', () => { + const card = (url: string): AdfNode => ({ attrs: { url }, marks: [{ attrs: { href: 'https://example.com/' }, type: 'link' }], type: 'inlineCard' }) + const content = [{ content: [card('https://a.example/'), card('https://b.example/')], type: 'paragraph' }, { content: [card('https://c.example/')], type: 'paragraph' }] + const refused = writableContent({ content, type: 'doc', version: 1 }) + assert.deepEqual(!refused.ok && refused.error.path, ['content', 0, 'content', 0]) +}) + test('writes a card holding no link mark, and a node type it does not know holding one', () => { const comment = { attrs: { annotationType: 'inlineComment', id: 'c1' }, type: 'annotation' } const link = { attrs: { href: 'https://example.com/' }, type: 'link' } @@ -159,7 +166,7 @@ test('writes a card holding no link mark, and a node type it does not know holdi for (const write of [adfToHtml, adfToLosslessMarkdown, adfToPortableMarkdown]) assert.equal(write({ content: [marked], type: 'doc', version: 1 }).ok, true) }) -test('names a link-marked card at any depth by its path alone, in time linear in the depth', () => { +test('names a link-marked card at any depth by its path alone, with no step quadratic in the depth', () => { const depth = 100_000 let node: AdfNode = { attrs: { url: 'https://example.com/' }, marks: [{ attrs: { href: 'https://example.com/' }, type: 'link' }], type: 'inlineCard' } for (let level = 0; level < depth; level += 1) node = { content: [node], type: 'panel' } diff --git a/src/adf/document.ts b/src/adf/document.ts index d05df5d..73e8a89 100644 --- a/src/adf/document.ts +++ b/src/adf/document.ts @@ -1,4 +1,4 @@ -import { failure, faulted, success, type ConvertFault, type Result } from '../result.ts' +import { failure, faulted, success, type ConvertError, type ConvertFault, type Result } from '../result.ts' import { isJsonValue, overNested, type JsonValue } from '../json-value.ts' import { largestNesting } from '../nesting.ts' import { withinLongest } from '../length-bound.ts' @@ -56,7 +56,7 @@ export function writableContent(document: AdfDocument): Result | undefined { +// Walks every node, carried or of an unknown type, so a link-marked card is refused wherever it stands (docs/decisions.md §Writers and carry readers refuse an inline card a link marks). +export function refusedShape(content: readonly AdfNode[]): ConvertError | undefined { type Step = { index: number; node: AdfNode; parent: Step | undefined } const pending: Step[] = content.map((node, index) => ({ index, node, parent: undefined })).reverse() for (let step = pending.pop(); step !== undefined; step = pending.pop()) { @@ -156,7 +156,7 @@ function refusedShape(content: readonly AdfNode[]): Result | undefined { const path: (number | string)[] = [] for (let up: Step | undefined = step; up !== undefined; up = up.parent) path.push(up.index, 'content') path.reverse() - return failure('unsupported-node-shape', 'an inlineCard takes no link mark: this one holds one', path) + return { code: 'unsupported-node-shape', message: 'an inlineCard takes no link mark: this one holds one', path } } for (const child of nodeContent(step.node).map((node, index) => ({ index, node, parent: step })).reverse()) pending.push(child) } diff --git a/src/html/parse/html-to-adf.test.ts b/src/html/parse/html-to-adf.test.ts index 8516f12..90ea354 100644 --- a/src/html/parse/html-to-adf.test.ts +++ b/src/html/parse/html-to-adf.test.ts @@ -180,6 +180,13 @@ test('reads an image standing alone in a paragraph, a link around it where one m assert.deepEqual(read(htmlToAdf('

\n')), document(linked)) }) +test('refuses a carry element holding an inline card a link marks, at any depth in the node it carries', () => { + const card = { attrs: { url: 'https://e.com/r' }, marks: [{ attrs: { href: 'https://e.com/' }, type: 'link' }], type: 'inlineCard' } + for (const html of [`

${carry('span', card, '')}

\n`, `${carry('div', { content: [{ content: [card], type: 'futureNode' }], type: 'futureBlock' }, '')}\n`]) { + assert.match(message(htmlToAdf(html)), /^a carry element's data-adf holds a node every writer refuses: an inlineCard takes no link mark/) + } +}) + test('reads a carry element from its data-adf, what it shows read back as adfToHtml writes it', () => { assert.deepEqual(read(htmlToAdf(`${carry('div', panel, '

Check the collation.

')}\n

Ask ${carry('span', mention, '@Mikael')}

\n`)), document(panel, paragraph(text('Ask '), mention))) assert.deepEqual(read(htmlToAdf(`

${carry('span', text('a\u0085b', { type: 'underline' }), 'ab').replace('\u0085', '\\u0085')}

\n`)), document(paragraph(text('a\u0085b', { type: 'underline' })))) diff --git a/src/markdown/parse/markdown-to-adf.test.ts b/src/markdown/parse/markdown-to-adf.test.ts index 1320d19..c70fd6d 100644 --- a/src/markdown/parse/markdown-to-adf.test.ts +++ b/src/markdown/parse/markdown-to-adf.test.ts @@ -489,6 +489,14 @@ test('names the node JSON an opaque carry restores alone', () => { assert.equal(content(losslessMarkdownToAdf('!adf:carry{json="{\\"kind\\":\\"x\\"}"}\n')), `${node}\\!adf: keeps the prefix literal`) }) +test('refuses a carry holding an inline card a link marks, whether it stands alone, in a block or in a fence', () => { + const card = { attrs: { url: 'https://e.com/r' }, marks: [{ attrs: { href: 'https://e.com/' }, type: 'link' }], type: 'inlineCard' } + const json = JSON.stringify(card).replaceAll('"', '\\"') + for (const markdown of [`!adf:carry{json="${json}"}\n`, `\`\`\`adf:futureBlock\n${JSON.stringify({ content: [card] }, null, 2)}\n\`\`\`\n`]) { + assert.match(String(content(losslessMarkdownToAdf(markdown))), /^unsupported-node-shape: the opaque carry holds a node every writer refuses: an inlineCard takes no link mark/) + } +}) + test('names the shape the inline carry reads alone', () => { assert.equal(content(losslessMarkdownToAdf('!adf:carry[x]{json="{}"}\n')), 'unsupported-node-shape: carry takes no content: this one holds some') assert.equal(content(losslessMarkdownToAdf('!adf:carry{}\n')), 'unsupported-node-shape: carry holds one json attribute alone: this one does not')