diff --git a/src/conformance/adf-property.test.ts b/src/conformance/adf-property.test.ts index ff274eb..03d9946 100644 --- a/src/conformance/adf-property.test.ts +++ b/src/conformance/adf-property.test.ts @@ -3,7 +3,7 @@ import fc from 'fast-check' import test from 'node:test' import type { AdfNode } from '../adf/document.ts' -import { adfDocument, propertyRuns, propertyTimeout } from './property-harness.ts' +import { adfDocument, blockTaskItemChildren, propertyRuns, propertyTimeout } from './property-harness.ts' import { adfToLosslessMarkdown } from '../markdown/emit/adf-to-markdown.ts' import { adfToPortableMarkdown, reduceToPortable } from '../markdown/portable/adf-to-portable-markdown.ts' import { losslessMarkdownToAdf, portableMarkdownToAdf } from '../markdown/parse/markdown-to-adf.ts' @@ -25,16 +25,6 @@ function shownText(nodes: readonly AdfNode[]): string[] { return shown.filter((text) => text !== '') } -function heldByBlockTaskItems(nodes: readonly AdfNode[]): string[] { - const held: string[] = [] - for (const node of nodes) { - const content = node.content ?? [] - if (node.type === 'blockTaskItem') for (const child of content) held.push(child.type) - for (const type of heldByBlockTaskItems(content)) held.push(type) - } - return held -} - test('a generated document emits markdown that reads back to it', { timeout: propertyTimeout }, () => { fc.assert( fc.property(adfDocument, (document) => { @@ -55,7 +45,7 @@ test('a generated document writes portable markdown refusing only what the guard assert.ok(written.ok, written.ok ? '' : `${written.error.code}: ${written.error.message}`) const read = portableMarkdownToAdf(written.value) assert.ok(read.ok, read.ok ? '' : `${read.error.code}: ${read.error.message} — reading ${JSON.stringify(written.value)}`) - assert.deepEqual(heldByBlockTaskItems(read.value.content ?? []).filter((type) => type !== 'paragraph'), [], `reading ${JSON.stringify(written.value)}`) + assert.deepEqual(blockTaskItemChildren(read.value.content ?? []).filter((type) => type !== 'paragraph'), [], `reading ${JSON.stringify(written.value)}`) const reduced = reduceToPortable(document) assert.deepEqual(shownText(read.value.content ?? []), reduced.ok ? shownText(reduced.value.content ?? []) : reduced, `reading ${JSON.stringify(written.value)}`) assert.deepEqual(adfToPortableMarkdown(read.value), written, `reading ${JSON.stringify(written.value)}`) diff --git a/src/conformance/markdown-property.test.ts b/src/conformance/markdown-property.test.ts index ffefbff..2b01b2b 100644 --- a/src/conformance/markdown-property.test.ts +++ b/src/conformance/markdown-property.test.ts @@ -7,7 +7,7 @@ import type { Arbitrary, DepthIdentifier } from 'fast-check' import type { AttributeVocabulary } from '../adf/attribute-vocabulary.ts' import type { JsonValue } from '../json-value.ts' import type { Result } from '../result.ts' -import { adfDocument, attributes, jsonKey, jsonValue, markdownPieces, propertyRuns, propertyTimeout, textOf } from './property-harness.ts' +import { adfDocument, attributes, blockTaskItemChildren, jsonKey, jsonValue, markdownPieces, propertyRuns, propertyTimeout, textOf } from './property-harness.ts' import { adfToLosslessMarkdown } from '../markdown/emit/adf-to-markdown.ts' import { adfToPortableMarkdown } from '../markdown/portable/adf-to-portable-markdown.ts' import { blockArgument, listBreakName, marksAttribute } from '../markdown/block-directive.ts' @@ -418,16 +418,6 @@ function taskListOf(bodies: Arbitrary): Arbitrary { const taskMarkdown = taskListOf(fc.oneof(cleanMarkdown, hostileMarkdown, taskListOf(cleanMarkdown))) -function blockTaskItemChildren(document: AdfDocument): string[] { - const types: string[] = [] - const pending = [...nodeContent(document)] - for (let node = pending.pop(); node !== undefined; node = pending.pop()) { - if (node.type === 'blockTaskItem') for (const child of nodeContent(node)) types.push(child.type) - pending.push(...nodeContent(node)) - } - return types -} - function holdsDirectiveShape(document: AdfDocument): boolean { const pending = [...nodeContent(document)] for (let node = pending.pop(); node !== undefined; node = pending.pop()) { @@ -483,7 +473,7 @@ test('generated task lists read as portable markdown refuse, or build block task fc.property(taskMarkdown, (input) => { const parsed = portableMarkdownToAdf(input) if (!parsed.ok) return - assert.deepEqual(blockTaskItemChildren(parsed.value).filter((type) => type !== 'paragraph'), [], `reading ${JSON.stringify(input)}`) + assert.deepEqual(blockTaskItemChildren(nodeContent(parsed.value)).filter((type) => type !== 'paragraph'), [], `reading ${JSON.stringify(input)}`) const written = adfToPortableMarkdown(parsed.value) assert.ok(written.ok, written.ok ? '' : `${written.error.code}: ${written.error.message} — spelling ${JSON.stringify(input)}`) const read = portableMarkdownToAdf(written.value) diff --git a/src/conformance/property-harness.ts b/src/conformance/property-harness.ts index e64d540..330b80c 100644 --- a/src/conformance/property-harness.ts +++ b/src/conformance/property-harness.ts @@ -9,7 +9,7 @@ import type { JsonValue } from '../json-value.ts' import { blockArgument } from '../markdown/block-directive.ts' import { blockNodes } from '../adf/block-nodes.ts' import { directivePrefix } from '../markdown/directive-syntax.ts' -import { emptyKeys, mergeAdjacentText } from '../adf/document.ts' +import { emptyKeys, mergeAdjacentText, nodeContent } from '../adf/document.ts' import { inlineNodes } from '../adf/inline-nodes.ts' import { joinsWhenRead } from '../markdown/adjacent-text.ts' import { markAttributes } from '../adf/mark-attributes.ts' @@ -202,3 +202,13 @@ export function propertyRuns(gateRuns: number): { gate: boolean; numRuns: number assert.ok(/^[1-9]\d*$/.test(deepRuns), `${deepRunsVariable} is a run count in digits, such as ${deepRunsVariable}=10000: found ${JSON.stringify(deepRuns)}`) return { gate: false, numRuns: Number(deepRuns) } } + +export function blockTaskItemChildren(nodes: readonly AdfNode[]): string[] { + const types: string[] = [] + const pending = [...nodes] + for (let node = pending.pop(); node !== undefined; node = pending.pop()) { + if (node.type === 'blockTaskItem') for (const child of nodeContent(node)) types.push(child.type) + for (const child of nodeContent(node)) pending.push(child) + } + return types +} diff --git a/src/markdown/parse/markdown-to-adf.ts b/src/markdown/parse/markdown-to-adf.ts index db788f7..59ec8c9 100644 --- a/src/markdown/parse/markdown-to-adf.ts +++ b/src/markdown/parse/markdown-to-adf.ts @@ -121,6 +121,7 @@ function readBlock(block: ReadBlock, reading: Reading, path: ConvertErrorPath, d case 'blockquote': return reading.claims.alerts ? quoteNode(block.blocks, reading, path, depth) : containerNode({ type: 'blockquote' }, block.blocks, reading, path, depth) case 'bulletList': + // readBlocks reads a portable task list before it reaches here. return listNode({ type: 'bulletList' }, block.items, reading, path, depth) case 'code': return codeBlockNode(block.language, block.text, reading, path, depth) diff --git a/src/markdown/portable/task-list.ts b/src/markdown/portable/task-list.ts index 3b0f526..03c61ff 100644 --- a/src/markdown/portable/task-list.ts +++ b/src/markdown/portable/task-list.ts @@ -29,17 +29,16 @@ function itemsOf(children: readonly AdfNode[]): AdfNode[][] { return items } +// The item reads as one run: the task's blocks, then the children after it. function itemParts(item: readonly AdfNode[]): { kept: AdfNode[]; rest: AdfNode[] } { - const [task] = item + const [task, ...after] = item if (task === undefined || !isTaskItem(task)) return { kept: [], rest: [...item] } const content = task.type === 'blockTaskItem' ? nodeContent(task) : [] + const run = [...content, ...after] const paragraphs = runEnd(content, 0, 'paragraph') - const nested = runEnd(content, paragraphs, 'taskList') - const unheld = content.slice(nested) - const after = item.slice(1) - const beside = unheld.length === 0 ? runEnd(after, 0, 'taskList') : 0 + const nested = runEnd(run, paragraphs, 'taskList') const head = task.type === 'blockTaskItem' ? taskOfParagraphs(task, content.slice(0, paragraphs)) : task - return { kept: [head, ...content.slice(paragraphs, nested), ...after.slice(0, beside)], rest: [...unheld, ...after.slice(beside)] } + return { kept: [head, ...run.slice(paragraphs, nested)], rest: run.slice(nested) } } function runEnd(nodes: readonly AdfNode[], start: number, type: string): number {