4b - review nits: the list's headroom folds into its item loop, emitBlocks inlined, panel-wrapped boundary pinned, 4c note says cubic
This commit is contained in:
@@ -520,6 +520,7 @@ test('refuses a document nested deeper than the emitter carries', () => {
|
||||
const deep = document(listed(directiveLists, [{ type: 'rule' }]))
|
||||
assert.deepEqual(markdownToAdf(markdown(adfToMarkdown(deep))), { ok: true, value: deep })
|
||||
assert.equal(code(adfToMarkdown(document(listed(directiveLists + 1, [{ type: 'rule' }])))), 'unsupported-nesting-depth')
|
||||
assert.equal(code(adfToMarkdown(document({ content: [listed(directiveLists, [{ type: 'rule' }])], type: 'panel' }))), 'unsupported-nesting-depth')
|
||||
const chain = (levels: number): AdfNode => {
|
||||
let card: AdfNode = { type: 'blockCard' }
|
||||
for (let level = 0; level < levels; level += 1) card = { content: [card], type: 'blockCard' }
|
||||
|
||||
@@ -31,18 +31,13 @@ export function adfToMarkdown(document: AdfDocument): Result<string> {
|
||||
const fault = adfDocumentFault(document)
|
||||
if (fault !== undefined) return faulted(fault, [])
|
||||
if (document.version !== 1) return failure('unsupported-document-version', `no markdown spelling carries ADF version ${document.version}`, [])
|
||||
const blocks = emitBlocks(nodeContent(document), 'document', [], 0)
|
||||
if (!blocks.ok) return blocks
|
||||
return success(blocks.value === '' ? '' : `${blocks.value}\n`)
|
||||
}
|
||||
|
||||
function emitBlocks(nodes: readonly AdfNode[], container: BlockContainer, path: ConvertErrorPath, depth: number): Result<string> {
|
||||
const walk = walkBlocks(nodes, path, depth)
|
||||
const walk = walkBlocks(nodeContent(document), [], 0)
|
||||
if (!walk.ok) return walk
|
||||
return success(joinBlocks(walk.value.blocks, container))
|
||||
const text = joinBlocks(walk.value.blocks, 'document')
|
||||
return success(text === '' ? '' : `${text}\n`)
|
||||
}
|
||||
|
||||
// The walk's headroom is the least slack any depth guard below it has, so a spelling that sinks the walked blocks a level can refuse rather than walk again.
|
||||
// headroom: the least slack any depth guard below the walk has.
|
||||
function walkBlocks(nodes: readonly AdfNode[], path: ConvertErrorPath, depth: number): Result<Walk> {
|
||||
let headroom = largestNesting - depth
|
||||
if (headroom < 0) return tooDeep(path)
|
||||
@@ -224,25 +219,28 @@ function emitList(node: AdfNode, path: ConvertErrorPath, depth: number): Result<
|
||||
if (start === undefined || items.length === 0) return undefined
|
||||
if (items.some((item) => item.type !== 'listItem' || !carriesOnly(item, []))) return undefined
|
||||
const walked: WalkedItem[] = []
|
||||
let headroom = Number.POSITIVE_INFINITY
|
||||
for (const [offset, item] of items.entries()) {
|
||||
const walk = walkBlocks(nodeContent(item), [...path, 'content', offset], depth + 1)
|
||||
if (!walk.ok) return walk
|
||||
headroom = Math.min(headroom, walk.value.headroom)
|
||||
walked.push({ node: item, walk: walk.value })
|
||||
}
|
||||
const headroom = Math.min(...walked.map((item) => item.walk.headroom))
|
||||
const lines: string[] = []
|
||||
for (const [offset, item] of walked.entries()) {
|
||||
const line = listItemLines(item.walk.blocks, ordered ? `${start + offset}. ` : '- ')
|
||||
if (line === undefined) return headroom < 1 ? tooDeep(path) : emitDirectiveBlock(node, ordered ? blockDirectives.orderedList : blockDirectives.bulletList, path, depth, () => success(directiveItems(walked)))
|
||||
if (line === undefined) {
|
||||
if (headroom < 1) return tooDeep(path)
|
||||
return emitDirectiveBlock(node, ordered ? blockDirectives.orderedList : blockDirectives.bulletList, path, depth, () => success({ blocks: directiveItems(walked), headroom: headroom - 1 }))
|
||||
}
|
||||
lines.push(line)
|
||||
}
|
||||
return success({ headroom, spelling: 'list', text: lines.join('\n') })
|
||||
}
|
||||
|
||||
// The directive form sinks each item's blocks a level below where the walk read them.
|
||||
function directiveItems(items: readonly WalkedItem[]): Walk {
|
||||
const blocks = items.map((item) => ({ ...directivePair(item.node, listItemOpener, joinBlocks(item.walk.blocks, 'directive'), item.walk.headroom - 1), node: item.node }))
|
||||
return { blocks, headroom: Math.min(...blocks.map((block) => block.headroom)) }
|
||||
function directiveItems(items: readonly WalkedItem[]): PlacedBlock[] {
|
||||
return items.map((item) => ({ ...directivePair(item.node, listItemOpener, joinBlocks(item.walk.blocks, 'directive'), item.walk.headroom - 1), node: item.node }))
|
||||
}
|
||||
|
||||
function listStart(node: AdfNode, items: number): number | undefined {
|
||||
|
||||
@@ -64,11 +64,9 @@ proves 12, 13 spells 11's gaps in 12's grammar, and 12 rewrites code 4b and 4c c
|
||||
export persona runs in bulk walks the document twice. Both walks are linear, so this is a
|
||||
constant factor rather than 4b's class change, and the parting is what gives depth its own
|
||||
code (§8) — measure before joining them back.
|
||||
**Settled** (the maintainer, 2026-09-18): the limit stays 500 readable lists. The one walk
|
||||
counts items one below the list, as the readable form does, and reports its headroom; a
|
||||
list falling back to the directive form refuses when that form's extra level no longer
|
||||
fits. Counting every list twice was rejected for halving the limit, counting the directive
|
||||
form once for doubling the parser's frames per level.
|
||||
**Settled** (the maintainer, 2026-09-18): the limit stays 500 readable lists, the walk
|
||||
reporting its headroom (§11). Counting every list twice was rejected for halving the limit,
|
||||
counting the directive form once for doubling the parser's frames per level.
|
||||
**Measured** (2026-09-18): `adfDocumentFault` walks a 9 MB document in 52 ms against 314 ms
|
||||
for the emit, so its two walks stay parted.
|
||||
- [ ] **4c — The scanning rule's remaining sites (`0.2.0`).** A trailing-anchored regex re-walks
|
||||
@@ -90,9 +88,10 @@ proves 12, 13 spells 11's gaps in 12's grammar, and 12 rewrites code 4b and 4c c
|
||||
`readDirectiveContent`'s scan splits into named steps with that fix rather than keeping its
|
||||
complexity (the maintainer, 2026-09-16). A sixth 4b leaves behind: the parser asks
|
||||
`commonMarkSpelling` at every directive-spelled list it reads, and the answer spells the whole
|
||||
subtree below, so nested directive lists cost O(depth × subtree) — 250 rule-first levels parse
|
||||
in 1.3 s at 16.5 kB, 1.5 MB of that shape at 250 levels in 0.6 s — bounded by the depth guard like
|
||||
`readNestedDirective` (the maintainer, 2026-09-18).
|
||||
subtree below, itself quadratic in the depth left, so nested directive lists cost about the
|
||||
cube of their depth — 250 rule-first levels parse in 1.3 s at 16.5 kB, 1.5 MB of that shape
|
||||
at 250 levels in 0.6 s — bounded by the depth guard like `readNestedDirective` (the
|
||||
maintainer, 2026-09-18).
|
||||
- [ ] **4d — What the gate says while it runs (`0.2.1`).** `ci.sh` runs nine legs and announces
|
||||
none of them, so five minutes of a Gitea run read as silence and a hang cannot be told from
|
||||
a slow pull — the maintainer hit exactly this on the `0.1.0` release. Three causes, each its
|
||||
|
||||
Reference in New Issue
Block a user