diff --git a/src/conformance/adf-property.test.ts b/src/conformance/adf-property.test.ts index 8fc1786..cbf9328 100644 --- a/src/conformance/adf-property.test.ts +++ b/src/conformance/adf-property.test.ts @@ -25,6 +25,16 @@ 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) => { @@ -38,13 +48,14 @@ test('a generated document emits markdown that reads back to it', { timeout: pro ) }) -test('a generated document writes portable markdown refusing only what the guard refuses, and that markdown reads back to its text and to itself', { timeout: propertyTimeout }, () => { +test('a generated document writes portable markdown refusing only what the guard refuses, and that markdown reads back to its text and to itself, a block task item holding paragraphs alone', { timeout: propertyTimeout }, () => { fc.assert( fc.property(adfDocument, (document) => { const written = adfToPortableMarkdown(document) 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)}`) 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/markdown/parse/portable-markdown-to-adf.test.ts b/src/markdown/parse/portable-markdown-to-adf.test.ts index 86e95f8..32c546a 100644 --- a/src/markdown/parse/portable-markdown-to-adf.test.ts +++ b/src/markdown/parse/portable-markdown-to-adf.test.ts @@ -148,6 +148,9 @@ test('reads a bullet list whose every item leads with a task marker to a task li test('moves a nested task list beside its item and makes an item holding more than one paragraph a block task item', () => { assert.deepEqual(read('- [x] Parent\n - [ ] Child\n- [ ] Next\n'), [bare('taskList', task('DONE', text('Parent')), bare('taskList', task('TODO', text('Child'))), task('TODO', text('Next')))]) assert.deepEqual(read('- [x] First.\n\n Second.\n- [ ] Next\n'), [bare('taskList', node('blockTaskItem', { state: 'DONE' }, said('First.'), said('Second.')), task('TODO', text('Next')))]) + const nested = bare('taskList', node('blockTaskItem', { state: 'DONE' }, said('p1'), said('p2')), bare('taskList', task('TODO', text('n'))), task('TODO', text('B'))) + assert.deepEqual(read('- [x] p1\n\n p2\n - [ ] n\n- [ ] B\n'), [nested]) + assert.deepEqual(roundTripped(nested), [nested]) }) test('stands a block a task item cannot hold, and what follows it in the item, after the task list, which resumes at the next task', () => { diff --git a/src/markdown/portable/adf-to-portable-markdown.test.ts b/src/markdown/portable/adf-to-portable-markdown.test.ts index 55e71ca..bb3120d 100644 --- a/src/markdown/portable/adf-to-portable-markdown.test.ts +++ b/src/markdown/portable/adf-to-portable-markdown.test.ts @@ -132,6 +132,8 @@ test('spells a task list as a bullet list whose items lead with their state', () const blockTask = node('blockTaskItem', { state: 'DONE' }, said('First.'), said('Second.')) const codeTask = node('blockTaskItem', { state: 'TODO' }, { content: [text('x')], type: 'codeBlock' }) assert.equal(portable(node('taskList', {}, blockTask, codeTask)), '- [x] First.\n\n Second.\n- [ ]\n\n```\nx\n```\n') + const parent = node('blockTaskItem', { state: 'TODO' }, said('q'), said('q2')) + assert.equal(portable(node('taskList', {}, parent, node('taskList', {}, task('DONE', 'n')), task('DONE', 'B'))), '- [ ] q\n\n q2\n - [x] n\n- [x] B\n') const listTask = node('blockTaskItem', { state: 'DONE' }, bulletList(item(said('a')))) assert.equal(portable(node('taskList', {}, task('TODO', ''), listTask, node('taskList', {}, task('TODO', 'b')))), '- \\[ ]\n- \\[x]\n- a\n- \\[ ] b\n') assert.equal(portable(node('taskList', {}, task('DONE', 'a'), node('taskList', {}, task('TODO', '')))), '- [x] a\n - [ ]\n') diff --git a/src/markdown/portable/adf-to-portable-markdown.ts b/src/markdown/portable/adf-to-portable-markdown.ts index 7fe393c..03e854c 100644 --- a/src/markdown/portable/adf-to-portable-markdown.ts +++ b/src/markdown/portable/adf-to-portable-markdown.ts @@ -314,11 +314,8 @@ function reduceTaskList(node: AdfNode, reduction: Reduction): Result return success(listOf(nodeContent(tasksAsText({ content: tasks, type: 'taskList' })), 'bulletList')) } -// The writer nests a list in the task before it, so one closing a block task item's blocks merges with it. function nestIn(tasks: AdfNode[], nested: readonly AdfNode[]): void { - const previous = tasks.at(-1) - if (previous?.type === 'blockTaskItem') tasks[tasks.length - 1] = { ...previous, content: mergedLists([...nodeContent(previous), ...nested]) } - else for (const block of mergedLists(nested)) tasks.push(block) + for (const block of mergedLists(nested)) tasks.push(block) } function isTask(node: AdfNode | undefined): boolean { diff --git a/todo.md b/todo.md index 166422b..6258d2f 100644 --- a/todo.md +++ b/todo.md @@ -6,7 +6,7 @@ `Bar = 9` -`Next ID = 93` +`Next ID = 94` | Goal | W | |---|---| @@ -47,6 +47,7 @@ | 75 | 0.3.0 | | **Make the generated-markdown property write block carries as `adf:` fences, canonical and hostile.** | 2 | 2 | 3 | 7 | 1 | 17.5 | | 81 | 0.3.0 | | **Fail the gate unless the README's error tables list exactly the codes `ConvertErrorCode` holds.** | 1 | 2 | 5 | 5 | 3 | 16.3 | | 46 | 0.3.0 | | **Publish the bundle size in the README, failing the release pipeline when it drifts.** | 2 | 4 | 5 | 5 | 3, 8 | 14.8 | +| 93 | 0.3.0 | | **Report a refusal's `path` where the node would stand after `portableMarkdownToAdf` moves it out of a task item.** | 2 | 3 | 3 | 5 | 1 | 13.2 | | 72 | 0.3.0 | decision | **Re-create `gitea.larvit.se/larvit/adf-codec` as a read-only pull mirror of GitHub, its issues and Actions off and its description pointing at GitHub.** | 2 | 1 | 3 | 6 | 3 | 13.1 | | 52 | 0.3.0 | | **Spell `colwidth` as a comma list, `colwidth="340,420"`.** | 3 | 3 | 5 | 6 | 6 | 11.5 | | 51 | 0.3.0 | | **Match a reference label to its definition under Unicode case folding.** | 2 | 2 | 2 | 7 | 4, 5 | 10.9 | @@ -74,7 +75,8 @@ Today `- a` then `+ b`, or `1.` then `1)`, reads as one list; CommonMark reads t 301 and 302). Goal 5 settles it for the lossless flavour too: CommonMark spells adjacent lists by changing the marker, so `adfToLosslessMarkdown` and `adfToPortableMarkdown` alternate `-` and `*` between adjacent bullet lists, a task list among them, and `.` and `)` between adjacent ordered -lists, and `!adf:listBreak` retires. `*` is the maintainer's pick (2026-10-04). Breaking: `MIGRATION.md`'s Readings and +lists, and `!adf:listBreak` retires. Until then, a bullet list standing after a task list joins +it when written, so `adfToPortableMarkdown` writes every task's marker as text. `*` is the maintainer's pick (2026-10-04). Breaking: `MIGRATION.md`'s Readings and Spellings tables gain their rows. Examples 301 and 302 lose their `pending` exceptions, and the spelling leaves the README's "Seven CommonMark spellings" bullet, which counts one fewer. @@ -249,6 +251,13 @@ or report the unminified gzip). The figure lands in README §The package beside dependencies" claim. Measured today, unminified: tarball 60.4 kB, unpacked 221.5 kB, JS gzipped 45.6 kB. +### 93. Report a refusal's `path` where the node would stand after `portableMarkdownToAdf` moves it out of a task item. + +`readBlocks` fixes a block's path before `splitTaskList` and `bulletNode` move blocks beside or +after their task, so `- [x] A\n\n ```\n x\n ```\n\n
x
\n` reports +`/content/0/content/0/content/2` for a block that would stand at `/content/2`. The README says +`path` is the node's place from the document root. + ### 72. Re-create `gitea.larvit.se/larvit/adf-codec` as a read-only pull mirror of GitHub, its issues and Actions off and its description pointing at GitHub. Lands after every other `0.3.0` item (the maintainer, 2026-10-05). The maintainer does this. Gitea