From ddccd0024310d1a74ba2bdbbb349a2d0c21d0e77 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Sun, 18 Oct 2026 11:31:11 +0200 Subject: [PATCH] =?UTF-8?q?Answer=20stability=20round=201:=200=20?= =?UTF-8?q?=F0=9F=94=B4,=203=20=F0=9F=9F=A1;=20pin=20the=20blank=20line's?= =?UTF-8?q?=20spaces=20in=20an=20item's=20code=20block,=20drop=20the=20tim?= =?UTF-8?q?ing-only=20tests,=20and=20file=20tab-indented=20lines=20as=20it?= =?UTF-8?q?em=20120?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/decisions.md | 5 +++-- src/markdown/parse/blocks.test.ts | 18 ++++++++++-------- todo.md | 16 ++++++++++++++-- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/docs/decisions.md b/docs/decisions.md index c478d8f..cd8888f 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -828,8 +828,9 @@ changed, and a before-and-after figure in its PR; the gate times nothing. Measur `adfDocumentFault`'s shape and depth walks stay two — the parting gives depth its own code — at 52 ms for a 9 MB document the emit takes 314 ms over. The 500-level guard runs after the block walk, so it bounds nothing in it: `matchBlankLine` continues a blank line through the open list items in -one step, and an item reads a line's indentation only up to its own width, which took 20000 nested -items and as many blank lines from 10.7 s to 39 ms. +one step, which took 20000 nested items and as many blank lines from 10.7 s to 39 ms; and an item +reads a line's indentation only up to its own width, though a tab an item's width splits still +costs the whole line at each level (`todo.md` item 120). ## Only the hard break holds a raw newline diff --git a/src/markdown/parse/blocks.test.ts b/src/markdown/parse/blocks.test.ts index e31b51d..0e945e1 100644 --- a/src/markdown/parse/blocks.test.ts +++ b/src/markdown/parse/blocks.test.ts @@ -123,7 +123,7 @@ test("releases an indented code block's held blank lines as a walk rather than a assert.deepEqual(kinds(` a\n${'\n'.repeat(200000)} b\n`), ['code']) }) -// A level is one list item. The gate times nothing; a quadratic walk outlasts Bun's 5 s test timeout. +// A level is one list item. function deepestItem(markdown: string): { depth: number; kinds: string[] } { let blocks = parseBlocks(markdown, flavourClaims.lossless).blocks let depth = 0 @@ -134,19 +134,21 @@ function deepestItem(markdown: string): { depth: number; kinds: string[] } { return { depth, kinds: blocks.map((block) => block.kind) } } -test('continues a blank line through a deep run of list items in time linear in the lines', () => { - const depth = 20000 +test('continues a blank line and an indented line through list items nested past the 500 levels the parser carries', () => { + const depth = 1000 assert.deepEqual(deepestItem(`${'- '.repeat(depth)}a\n${'\n'.repeat(depth)}`), { depth, kinds: ['paragraph'] }) assert.deepEqual(deepestItem(`${'- '.repeat(depth)}a\n${'\n'.repeat(depth)}${' '.repeat(depth)}b\n`), { depth, kinds: ['paragraph', 'paragraph'] }) + assert.deepEqual(deepestItem(`${'- '.repeat(depth)}a\n${' '.repeat(depth)}b\n`), { depth, kinds: ['paragraph'] }) }) -test('stops a blank line at the first blockquote and at an empty item', () => { +test('stops a blank line at the first blockquote', () => { assert.deepEqual(deepestItem('- > a\n\n b\n'), { depth: 1, kinds: ['blockquote', 'paragraph'] }) - assert.deepEqual(deepestItem('- - \n\n a\n'), { depth: 2, kinds: [] }) assert.deepEqual(deepestItem('- > - a\n\n > b\n'), { depth: 1, kinds: ['blockquote', 'blockquote'] }) }) -test('continues a line through a deep run of list items in time linear in the line', () => { - const depth = 20000 - assert.deepEqual(deepestItem(`${'- '.repeat(depth)}a\n${`${' '.repeat(depth)}b\n`.repeat(3)}`), { depth, kinds: ['paragraph'] }) +test('stops a blank line at an empty item, and gives a code block in an item the blank line without its spaces', () => { + assert.deepEqual(deepestItem('- - \n\n a\n'), { depth: 2, kinds: [] }) + assert.deepEqual(parseBlocks('- ```\n \n x\n ```\n', flavourClaims.lossless).blocks.map((block) => block.kind === 'bulletList' && block.items[0]?.[0]), [ + { kind: 'code', language: '', position: { line: 1, offset: 0 }, text: '\nx' }, + ]) }) diff --git a/todo.md b/todo.md index 8a97489..355109f 100644 --- a/todo.md +++ b/todo.md @@ -6,12 +6,13 @@ `Bar = 0.1` -`Next ID = 120` +`Next ID = 121` ## Items | ID | Release | Blocked by | Exempt | Item | L | E | I | R | S | Moves | Score | |---|---|---|---|---|---|---|---|---|---|---|---| +| 120 | 0.3.0 | | defect | **Read a tab-indented line under nested list items in time linear in the line, where today a tab an item's width splits copies the whole line at each level.** | 2 | 1 | 2 | 3 | 3 | 9 by 2 | 6.0 | | 115 | 0.3.0 | | defect | **Build text holding no `border` mark in `losslessMarkdownToAdf`, and have `adfToLosslessMarkdown` carry text holding one.** | 2 | 1 | 2 | 3 | 3 | 4 by 3 | 4.5 | | 100 | 0.3.0 | | decision | **Put the reading of an image's title as its caption back to a reader panel, now that a writer panel reads a title as hover text.** | 1 | 2 | 2 | 3 | 3 | 6 by 4 | 4.0 | | 33 | 0.3.0 | | defect | **Emit a line in time linear in its mark runs, in `adfToLosslessMarkdown` and `adfToPortableMarkdown`.** | 3 | 1 | 2 | 4 | 5 | 9 by 3 | 3.6 | @@ -23,7 +24,7 @@ | 102 | 0.3.0 | | decision | **Place a block a portable task list refuses at its path in the returned document without replaying `splitTaskList` on stand-in nodes.** | 2 | 1 | 2 | 3 | 4 | 10 by 4 | 2.0 | | 59 | 0.3.0 | | decision | **Group the directive grammar into `src/markdown/directive/`, move `Read` to `result.ts`, and move `Flavour` and `flavourClaims` to `markdown/flavour.ts`.** | 3 | 1 | 2 | 4 | 5 | 10 by 5 | 1.5 | | 62 | 0.3.0 | | decision | **Move the portable flavour's reading out of `parse/` and its writing out of `emit/` into `markdown/portable/`, so `emit/` no longer imports `portable/`.** | 3 | 1 | 2 | 4 | 5 | 10 by 5 | 1.5 | -| 73 | 0.3.0 | 33, 59, 60, 62, 91, 92, 95, 100, 102, 109, 115 | | **Announce the package where someone needing an ADF converter already reads: JRACLOUD-77436, the Atlassian developer community and Stack Overflow's ADF-to-markdown questions.** | 1 | 2 | 2 | 3 | 2 | 3 by 8 | 32.0 | +| 73 | 0.3.0 | 33, 59, 60, 62, 91, 92, 95, 100, 102, 109, 115, 120 | | **Announce the package where someone needing an ADF converter already reads: JRACLOUD-77436, the Atlassian developer community and Stack Overflow's ADF-to-markdown questions.** | 1 | 2 | 2 | 3 | 2 | 3 by 8 | 32.0 | | 81 | 0.4.0 | | | **Fail the gate unless the README's error tables list exactly the codes `ConvertErrorCode` holds.** | 1 | 1 | 1 | 1 | 2 | 3 by 2 | 8.0 | | 45 | 0.4.0 | | defect | **Replace `isAdfDocument` with a reader returning `Result`.** | 2 | 1 | 3 | 4 | 3 | 1 by 2 | 6.0 | | 6 | 0.4.0 | | decision | **Specify the HTML dialect.** | 3 | 2 | 1 | 4 | 6 | | 0.0 | @@ -57,6 +58,17 @@ ## Details +### 120. Read a tab-indented line under nested list items in time linear in the line, where today a tab an item's width splits copies the whole line at each level. + +`removeColumns` in `src/markdown/parse/blocks.ts` returns `' '.repeat(n) + line.text.slice(index)` +where an item's width splits a tab, a fresh string, so the next level's `slice` copies the whole +line instead of sharing it. Measured on 2026-10-18 on 20 tab-indented lines under 20000, 40000 and +80000 nested list items: 176, 460 and 1812 ms, where the same lines indented with spaces took 73, 90 +and 173 ms. Keep the split tab's leftover columns as a number on `Line` beside `column` and `text`, +so a cut never builds a string. `docs/decisions.md` §Cost fixes are measured, never timed names +this exception (cites item 120); delete that clause with the fix. Found while reviewing item 98's +chunk, 2026-10-18. + ### 115. Build text holding no `border` mark in `losslessMarkdownToAdf`, and have `adfToLosslessMarkdown` carry text holding one. `!adf:border[x]{color="#091e42" size=2}` builds text holding `border`, which