From 41ccdcb76edbce67e4a0f8118bebd0af23e173e8 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Thu, 22 Oct 2026 19:12:45 +0200 Subject: [PATCH] =?UTF-8?q?Answer=20stability=20round=201:=200=20?= =?UTF-8?q?=F0=9F=94=B4,=201=20=F0=9F=9F=A1;=20pin=20the=20nesting=20limit?= =?UTF-8?q?=20of=20task=20lists?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/markdown/parse/markdown-to-adf.ts | 2 +- src/markdown/parse/portable-markdown-to-adf.test.ts | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/markdown/parse/markdown-to-adf.ts b/src/markdown/parse/markdown-to-adf.ts index 1d843bb..4727fc9 100644 --- a/src/markdown/parse/markdown-to-adf.ts +++ b/src/markdown/parse/markdown-to-adf.ts @@ -219,7 +219,7 @@ function bulletBlocks(items: readonly Block[][], reading: Reading, path: Convert const { nodes: blocks, refusal } = tasks.value if (refusal === undefined) return success(blocks) const place = pathTo(blocks, refusal.marker) - // Failures are values, so a marker the split ever copied out of sight leaves the error unplaced rather than asserting; task-list.test.ts pins that the split keeps it. + // Failures are values, so a marker the split ever copied out of sight leaves the error at its pre-split path rather than asserting; task-list.test.ts pins that the split keeps it. if (place === undefined) return { error: refusal.error, ok: false } const [blockIndex, ...steps] = place return { error: { ...refusal.error, path: [...path.slice(0, -1), listIndex + blockIndex, ...steps.slice(0, -2), ...refusal.within] }, ok: false } diff --git a/src/markdown/parse/portable-markdown-to-adf.test.ts b/src/markdown/parse/portable-markdown-to-adf.test.ts index 03f0ee6..48e12bf 100644 --- a/src/markdown/parse/portable-markdown-to-adf.test.ts +++ b/src/markdown/parse/portable-markdown-to-adf.test.ts @@ -205,6 +205,19 @@ test('names where a refused block stands after the task list splits', () => { assert.equal(refused('- [ ] a\n\n
x
\n\n tail y\n'), '/content/1') }) +test('holds task lists to the 500 levels the parser carries, refusing one level deeper where it stands', () => { + const nested = (levels: number): string => Array.from({ length: levels }, (_, level) => `${' '.repeat(level)}- [ ] a\n`).join('') + assert.ok(portableMarkdownToAdf(nested(largestNesting)).ok) + const refused = portableMarkdownToAdf(nested(largestNesting + 1)) + assert.deepEqual(refused.ok ? undefined : [refused.error.code, refused.error.path], [ + 'unsupported-nesting-depth', + ['content', 0, ...Array.from({ length: largestNesting }, () => ['content', 1]).flat(), 'content', 0], + ]) + assert.ok(portableMarkdownToAdf(`${'> '.repeat(largestNesting - 1)}- [ ] a\n`).ok) + const quoted = portableMarkdownToAdf(`${'> '.repeat(largestNesting)}- [ ] a\n`) + assert.equal(quoted.ok ? undefined : quoted.error.code, 'unsupported-nesting-depth') +}) + test('reads a task list beside a list of another bullet character as two lists', () => { const tasks = (state: string, value: string): AdfNode => bare('taskList', node('taskItem', { state }, text(value))) assert.deepEqual(read('- [x] a\n* b\n'), [tasks('DONE', 'a'), bare('bulletList', bare('listItem', bare('paragraph', text('b'))))])