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:
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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])
|
||||
})
|
||||
|
||||
@@ -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'))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user