diff --git a/src/markdown/parse/markdown-to-adf.ts b/src/markdown/parse/markdown-to-adf.ts index 8e4bd4c..1d843bb 100644 --- a/src/markdown/parse/markdown-to-adf.ts +++ b/src/markdown/parse/markdown-to-adf.ts @@ -17,10 +17,10 @@ import { leadingMarker, readAlertMarker, readTaskMarker } from '../portable/conv import { mintTaskIds } from '../portable/task-ids.ts' import { parseBlocks } from './blocks.ts' import { parseInlineContent } from './inline-content.ts' +import { pathTo, splitTaskList } from '../portable/task-list.ts' import { readBlockDirectiveNode } from '../directive/node-reading.ts' import { spellStringAttribute, unsupportedNodeShape } from '../directive/syntax.ts' import { spellsEmpty } from '../directive/empty-keys.ts' -import { pathTo, splitTaskList } from '../portable/task-list.ts' import { withinEngineLimits } from '../length-bound.ts' type Paragraph = Extract @@ -86,7 +86,7 @@ function readBlocks(blocks: readonly ReadBlock[], reading: Reading, path: Conver return positioned(faulted(fault, nodePath), block.position) } if (block.kind === 'bulletList' && flavourClaims[reading.flavour].taskMarkers) { - const listed = positioned(bulletBlocks(block.items, reading, nodePath, depth), block.position) + const listed = positioned(bulletBlocks(block.items, reading, nodePath, depth, content.length), block.position) if (!listed.ok) return listed for (const node of listed.value) content.push(node) continue @@ -209,7 +209,7 @@ function taskItems(items: readonly Block[][]) { return led } -function bulletBlocks(items: readonly Block[][], reading: Reading, path: ConvertErrorPath, depth: number): Result { +function bulletBlocks(items: readonly Block[][], reading: Reading, path: ConvertErrorPath, depth: number, listIndex: number): Result { const tasks = taskBlocks(items, reading, path, depth) if (tasks === undefined) { const list = listNode({ type: 'bulletList' }, items, reading, path, depth) @@ -218,14 +218,14 @@ function bulletBlocks(items: readonly Block[][], reading: Reading, path: Convert if (!tasks.ok) return tasks const { nodes: blocks, refusal } = tasks.value if (refusal === undefined) return success(blocks) - const listIndex = path.at(-1) const place = pathTo(blocks, refusal.marker) - if (place === undefined || typeof listIndex !== 'number') return { error: refusal.error, ok: false } + // Failures are values, so a marker the split ever copied out of sight leaves the error unplaced rather than asserting; task-list.test.ts pins that the split keeps it. + if (place === undefined) return { error: refusal.error, ok: false } const [blockIndex, ...steps] = place return { error: { ...refusal.error, path: [...path.slice(0, -1), listIndex + blockIndex, ...steps.slice(0, -2), ...refusal.within] }, ok: false } } -// A block a task list refused. A placeholder holding `marker` stands where the block's node would, so the split that builds the list places the refusal too, and `within` is the error's path below that node. +// A block a task list refused. A placeholder holding `marker` as its one child stands where the block's node would, so the split that builds the list places the refusal too, and `within` is the error's path below that node. type Refusal = { error: ConvertError; marker: AdfNode; within: ConvertErrorPath } // Nodes read from task blocks and the first refusal among them; a list is read up to the task holding its refusal. @@ -255,12 +255,12 @@ function taskBlocks(items: readonly Block[][], reading: Reading, path: ConvertEr // The nodes a block of a task reads to. A refused one is a placeholder, typed as the split tells paragraphs and task lists from other blocks, so a paragraph that failed to read stays a paragraph. function readTaskBlock(block: ReadBlock, reading: Reading, path: ConvertErrorPath, depth: number): TaskNodes { const nodePath = [...path, 'content', 0] - const nested = block.kind === 'bulletList' && depth <= largestNesting ? taskBlocks(block.items, reading, nodePath, depth) : undefined + const nested = block.kind === 'bulletList' ? taskBlocks(block.items, reading, nodePath, depth) : undefined if (nested?.ok) return nested.value const read = positioned(nested ?? readBlocks([block], reading, path, depth), block.position) if (read.ok) return { nodes: read.value, refusal: undefined } const marker: AdfNode = { text: '', type: 'text' } - const type = block.kind === 'taskParagraph' ? 'paragraph' : block.kind === 'bulletList' && taskItems(block.items) !== undefined ? 'taskList' : block.kind + const type = block.kind === 'taskParagraph' ? 'paragraph' : nested === undefined ? block.kind : 'taskList' return { nodes: [{ content: [marker], type }], refusal: { error: read.error, marker, within: read.error.path.slice(nodePath.length) } } } diff --git a/src/markdown/portable/task-list.test.ts b/src/markdown/portable/task-list.test.ts index 426abf0..89c7f79 100644 --- a/src/markdown/portable/task-list.test.ts +++ b/src/markdown/portable/task-list.test.ts @@ -4,12 +4,15 @@ import test from 'node:test' import type { AdfNode } from '../../adf/document.ts' import { pathTo, splitTaskList } from './task-list.ts' -test('names the steps from a block to a node deep in it, and none for a node it does not hold', () => { +test('names the steps from a block to a node deep in it', () => { const target: AdfNode = { text: 'x', type: 'text' } const blocks: AdfNode[] = [{ type: 'rule' }, { content: [{ content: [{ type: 'paragraph' }, { content: [target], type: 'paragraph' }], type: 'blockquote' }], type: 'panel' }] assert.deepEqual(pathTo(blocks, target), [1, 'content', 0, 'content', 1, 'content', 0]) assert.deepEqual(pathTo(blocks, blocks[0] ?? target), [0]) - assert.equal(pathTo(blocks, { text: 'x', type: 'text' }), undefined) +}) + +test('names no steps to a node the blocks do not hold', () => { + assert.equal(pathTo([{ content: [{ text: 'x', type: 'text' }], type: 'paragraph' }], { text: 'x', type: 'text' }), undefined) }) test('keeps the inline nodes of a lone paragraph in the taskItem it becomes, so pathTo still finds them', () => {