From 091ee1e238c6169f036807255fc63ba41a293a90 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 9 Nov 2026 05:29:06 +0100 Subject: [PATCH] =?UTF-8?q?Answer=20stability=20round=201:=201=20?= =?UTF-8?q?=F0=9F=94=B4,=200=20=F0=9F=9F=A1=20(1=20misread);=20keep=20the?= =?UTF-8?q?=20task=20list's=20headroom,=20and=20pin=20its=20refusal=20agai?= =?UTF-8?q?nst=20a=20bullet=20chain's?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../portable/adf-to-portable-markdown.test.ts | 19 +++++++++++++++++++ src/markdown/portable/block-spelling.ts | 5 ++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/markdown/portable/adf-to-portable-markdown.test.ts b/src/markdown/portable/adf-to-portable-markdown.test.ts index 414985b..16d3c3d 100644 --- a/src/markdown/portable/adf-to-portable-markdown.test.ts +++ b/src/markdown/portable/adf-to-portable-markdown.test.ts @@ -511,6 +511,25 @@ test('writes a task list nested to the deepest level the reader carries, and ref assert.equal(crowded.ok ? undefined : crowded.error.code, 'unsupported-nesting-depth') }) +test('refuses a list that cannot be a markdown list at the depth a bullet chain in its place refuses', () => { + // The item's text is two markers, which reads as a thematic break and the directive form is out of reach. + const unspellable = bulletList(item(bulletList(item()))) + const task = (value: string): AdfNode => node('taskItem', { state: 'TODO' }, text(value)) + const tasks = node('taskList', {}, task('a'), node('taskList', {}, task('b'), node('taskList', {}, task('c')))) + const bullets = bulletList(item(said('a'), bulletList(item(said('b'), bulletList(item(said('c'))))))) + const refusal = (chain: AdfNode, panels: number): string | undefined => { + let wrapped: AdfNode = bulletList(item(unspellable, chain)) + for (let level = 0; level < panels; level += 1) wrapped = node('panel', { panelType: 'info' }, wrapped) + const result = adfToPortableMarkdown(document(wrapped)) + return result.ok ? undefined : result.error.code + } + const edge = largestNesting - 4 + assert.equal(refusal(bullets, edge - 1), undefined) + assert.equal(refusal(bullets, edge), 'unsupported-nesting-depth') + assert.equal(refusal(tasks, edge - 1), undefined) + assert.equal(refusal(tasks, edge), 'unsupported-nesting-depth') +}) + test('writes a task list nested in the task before it, and more than one beside each other in that task', () => { const done = (value: string): AdfNode => node('taskItem', { state: 'DONE' }, text(value)) const list = (...content: AdfNode[]): AdfNode => ({ content, type: 'taskList' }) diff --git a/src/markdown/portable/block-spelling.ts b/src/markdown/portable/block-spelling.ts index 3c624de..7024ede 100644 --- a/src/markdown/portable/block-spelling.ts +++ b/src/markdown/portable/block-spelling.ts @@ -7,6 +7,7 @@ import { emitBlock, joinBlocks, quotedBlocks, spelledList, walkBlocks } from '.. 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' const portableBlockWriters: FlavourBlockWriters = { @@ -40,6 +41,7 @@ function headed(head: string): (body: BlockText) => BlockText { function tryTaskList(node: AdfNode, path: ConvertErrorPath, depth: number, writing: Writing): Result { const items: PlacedBlock[][] = [] + let headroom = largestNesting - depth - 1 // The portable reduction opens every task list with a task, so this empty array is replaced before any block is pushed to it. let item: PlacedBlock[] = [] // A child other than a task nests in the task before it. @@ -47,13 +49,14 @@ function tryTaskList(node: AdfNode, path: ConvertErrorPath, depth: number, writi const task = isTaskItem(child) const walk = task ? taskBlocks(child, [...path, 'content', index], depth + 1, writing) : placedBlock(child, [...path, 'content', index], depth + 1, writing) if (!walk.ok) return walk + headroom = Math.min(headroom, walk.value.headroom) if (task) { item = [] items.push(item) } for (const block of walk.value.blocks) item.push(block) } - const list = spelledList(items.map((blocks) => joinBlocks(blocks, 'list-item')), undefined, Number.POSITIVE_INFINITY) + const list = spelledList(items.map((blocks) => joinBlocks(blocks, 'list-item')), undefined, headroom) // The portable reduction leaves no task a list item cannot hold: a directive here would break the flavour. return list === undefined ? failure('unsupported-node-shape', 'a task holds blocks no list item spells', path) : success(list) }