28 - the line's retry takes one fallback per pass, so it cannot spin #110

Merged
lilleman merged 5 commits from line-fallback-progress into main 2026-09-20 18:04:47 +02:00
2 changed files with 27 additions and 18 deletions
Showing only changes of commit e34d39fdee - Show all commits
+3
View File
@@ -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` 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 owed. A walk pushes one at a time. A literal spread (`[...value]`) is not the same thing and
is fine (4c). 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 - 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 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 hoists the scan that does not vary with the character. The pipeline persona feeds documents
+24 -18
View File
@@ -35,6 +35,8 @@ type LineAttempt =
| { carry?: undefined; line?: undefined; openingLinkAsDirective: true } | { carry?: undefined; line?: undefined; openingLinkAsDirective: true }
| { carry?: undefined; line: string; openingLinkAsDirective?: undefined } | { carry?: undefined; line: string; openingLinkAsDirective?: undefined }
type LineFallbacks = { carried: Set<number>; openingLinkAsDirective: boolean }
export function emitInlineLine(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath): Result<string> { export function emitInlineLine(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath): Result<string> {
const emitted = emitLine(nodes, container, path) const emitted = emitLine(nodes, container, path)
if (!emitted.ok) return emitted 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 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<EmittedLine> { function emitLine(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath): Result<EmittedLine> {
const carried = new Set<number>() const fallbacks: LineFallbacks = { carried: new Set(), openingLinkAsDirective: false }
let openingLinkAsDirective = false
for (;;) { for (;;) {
const emission = lineSegments(nodes, container, path, carried, openingLinkAsDirective) const emission = lineSegments(nodes, container, path, fallbacks)
if (!emission.ok) return emission if (!emission.ok) return emission
if (emission.value.carry !== undefined) { if (emission.value.carry !== undefined) {
carryRange(carried, emission.value.carry) const taken = takeFallback(fallbacks, emission.value.carry, path)
if (!taken.ok) return taken
continue continue
} }
const attempt = attemptLine(emission.value.segments, container, path) const attempt = attemptLine(emission.value.segments, container, path)
if (!attempt.ok) return attempt if (!attempt.ok) return attempt
if (attempt.value.line !== undefined) return success({ line: attempt.value.line, openingLinkAsDirective, segments: emission.value.segments }) if (attempt.value.line !== undefined) {
if (attempt.value.carry !== undefined) carryRange(carried, attempt.value.carry) return success({ line: attempt.value.line, openingLinkAsDirective: fallbacks.openingLinkAsDirective, segments: emission.value.segments })
else openingLinkAsDirective = true }
const taken = takeFallback(fallbacks, attempt.value.carry ?? 'opening-link', path)
if (!taken.ok) return taken
} }
} }
function carryRange(carried: Set<number>, range: NodeRange): void { // emitLine ends because each fallback is takeable once: a pass taking none re-emits the line it just emitted.
for (let index = range.first; index <= range.last; index += 1) carried.add(index) function takeFallback(fallbacks: LineFallbacks, fallback: NodeRange | 'opening-link', path: ConvertErrorPath): Result<null> {
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( function lineSegments(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath, fallbacks: LineFallbacks): Result<Emission> {
nodes: readonly AdfNode[], const context: InlineContext = { atBlockEnd: true, bracketed: false, ...fallbacks, path, spansLines: container === 'paragraph' }
container: LineContainer,
path: ConvertErrorPath,
carried: ReadonlySet<number>,
openingLinkAsDirective: boolean,
): Result<Emission> {
const context: InlineContext = { atBlockEnd: true, bracketed: false, carried, openingLinkAsDirective, path, spansLines: container === 'paragraph' }
const emission = emitRun(nodes, 0, 0, context) const emission = emitRun(nodes, 0, 0, context)
if (!emission.ok) return emission if (!emission.ok) return emission
if (emission.value.carry !== undefined) return emission if (emission.value.carry !== undefined) return emission