Read the container blocks, and part a nested list the tight spelling would swallow
CI / gate (push) Successful in 6s

This commit is contained in:
2026-08-28 14:46:14 +02:00
parent 1bc456f732
commit e73de651be
24 changed files with 540 additions and 150 deletions
+7 -7
View File
@@ -159,8 +159,8 @@ test('breaks a mark run at the node it carries', () => {
test('refuses a carried node nested deeper than the emitter carries', () => {
let node: AdfNode = { type: 'blockCard' }
for (let depth = 0; depth < 600; depth += 1) node = { content: [node], type: 'blockCard' }
assert.equal(code(adfToMarkdown(document(node))), 'unsupported-node-shape')
assert.equal(code(adfToMarkdown(document(paragraph(node)))), 'unsupported-node-shape')
assert.equal(code(adfToMarkdown(document(node))), 'unsupported-nesting-depth')
assert.equal(code(adfToMarkdown(document(paragraph(node)))), 'unsupported-nesting-depth')
})
test('refuses a node whose content model the canonical form cannot emit', () => {
@@ -261,19 +261,19 @@ test('refuses a node carrying one mark type twice', () => {
assert.equal(code(adfToMarkdown(document(paragraph({ marks: [em, em], text: 'x', type: 'text' })))), 'unsupported-node-shape')
})
test('refuses a nested list the tight spelling would swallow', () => {
test('parts a nested list the tight spelling would swallow from the block above it', () => {
const item = (...content: AdfNode[]): AdfNode => ({ content, type: 'listItem' })
const text = (value: string): AdfNode => ({ content: [{ text: value, type: 'text' }], type: 'paragraph' })
const outer = (...content: AdfNode[]): AdfDocument => document({ content: [item(...content)], type: 'bulletList' })
const ordered: AdfNode = { attrs: { order: 2 }, content: [item(text('b'))], type: 'orderedList' }
assert.equal(code(adfToMarkdown(outer(text('a'), ordered))), 'unspellable-line-start')
assert.equal(code(adfToMarkdown(outer(text('a'), { content: [item()], type: 'bulletList' }))), 'unspellable-line-start')
assert.equal(markdown(adfToMarkdown(outer(text('a'), ordered))), '- a\n\n 2. b\n')
assert.equal(markdown(adfToMarkdown(outer(text('a'), { content: [item()], type: 'bulletList' }))), '- a\n\n -\n')
assert.equal(markdown(adfToMarkdown(outer(text('a'), { content: [item(text('b'))], type: 'bulletList' }))), '- a\n - b\n')
})
test('refuses marks and attributes nested deeper than the emitter carries', () => {
const marks: AdfMark[] = Array.from({ length: 600 }, (_, index) => ({ type: index % 2 === 0 ? 'em' : 'strong' }))
assert.equal(code(adfToMarkdown(document(paragraph({ marks, text: 'x', type: 'text' })))), 'unsupported-node-shape')
assert.equal(code(adfToMarkdown(document(paragraph({ marks, text: 'x', type: 'text' })))), 'unsupported-nesting-depth')
let attrs: AdfMark['attrs'] = { depth: 'x' }
for (let depth = 0; depth < 600; depth += 1) attrs = { depth: attrs }
assert.equal(code(adfToMarkdown(document(paragraph({ marks: [{ attrs, type: 'em' }], text: 'x', type: 'text' })))), 'not-an-adf-document')
@@ -380,7 +380,7 @@ test('spells one code span over a run of code-marked nodes', () => {
test('refuses a document nested deeper than the emitter carries', () => {
let node: AdfNode = paragraph({ text: 'x', type: 'text' })
for (let depth = 0; depth < 600; depth += 1) node = { content: [node], type: 'blockquote' }
assert.equal(code(adfToMarkdown(document(node))), 'unsupported-node-shape')
assert.equal(code(adfToMarkdown(document(node))), 'unsupported-nesting-depth')
})
test('emits an empty list item without trailing whitespace', () => {
+2 -7
View File
@@ -31,7 +31,7 @@ export function adfToMarkdown(document: AdfDocument): Result<string> {
}
function emitBlocks(nodes: readonly AdfNode[], container: BlockContainer, path: ConvertErrorPath, depth: number): Result<EmittedBody> {
if (depth > largestNesting) return failure('unsupported-node-shape', `the document nests deeper than the ${largestNesting} levels the emitter carries`, path)
if (depth > largestNesting) return failure('unsupported-nesting-depth', `the document nests deeper than the ${largestNesting} levels the emitter carries`, path)
const blocks: PlacedBlock[] = []
for (const [index, node] of nodes.entries()) {
const nodePath = [...path, 'content', index]
@@ -60,12 +60,7 @@ function separationBetween(previous: PlacedBlock, next: PlacedBlock, container:
if (previous.node.type === next.node.type) {
return failure('unspellable-adjacent-lists', `two adjacent ${next.node.type} nodes read back as one list`, next.path)
}
if (container === 'list-item') {
if (!interruptsParagraph(next.node)) {
return failure('unspellable-line-start', `a ${next.node.type} that cannot interrupt the block above it has no tight spelling`, next.path)
}
return success('\n')
}
if (container === 'list-item') return success(interruptsParagraph(next.node) ? '\n' : '\n\n')
}
if (container !== 'directive' || plainPair) return success('\n\n')
if (previous.spelling === 'directive' && next.spelling === 'directive') return success('\n')
+1 -1
View File
@@ -140,7 +140,7 @@ function refuseContentAndText(node: AdfNode, path: ConvertErrorPath): Result<nul
function emitRun(nodes: readonly AdfNode[], depth: number, firstIndex: number, context: InlineContext): Result<Emission> {
if (depth > largestNesting) {
return failure('unsupported-node-shape', `the marks nest deeper than the ${largestNesting} levels the emitter carries`, context.path)
return failure('unsupported-nesting-depth', `the marks nest deeper than the ${largestNesting} levels the emitter carries`, context.path)
}
const runs = inlineRuns(nodes, depth, firstIndex, context.carried)
const segments: InlineSegment[] = []