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

Answer architecture round 2: itemParts takes one run of task lists over the item, the properties share blockTaskItemChildren, and readBlock's bullet case names who reads a task list first

This commit is contained in:
2026-10-09 14:20:36 +02:00
parent a99c5b17c8
commit e487fa4111
5 changed files with 21 additions and 31 deletions
+2 -12
View File
@@ -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)}`)
+2 -12
View File
@@ -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<string>): Arbitrary<string> {
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)
+11 -1
View File
@@ -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
}
+1
View File
@@ -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)
+5 -6
View File
@@ -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 {