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

Answer stability round 1: 2 🔴, 1 🟡 (0 misread); refuse a link-marked card in the carry readers, and pin the order of two

This commit is contained in:
2026-11-04 20:30:08 +01:00
committed by Lillem4n
parent 13f03a4281
commit deae360969
5 changed files with 32 additions and 8 deletions
+4 -2
View File
@@ -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<BoundedJso
}
export function carriedNode({ bounded }: BoundedJson, holder: string): Read<AdfNode> {
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 {
+8 -1
View File
@@ -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' }
+5 -5
View File
@@ -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<readonly AdfNode[
if (document.version !== 1) return failure('unsupported-document-version', `no format this library writes carries ADF version ${document.version}`, [])
if (document.content === undefined) return success(undefined)
const refused = refusedShape(document.content)
if (refused !== undefined) return refused
if (refused !== undefined) return { error: refused, ok: false }
// Text alone past the bound is refused before anything is written (docs/decisions.md §Output past V8's longest string is refused on every engine).
return withinLongest(heldTextLength(document.content), document.content)
}
@@ -147,8 +147,8 @@ export function nodeMarks(node: { marks?: AdfMark[] }): readonly AdfMark[] {
return node.marks ?? []
}
// The writers' refusals of what Atlassian's schema forbids, in document order, at any depth and inside nodes no writer reads. Each rule stands on the schema alone.
function refusedShape(content: readonly AdfNode[]): Result<never> | 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<never> | 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)
}
+7
View File
@@ -180,6 +180,13 @@ test('reads an image standing alone in a paragraph, a link around it where one m
assert.deepEqual(read(htmlToAdf('<p><a href="https://example.com/" title="Home"><img src="https://example.com/moon.jpg"></a></p>\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 [`<p>${carry('span', card, '')}</p>\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, '<p>Check the collation.</p>')}\n<p>Ask ${carry('span', mention, '@Mikael')}</p>\n`)), document(panel, paragraph(text('Ask '), mention)))
assert.deepEqual(read(htmlToAdf(`<p>${carry('span', text('a\u0085b', { type: 'underline' }), 'ab').replace('\u0085', '\\u0085')}</p>\n`)), document(paragraph(text('a\u0085b', { type: 'underline' }))))
@@ -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')