From 75791783d88844944e9f8b90476addcab889345b Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Tue, 22 Sep 2026 21:51:47 +0200 Subject: [PATCH] 32 - a mark's attribute depth is counted from its value, the marks spelling refusing its own --- AGENTS.md | 8 ++++---- src/adf/document.test.ts | 6 +++--- src/adf/document.ts | 13 +++++-------- src/markdown/emit/adf-to-markdown.test.ts | 9 +++++++-- src/markdown/emit/adf-to-markdown.ts | 10 ++++++---- src/markdown/emit/block-directive-spelling.ts | 14 ++++++++++---- 6 files changed, 35 insertions(+), 25 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 1257ae1..9496e3e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -163,10 +163,10 @@ shape, and threading a path to the ninth โ€” a malformed node anywhere in the tr manual stack ยง11's no-recursion rule forces, whose empty half no input reaches. The message names the violation instead. Depth is not one of the nine: `adfDocumentFault` returns the code with the message, so an attribute value past 500 levels is `unsupported-nesting-depth` from the emitter as -it already is from the parser, both directions refusing the same value. A node's attribute is -counted from the value itself, never from the `attrs` object holding it; a mark's is counted three -levels in, because the block directive spells the whole mark set as one JSON attribute and the -parser reads the value at the bottom of array, mark and `attrs`. `isAdfDocument` is true for a depth fault: +it already is from the parser, both directions refusing the same value. An attribute, a node's or +a mark's, is counted from the value itself, never from the `attrs` object holding it; a spelling +nesting the value deeper โ€” the block directive's marks attribute, three levels in, or the carry โ€” +refuses in its own format, as that format's parser does. `isAdfDocument` is true for a depth fault: a deep document is a document, as the 2000-level blocks and the 600-deep marks the guard already waves through are, and depth is the walks' answer rather than the shape's. A non-finite number stays parted where depth is joined: the parse says `unsupported-node-shape` because the markdown is diff --git a/src/adf/document.test.ts b/src/adf/document.test.ts index 4efd5f9..c6a1b93 100644 --- a/src/adf/document.test.ts +++ b/src/adf/document.test.ts @@ -68,9 +68,9 @@ test('names the attribute nesting past the levels the parser reads one at, and s assert.equal(faultCode(withAttribute(nested(largestNesting + 1))), 'unsupported-nesting-depth') assert.equal(isAdfDocument(withAttribute(nested(largestNesting + 1))), true) const marked = (levels: number): unknown => ({ content: [{ marks: [{ attrs: { a: nested(levels) }, type: 'link' }], text: 'x', type: 'text' }], type: 'doc', version: 1 }) - assert.equal(fault(marked(largestNesting - 3)), 'accepted') - assert.equal(fault(marked(largestNesting - 2)), deeper('a', 'link', largestNesting - 3)) - assert.equal(isAdfDocument(marked(largestNesting - 2)), true) + assert.equal(fault(marked(largestNesting)), 'accepted') + assert.equal(fault(marked(largestNesting + 1)), deeper('a', 'link')) + assert.equal(isAdfDocument(marked(largestNesting + 1)), true) }) test('accepts the JSON values an attribute may hold', () => { diff --git a/src/adf/document.ts b/src/adf/document.ts index 765ab9b..490c5ac 100644 --- a/src/adf/document.ts +++ b/src/adf/document.ts @@ -23,9 +23,6 @@ export type AdfDocument = { version: number } -// A block directive spells the whole mark set as one JSON attribute, so a mark's value sits three levels inside it. -const markAttributeNesting = largestNesting - 3 - const documentKeys = ['content', 'type', 'version'] const markKeys = ['attrs', 'type'] const nodeKeys = ['attrs', 'content', 'marks', 'text', 'type'] @@ -47,8 +44,8 @@ export function adfDocumentFault(value: unknown): ConvertFault | undefined { return nestingFault(content) } -export function attributeNestingMessage(key: string, type: string, levels: number = largestNesting): string { - return `the ${key} attribute of ${type} nests deeper than the ${levels} levels an attribute carries` +export function attributeNestingMessage(key: string, type: string): string { + return `the ${key} attribute of ${type} nests deeper than the ${largestNesting} levels an attribute carries` } export function carriesOnly(node: AdfNode, attributes: readonly string[]): boolean { @@ -116,15 +113,15 @@ function nestingFault(nodes: readonly AdfNode[]): ConvertFault | undefined { function marksFault(marks: readonly AdfMark[]): ConvertFault | undefined { for (const mark of marks) { - const fault = attributesFault(nodeAttrs(mark), mark.type, markAttributeNesting) + const fault = attributesFault(nodeAttrs(mark), mark.type) if (fault !== undefined) return fault } return undefined } -function attributesFault(attrs: AdfAttributes, type: string, levels: number = largestNesting): ConvertFault | undefined { +function attributesFault(attrs: AdfAttributes, type: string): ConvertFault | undefined { for (const [key, value] of Object.entries(attrs)) { - if (overNested(value, levels)) return { code: 'unsupported-nesting-depth', message: attributeNestingMessage(key, type, levels) } + if (overNested(value)) return { code: 'unsupported-nesting-depth', message: attributeNestingMessage(key, type) } } return undefined } diff --git a/src/markdown/emit/adf-to-markdown.test.ts b/src/markdown/emit/adf-to-markdown.test.ts index e5a2969..ec45af7 100644 --- a/src/markdown/emit/adf-to-markdown.test.ts +++ b/src/markdown/emit/adf-to-markdown.test.ts @@ -375,11 +375,16 @@ test('refuses marks and attributes nested deeper than the emitter carries', () = assert.deepEqual(toEditorNormal(read.value), document(node)) } - assert.equal(markdown(adfToMarkdown(document(paragraph({ marks: [{ attrs, type: 'em' }], text: 'x', type: 'text' })))), deeper('depth', 'em', largestNesting - 3)) + assert.equal(markdown(adfToMarkdown(document(paragraph({ marks: [{ attrs, type: 'em' }], text: 'x', type: 'text' })))), deeper('depth', 'em')) + assert.equal( + markdown(adfToMarkdown(document(paragraph({ marks: [{ attrs: { deep: nested(largestNesting) }, type: 'em' }], text: 'x', type: 'text' })))), + `unsupported-nesting-depth: a carried node's JSON nests deeper than the ${largestNesting} levels its position leaves`, + ) assert.equal(markdown(adfToMarkdown(document(paragraph(card(largestNesting + 1))))), deeper('data', 'inlineCard')) assert.deepEqual(path(adfToMarkdown(document(paragraph(card(largestNesting + 1))))), []) roundTrips(paragraph(card(largestNesting))) - assert.equal(markdown(adfToMarkdown(document(marked(largestNesting - 2)))), deeper('deep', 'em', largestNesting - 3)) + assert.equal(markdown(adfToMarkdown(document(marked(largestNesting - 2)))), deeper('marks', 'panel')) + assert.deepEqual(path(adfToMarkdown(document(marked(largestNesting - 2)))), ['content', 0]) roundTrips(marked(largestNesting - 3)) }) diff --git a/src/markdown/emit/adf-to-markdown.ts b/src/markdown/emit/adf-to-markdown.ts index f0c176a..10d2825 100644 --- a/src/markdown/emit/adf-to-markdown.ts +++ b/src/markdown/emit/adf-to-markdown.ts @@ -147,9 +147,10 @@ function emitDirectiveBlock(node: AdfNode, model: BlockNodeModel, path: ConvertE if (node.text !== undefined) return failure('unsupported-node-shape', `a ${node.type} carries no text: this one holds text`, path) if (blockDirectiveForm(node.type) === 'leaf' && nodeContent(node).length > 0) return failure('unsupported-node-shape', `a ${node.type} holds no content: this one holds some`, path) if (model.contentModel === 'code') return emitCodeDirective(node, model, path, depth) - const opener = spellBlockDirectiveOpener(node, model) + const opener = spellBlockDirectiveOpener(node, model, path) if (opener === undefined) return commonMarkLine(carriedBlock(node, path, depth)) - return emitDirectiveBody(node, model, opener, path, walkBody) + if (!opener.ok) return opener + return emitDirectiveBody(node, model, opener.value, path, walkBody) } function emitDirectiveBody(node: AdfNode, model: BlockNodeModel, opener: string, path: ConvertErrorPath, walkBody: () => Result): Result { @@ -186,11 +187,12 @@ function tryCodeBlock(node: AdfNode, path: ConvertErrorPath): Result { const slot = languageSlot(nodeAttrs(node)['language']) - const opener = spellBlockDirectiveOpener(node, model, slot.kind === 'attribute' ? [] : ['language']) + const opener = spellBlockDirectiveOpener(node, model, path, slot.kind === 'attribute' ? [] : ['language']) if (opener === undefined) return commonMarkLine(carriedBlock(node, path, depth)) + if (!opener.ok) return opener const text = codeBlockText(node, path) if (!text.ok) return text - return success(directivePair(node, opener, fencedCodeBlock(slot.kind === 'fence' ? slot.info : '', text.value))) + return success(directivePair(node, opener.value, fencedCodeBlock(slot.kind === 'fence' ? slot.info : '', text.value))) } function codeBlockText(node: AdfNode, path: ConvertErrorPath): Result { diff --git a/src/markdown/emit/block-directive-spelling.ts b/src/markdown/emit/block-directive-spelling.ts index 5598932..1460699 100644 --- a/src/markdown/emit/block-directive-spelling.ts +++ b/src/markdown/emit/block-directive-spelling.ts @@ -1,12 +1,14 @@ import type { AdfNode } from '../../adf/document.ts' import type { BlockNodeModel } from '../../adf/block-nodes.ts' +import { attributeNestingMessage, nodeAttrs, nodeMarks } from '../../adf/document.ts' import { blockArgument } from '../block-directive-arguments.ts' +import { failure, success, type ConvertErrorPath, type Result } from '../../result.ts' import { isBareToken, spellAttributes, spellDirectiveOpener, spellJsonAttribute, spellVocabulary } from '../directive-syntax.ts' import { markValues, marksAttribute } from '../block-directive-marks.ts' -import { nodeAttrs, nodeMarks } from '../../adf/document.ts' +import { overNested } from '../../json-value.ts' import { vocabularyPairs } from '../../adf/attribute-vocabulary.ts' -export function spellBlockDirectiveOpener(node: AdfNode, model: BlockNodeModel, spelledByBody: readonly string[] = []): string | undefined { +export function spellBlockDirectiveOpener(node: AdfNode, model: BlockNodeModel, path: ConvertErrorPath, spelledByBody: readonly string[] = []): Result | undefined { const argumentAttribute = blockArgument(node.type) const slot = bareArgument(node, argumentAttribute) if (slot === undefined) return undefined @@ -15,8 +17,12 @@ export function spellBlockDirectiveOpener(node: AdfNode, model: BlockNodeModel, if (pairs === undefined) return undefined const spelledPairs = spellVocabulary(pairs) const marks = nodeMarks(node) - if (marks.length > 0) spelledPairs.push([marksAttribute, spellJsonAttribute(markValues(marks))]) - return spellDirectiveOpener(node.type, slot.argument, spellAttributes(spelledPairs)) + if (marks.length > 0) { + const values = markValues(marks) + if (overNested(values)) return failure('unsupported-nesting-depth', attributeNestingMessage(marksAttribute, node.type), path) + spelledPairs.push([marksAttribute, spellJsonAttribute(values)]) + } + return success(spellDirectiveOpener(node.type, slot.argument, spellAttributes(spelledPairs))) } // `undefined` where the argument slot holds a value no bare token spells.