diff --git a/src/markdown/portable/block-spelling.ts b/src/markdown/portable/block-spelling.ts index 878649a..a67093f 100644 --- a/src/markdown/portable/block-spelling.ts +++ b/src/markdown/portable/block-spelling.ts @@ -7,7 +7,6 @@ import { emitBlock, joinBlocks, quotedBlocks, spelledList, walkBlocks } from '.. import { emitInlineLine } from '../emit/inline-line.ts' import { failure, success } from '../../result.ts' import { isTaskItem } from '../../adf/block-nodes.ts' -import { largestNesting } from '../../nesting.ts' import { nodeAttrs, nodeContent } from '../../adf/document.ts' const portableBlockWriters: FlavourBlockWriters = { @@ -41,18 +40,20 @@ function headed(head: string): (body: BlockText) => BlockText { function tryTaskList(node: AdfNode, path: ConvertErrorPath, depth: number, writing: Writing): Result { const items: PlacedBlock[][] = [] - let headroom = largestNesting - depth - 1 + // The portable reduction leaves a task list opening with a task, so `item` is set before any child nests in it. + let item: PlacedBlock[] = [] // A child other than a task nests in the task before it. for (const [index, child] of nodeContent(node).entries()) { const task = isTaskItem(child) const walk = task ? taskBlocks(child, [...path, 'content', index], depth + 1, writing) : placedBlock(child, [...path, 'content', index], depth + 1, writing) if (!walk.ok) return walk - headroom = Math.min(headroom, walk.value.headroom) - const previous = items.at(-1) - if (task || previous === undefined) items.push([...walk.value.blocks]) - else for (const block of walk.value.blocks) previous.push(block) + if (task) { + item = [] + items.push(item) + } + for (const block of walk.value.blocks) item.push(block) } - const list = spelledList(items.map((blocks) => joinBlocks(blocks, 'list-item')), undefined, headroom) + const list = spelledList(items.map((blocks) => joinBlocks(blocks, 'list-item')), undefined, Number.POSITIVE_INFINITY) // The portable reduction leaves no task a list item cannot hold: a directive here would break the flavour. return list === undefined ? failure('unsupported-node-shape', 'a task holds blocks no list item spells', path) : success(list) } diff --git a/todo.md b/todo.md index 2be7206..05cab4d 100644 --- a/todo.md +++ b/todo.md @@ -13,7 +13,7 @@ | ID | Release | Blocked by | Exempt | Item | L | E | I | R | S | Moves | Score | |---|---|---|---|---|---|---|---|---|---|---|---| | 153 | 0.4.0 | | question | **Decide whether `src/markdown/` root may import `emit/`, `parse/` or `portable/`, and hold the answer in `.oxlintrc.json` and §The source parts by ADF and format.** | 1 | 2 | 2 | 3 | 2 | code health by 3 | 26.2 | -| 175 | 0.4.0 | | | **Delete the guards in `tryTaskList` that `reduceToPortable` makes unreachable, once a test shows `adfToPortableMarkdown` reaches none of them.** | 1 | 1 | 1 | 1 | 2 | code health by 2 | 17.4 | +| 175 | 0.4.0 | | | **Delete the `'a task holds blocks no list item spells'` failure and the child paths in `tryTaskList`, once `spelledList` yields a list the portable reduction can never refuse.** | 1 | 1 | 1 | 1 | 2 | code health by 2 | 17.4 | | 176 | 0.4.0 | | | **Delete `runEdge`'s `standing === 'moves'` give-up, and any code only it needs, once a test or an argument shows `runEdge` never reaches it, or else pin the case with a hand-worked line.** | 1 | 1 | 2 | 2 | 2 | code health by 2 | 17.4 | | 45 | 0.4.0 | | defect | **Replace `isAdfDocument` with a reader returning `Result`.** | 2 | 1 | 3 | 4 | 3 | 1 by 2 | 13.1 | | 127 | 0.4.0 | | principle | **Hold what `entity-references.test.ts`, `json-text.test.ts` and `result.test.ts` check by asserting outcomes, so none reads source text.** | 2 | 2 | 1 | 3 | 3 | code health by 2 | 13.1 | @@ -72,14 +72,13 @@ a paragraph opening with a spellable `!adf:link` (§Nothing recurses unbounded). `src/markdown/**` bans only `html/`; banning `emit/`, `parse/` and `portable/` for the root's own files, with its `ci.sh` probe, extends the decision. -### 175. Delete the guards in `tryTaskList` that `reduceToPortable` makes unreachable, once a test shows `adfToPortableMarkdown` reaches none of them. +### 175. Delete the `'a task holds blocks no list item spells'` failure and the child paths in `tryTaskList`, once `spelledList` yields a list the portable reduction can never refuse. -`reduceToPortable` runs first and turns a task list opening with a non-task into a bullet list. So -`tryTaskList` (`portable/block-spelling.ts`) never sees `previous === undefined`, and its -`'a task holds blocks no list item spells'` failure has no test. The `path` it passes to -`taskBlocks` and `placedBlock`, and its `Math.min` of `headroom`, change no output, since the -nesting refusal's path comes from `reduceToPortable`. Found by mutation testing for item 128, -2026-11-08. +`reduceToPortable` runs first, so `adfToPortableMarkdown` never reaches `tryTaskList`'s +(`portable/block-spelling.ts`) failure; no test reaches it either. It stays because `spelledList` +returns `undefined` for the blocks a list item cannot hold, and the type has no cast to drop it. +The `path` it passes to `taskBlocks` and `placedBlock` changes no output, since the nesting +refusal's path comes from `reduceToPortable`. Found by mutation testing for item 128, 2026-11-08. ### 176. Delete `runEdge`'s `standing === 'moves'` give-up, and any code only it needs, once a test or an argument shows `runEdge` never reaches it, or else pin the case with a hand-worked line.