3
0
mirror of https://github.com/larvit/adf-codec.git synced 2026-11-12 11:48:29 +01:00

Answer stability round 1: 0 🔴, 2 🟡 (0 misread); drop tryTaskList's dead child paths and splitTaskList's dead check, and group children in place of a list node

This commit is contained in:
2026-11-09 06:16:12 +01:00
committed by Lillem4n
parent 6b2693d385
commit 256f8f453f
5 changed files with 15 additions and 28 deletions
@@ -275,7 +275,7 @@ function reduceTaskList(node: AdfNode, reduction: Reduction): Result<AdfNode[]>
const standsAlone = !nestsTasks && !isTaskItem(child) && child.type !== 'taskList'
for (const block of standsAlone ? [{ content: blocks, type: 'listItem' }] : blocks) tasks.push(block)
}
const items = nestsTasks ? taskListItems({ content: tasks, type: 'taskList' }) : undefined
const items = nestsTasks ? taskListItems(tasks) : undefined
return success(items === undefined ? listOf(tasksAsText(tasks), 'bulletList') : splitTaskList(items))
}
+1 -9
View File
@@ -4,16 +4,8 @@ import test from 'node:test'
import { portableWriting } from './block-spelling.ts'
import { writeMarkdown } from '../emit/adf-to-markdown.ts'
test('refuses a task list a child other than a task opens, where the portable reduction would have left none', () => {
test('refuses a task list that opens with a child other than a task', () => {
const list = { content: [{ content: [{ text: 'stray', type: 'text' }], type: 'paragraph' }, { attrs: { state: 'TODO' }, type: 'taskItem' }], type: 'taskList' }
const written = writeMarkdown({ content: [{ type: 'rule' }, list], type: 'doc', version: 1 }, portableWriting)
assert.deepEqual(written.ok ? written.value : [written.error.code, written.error.path], ['unsupported-node-shape', ['content', 1]])
})
test('names the path of a failure inside a task list by the child it writes', () => {
const task = { attrs: { state: 'TODO' }, content: [{ text: 't', type: 'text' }], type: 'taskItem' }
const unwritable = { attrs: { state: 'TODO' }, content: [{ marks: [{ type: 'strong' }, { type: 'strong' }], text: 'x', type: 'text' }], type: 'taskItem' }
const list = { content: [task, { content: [task], type: 'taskList' }, unwritable], type: 'taskList' }
const written = writeMarkdown({ content: [list], type: 'doc', version: 1 }, portableWriting)
assert.deepEqual(written.ok ? written.value : written.error.path, ['content', 0, 'content', 2, 'content', 0])
})
+4 -8
View File
@@ -6,7 +6,6 @@ import { blockText, emptyLine, joinedText, opening, prefixedText, type BlockText
import { emitBlock, joinBlocks, quotedBlocks, spelledList, walkBlocks } from '../emit/adf-to-markdown.ts'
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'
import { taskListItems } from './task-list.ts'
@@ -41,21 +40,18 @@ function headed(head: string): (body: BlockText) => BlockText {
}
function tryTaskList(node: AdfNode, path: ConvertErrorPath, depth: number, writing: Writing): Result<EmittedBlock> {
const items = taskListItems(node)
// The portable reduction opens every task list with a task: a child nesting in no task would have no list item to stand in.
const items = taskListItems(nodeContent(node))
// The portable reduction opens every task list with a task.
if (items === undefined) return failure('unsupported-node-shape', 'a task list opens with a child other than a task', path)
const inners: BlockText[] = []
let headroom = largestNesting - depth - 1
let index = 0
for (const { after, task } of items) {
const blocks: PlacedBlock[] = []
for (const child of [task, ...after]) {
const childPath = [...path, 'content', index]
const walk = isTaskItem(child) ? taskBlocks(child, childPath, depth + 1, writing) : placedBlock(child, childPath, depth + 1, writing)
const walks = [taskBlocks(task, path, depth + 1, writing), ...after.map((child) => placedBlock(child, path, depth + 1, writing))]
for (const walk of walks) {
if (!walk.ok) return walk
headroom = Math.min(headroom, walk.value.headroom)
for (const block of walk.value.blocks) blocks.push(block)
index += 1
}
inners.push(joinBlocks(blocks, 'list-item'))
}
+5 -5
View File
@@ -26,15 +26,15 @@ test('groups a task list into tasks, each with the children after it up to the n
const first: AdfNode = { attrs: { state: 'TODO' }, type: 'taskItem' }
const nested: AdfNode = { content: [first], type: 'taskList' }
const second: AdfNode = { attrs: { state: 'DONE' }, content: [{ type: 'paragraph' }], type: 'blockTaskItem' }
assert.deepEqual(taskListItems({ content: [first, nested, nested, second], type: 'taskList' }), [
assert.deepEqual(taskListItems([first, nested, nested, second]), [
{ after: [nested, nested], task: first },
{ after: [], task: second },
])
assert.deepEqual(taskListItems({ type: 'taskList' }), [])
assert.deepEqual(taskListItems([]), [])
})
test('groups no task list a child other than a task opens', () => {
test('returns no items for a task list that opens with a child other than a task', () => {
const task: AdfNode = { attrs: { state: 'TODO' }, type: 'taskItem' }
assert.equal(taskListItems({ content: [{ type: 'paragraph' }, task], type: 'taskList' }), undefined)
assert.equal(taskListItems({ content: [{ content: [task], type: 'taskList' }, task], type: 'taskList' }), undefined)
assert.equal(taskListItems([{ type: 'paragraph' }, task]), undefined)
assert.equal(taskListItems([{ content: [task], type: 'taskList' }, task]), undefined)
})
+4 -5
View File
@@ -11,7 +11,7 @@ export function splitTaskList(items: readonly TaskListItem[]): AdfNode[] {
const { kept, rest } = itemParts(item)
for (const node of kept) tasks.push(node)
if (rest.length === 0) continue
if (tasks.length > 0) blocks.push({ content: tasks, type: 'taskList' })
blocks.push({ content: tasks, type: 'taskList' })
tasks = []
for (const block of rest) blocks.push(block)
}
@@ -19,13 +19,12 @@ export function splitTaskList(items: readonly TaskListItem[]): AdfNode[] {
return blocks
}
// A task and the children after it up to the next task, which nest in it.
// A task, then every child before the next task.
export type TaskListItem = { after: AdfNode[]; task: AdfNode }
// The list's items, or undefined where a child other than a task opens it.
export function taskListItems(list: AdfNode): TaskListItem[] | undefined {
export function taskListItems(children: readonly AdfNode[]): TaskListItem[] | undefined {
const items: TaskListItem[] = []
for (const child of nodeContent(list)) {
for (const child of children) {
const item = items.at(-1)
if (isTaskItem(child)) items.push({ after: [], task: child })
else if (item === undefined) return undefined