From e34d39fdeec9af41e12f71701842496eda9f234c Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Sun, 20 Sep 2026 17:49:54 +0200 Subject: [PATCH] 28 - the line's retry takes one fallback per pass, so it cannot spin --- AGENTS.md | 3 +++ src/markdown/emit/inline-line.ts | 42 ++++++++++++++++++-------------- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index e1fb8e2..9952a7e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -314,6 +314,9 @@ someone spells it or pins it. mark run's segments: the argument list caps near 125k and throws a `RangeError` where a `Result` is owed. A walk pushes one at a time. A literal spread (`[...value]`) is not the same thing and is fine (4c). +- A loop retrying an input until a fallback spells it refuses the pass taking no fallback, so its + termination is the loop's own check rather than an argument every give-way site has to honour + (28). - A reader takes the text and an index — a sticky regex whose `lastIndex` the caller sets on the line before it reads, `indexOf` — never a fresh slice per character, and a per-character walk hoists the scan that does not vary with the character. The pipeline persona feeds documents diff --git a/src/markdown/emit/inline-line.ts b/src/markdown/emit/inline-line.ts index 320d4fd..b05385b 100644 --- a/src/markdown/emit/inline-line.ts +++ b/src/markdown/emit/inline-line.ts @@ -35,6 +35,8 @@ type LineAttempt = | { carry?: undefined; line?: undefined; openingLinkAsDirective: true } | { carry?: undefined; line: string; openingLinkAsDirective?: undefined } +type LineFallbacks = { carried: Set; openingLinkAsDirective: boolean } + export function emitInlineLine(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath): Result { const emitted = emitLine(nodes, container, path) if (!emitted.ok) return emitted @@ -63,37 +65,41 @@ export function tryImageLine(alt: string | undefined, href: string, path: Conver return attempt.ok ? attempt.value.line : undefined } -// Every pass carries at least one more node, or flips openingLinkAsDirective, which happens once — so the loop ends. function emitLine(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath): Result { - const carried = new Set() - let openingLinkAsDirective = false + const fallbacks: LineFallbacks = { carried: new Set(), openingLinkAsDirective: false } for (;;) { - const emission = lineSegments(nodes, container, path, carried, openingLinkAsDirective) + const emission = lineSegments(nodes, container, path, fallbacks) if (!emission.ok) return emission if (emission.value.carry !== undefined) { - carryRange(carried, emission.value.carry) + const taken = takeFallback(fallbacks, emission.value.carry, path) + if (!taken.ok) return taken continue } const attempt = attemptLine(emission.value.segments, container, path) if (!attempt.ok) return attempt - if (attempt.value.line !== undefined) return success({ line: attempt.value.line, openingLinkAsDirective, segments: emission.value.segments }) - if (attempt.value.carry !== undefined) carryRange(carried, attempt.value.carry) - else openingLinkAsDirective = true + if (attempt.value.line !== undefined) { + return success({ line: attempt.value.line, openingLinkAsDirective: fallbacks.openingLinkAsDirective, segments: emission.value.segments }) + } + const taken = takeFallback(fallbacks, attempt.value.carry ?? 'opening-link', path) + if (!taken.ok) return taken } } -function carryRange(carried: Set, range: NodeRange): void { - for (let index = range.first; index <= range.last; index += 1) carried.add(index) +// emitLine ends because each fallback is takeable once: a pass taking none re-emits the line it just emitted. +function takeFallback(fallbacks: LineFallbacks, fallback: NodeRange | 'opening-link', path: ConvertErrorPath): Result { + if (fallback === 'opening-link') { + if (fallbacks.openingLinkAsDirective) return failure('unsupported-node-shape', 'an opening link spelled as a directive still reads as a link definition, so the line has no spelling left', path) + fallbacks.openingLinkAsDirective = true + return success(null) + } + const before = fallbacks.carried.size + for (let index = fallback.first; index <= fallback.last; index += 1) fallbacks.carried.add(index) + if (fallbacks.carried.size === before) return failure('unsupported-node-shape', 'a carry took no inline node the line had not carried, so the line has no spelling left', path) + return success(null) } -function lineSegments( - nodes: readonly AdfNode[], - container: LineContainer, - path: ConvertErrorPath, - carried: ReadonlySet, - openingLinkAsDirective: boolean, -): Result { - const context: InlineContext = { atBlockEnd: true, bracketed: false, carried, openingLinkAsDirective, path, spansLines: container === 'paragraph' } +function lineSegments(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath, fallbacks: LineFallbacks): Result { + const context: InlineContext = { atBlockEnd: true, bracketed: false, ...fallbacks, path, spansLines: container === 'paragraph' } const emission = emitRun(nodes, 0, 0, context) if (!emission.ok) return emission if (emission.value.carry !== undefined) return emission