From 356b7512c84e145bfedfdc92823b68485936c0a7 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Wed, 16 Sep 2026 19:40:30 +0200 Subject: [PATCH] 12c: the block walk opens no container past the depth the node walk refuses, and the closer messages name the escape --- src/markdown/parse/blocks.test.ts | 2 +- src/markdown/parse/blocks.ts | 13 +++++++++---- src/markdown/parse/markdown-to-adf.test.ts | 13 ++++++++++--- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/markdown/parse/blocks.test.ts b/src/markdown/parse/blocks.test.ts index b99f357..f4061be 100644 --- a/src/markdown/parse/blocks.test.ts +++ b/src/markdown/parse/blocks.test.ts @@ -106,7 +106,7 @@ test('opens a container where the content model takes content, and holds it open }) test('closes the containers a closer names past as unclosed, and crosses no list item or blockquote edge', () => { - const unclosed = (name: string): string => `the ${name} container is unclosed: no !adf:/${name} follows inside the block holding it` + const unclosed = (name: string): string => `the ${name} container is unclosed: no !adf:/${name} follows inside the block holding it; \\!adf: keeps the prefix literal` const unopened = (name: string): string => `the closer !adf:/${name} closes no ${name} container open where it stands; \\!adf: keeps the prefix literal` assert.deepEqual(faults('!adf:panel info\n!adf:expand\nPart.\n!adf:/expand\n!adf:/panel\n'), []) assert.deepEqual(faults('!adf:panel info\n!adf:expand\n!adf:layoutSection\nPart.\n!adf:/panel\n'), [unclosed('expand')]) diff --git a/src/markdown/parse/blocks.ts b/src/markdown/parse/blocks.ts index 05f3791..cb55c5d 100644 --- a/src/markdown/parse/blocks.ts +++ b/src/markdown/parse/blocks.ts @@ -17,6 +17,7 @@ import { } from '../commonmark-grammar.ts' import { blockDirectiveForm } from '../block-directive-forms.ts' import { directiveEscape, malformedDirective, readDirectiveLine, spellDirectiveCloser } from '../directive-syntax.ts' +import { largestNesting } from '../../nesting.ts' import { barePipeCells, isDelimiterRow, isPipeAlignment, isPipeDelimiter, malformedPipeTable, pipeCells } from '../pipe-table-syntax.ts' import { readLinkDefinitions } from '../link-reference-definitions.ts' @@ -115,7 +116,7 @@ function matchContainers(walk: Walk, line: Line): { depth: number; rest: Line } function continuesContainer(walk: Walk, container: OpenContainer, line: Line): Line | undefined { if (container.kind === 'blockquote') return blockquoteRest(removeColumns(line, largestOpenerIndentation)) - // A directive container has no continuation marker: only its own closer closes it. + // A directive container has no continuation marker. if (container.kind === 'directive') return line // A list item begins with at most one blank line: an empty one gives the second up. if (isBlankLine(line.text)) { @@ -203,7 +204,7 @@ function closeContainers(walk: Walk, depth: number): void { for (const container of walk.stack.slice(depth)) { if (container.kind !== 'directive') continue container.parent[container.index] = { - fault: malformedDirective(`the ${container.name} container is unclosed: no ${spellDirectiveCloser(container.name)} follows inside the block holding it`), + fault: malformedDirective(`the ${container.name} container is unclosed: no ${spellDirectiveCloser(container.name)} follows inside the block holding it; ${directiveEscape}`), kind: 'fault', position: container.position, } @@ -222,6 +223,11 @@ function applyDirectiveLine(walk: Walk, directive: DirectiveLine): void { function openDirective(walk: Walk, directive: Extract): void { const { name } = directive + // The node walk refuses this depth anyway; a deeper stack only makes every later line walk it. + if (walk.stack.length > largestNesting && blockDirectiveForm(name) === 'container') { + pushFault(walk, { code: 'unsupported-nesting-depth', message: `the input nests directive containers deeper than the ${largestNesting} levels the parser carries` }) + return + } const block: DirectiveBlock = { argument: directive.argument, attributes: directive.attributes, @@ -240,7 +246,7 @@ function openDirective(walk: Walk, directive: Extract { const bare = 'unsupported-node-shape: listBreak spells the bare leaf form, !adf:listBreak: this one spells more' assert.equal(content(markdownToAdf('- a\n\n!adf:listBreak x\n\n- b\n')), bare) assert.equal(content(markdownToAdf('- a\n\n!adf:listBreak {id=x}\n\n- b\n')), bare) - assert.equal(content(markdownToAdf('- a\n\n!adf:listBreak\n- b\n!adf:/listBreak\n')), 'malformed-directive: listBreak takes no body, so no !adf:/listBreak closes it') + assert.equal(content(markdownToAdf('- a\n\n!adf:listBreak\n- b\n!adf:/listBreak\n')), 'malformed-directive: listBreak takes no body, so no !adf:/listBreak closes it; \\!adf: keeps the prefix literal') assert.equal(content(markdownToAdf('!adf:listBreak{}\n')), 'unsupported-node-shape: listBreak takes the block form, !adf:listBreak, never the inline form') assert.deepEqual(path(markdownToAdf('Part.\n\n!adf:listBreak\n')), ['content', 1]) }) @@ -300,7 +300,7 @@ test('names the directive name no node reads back to', () => { }) test('names the container no closer closes inside the block holding it', () => { - const unclosed = (name: string): string => `malformed-directive: the ${name} container is unclosed: no !adf:/${name} follows inside the block holding it` + const unclosed = (name: string): string => `malformed-directive: the ${name} container is unclosed: no !adf:/${name} follows inside the block holding it; \\!adf: keeps the prefix literal` assert.equal(content(markdownToAdf('Part.\n!adf:expand\n')), unclosed('expand')) assert.deepEqual(path(markdownToAdf('Part.\n!adf:expand\n')), ['content', 1]) assert.equal(content(markdownToAdf('!adf:bulletList\n')), unclosed('bulletList')) @@ -318,7 +318,7 @@ test('names the closer that finds no container open where it stands', () => { }) test('names the leaf given a body at its opener, ahead of any refusal the leaf holds itself', () => { - const body = (name: string): string => `malformed-directive: ${name} takes no body, so no !adf:/${name} closes it` + const body = (name: string): string => `malformed-directive: ${name} takes no body, so no !adf:/${name} closes it; \\!adf: keeps the prefix literal` assert.equal(content(markdownToAdf('!adf:rule\nPart.\n!adf:/rule\n')), body('rule')) assert.deepEqual(position(markdownToAdf('Part.\n\n!adf:rule\nPart.\n!adf:/rule\n')), { line: 3, offset: 7 }) assert.equal(content(markdownToAdf('- a\n\n!adf:listBreak\n!adf:/listBreak\n\n- b\n')), body('listBreak')) @@ -668,6 +668,13 @@ test('refuses input nested deeper than the parser carries', () => { const marks = (levels: number): string => `${'!adf:underline['.repeat(levels)}a${']'.repeat(levels)}\n` assert.equal(code(markdownToAdf(marks(largestNesting + 1))), 'unsupported-nesting-depth') assert.deepEqual(content(markdownToAdf(marks(largestNesting))), [{ content: [marked('a', underline)], type: 'paragraph' }]) + const panels = (levels: number, body: string): string => `${'!adf:panel\n'.repeat(levels)}${body}${'!adf:/panel\n'.repeat(levels)}` + assert.equal(code(markdownToAdf(panels(1000, ''))), 'unsupported-nesting-depth') + assert.ok(markdownToAdf(panels(largestNesting, '!adf:paragraph {localId=a-1}\nPart.\n!adf:/paragraph\n')).ok) + assert.equal( + content(markdownToAdf(panels(largestNesting, '!adf:paragraph {localId=a-1}\n!adf:panel\n!adf:/paragraph\n'))), + `unsupported-nesting-depth: the input nests directive containers deeper than the ${largestNesting} levels the parser carries`, + ) }) test('decodes the backslash escapes CommonMark spells, and keeps the rest literal', () => {