From 7b726ef6ec6ccc0d50e4923e5a5426de89edb8ec Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Fri, 18 Sep 2026 19:37:38 +0200 Subject: [PATCH] 4c - the directive scan keeps the spans it read, so the slot parse never scans them again --- src/markdown/directive-syntax.ts | 36 ++++++++++++++++++++-------- src/markdown/parse/inline-content.ts | 29 +++++++++++++--------- 2 files changed, 44 insertions(+), 21 deletions(-) diff --git a/src/markdown/directive-syntax.ts b/src/markdown/directive-syntax.ts index af3a669..41e7fcb 100644 --- a/src/markdown/directive-syntax.ts +++ b/src/markdown/directive-syntax.ts @@ -17,7 +17,10 @@ export type DirectiveLine = | { argument: string | undefined; attributes: DirectiveAttributes; kind: 'opener'; name: string } | { kind: 'closer'; name: string } -export type DirectiveSpan = { attributes: DirectiveAttributes; content: string | undefined; length: number; name: string } +// spans: the directives the content holds, at their offset in it, so reading the slot back never scans them again. +export type DirectiveSpan = { attributes: DirectiveAttributes; content: string | undefined; length: number; name: string; spans: NestedSpans } + +export type NestedSpans = ReadonlyMap export type Read = { fault: ConvertFault; value?: undefined } | { fault?: undefined; value: T } @@ -25,7 +28,9 @@ type Attributes = { attributes: DirectiveAttributes; end: number } type AttributePair = { end: number; key: string; value: DirectiveValue } -type Content = { content: string | undefined; end: number } +type Content = { content: string | undefined; end: number; spans: NestedSpans } + +type DirectiveContent = { end: number; spans: NestedSpans } export const directivePrefix = '!adf:' @@ -41,6 +46,8 @@ const quotedEscapes = new RegExp(reservedSource, 'g') const rawReserved = new RegExp(reservedSource) const noAttributes: DirectiveAttributes = new Map() +export const noSpans: NestedSpans = new Map() + export const directiveEscape = `\\${directivePrefix} keeps the prefix literal` const closerFault = `a closer carries nothing after its name: this one does; ${directiveEscape}` @@ -213,14 +220,14 @@ function readNestedDirective(text: string, index: number, depth: number): Read { - if (text.charAt(index) !== '[') return { value: { content: undefined, end: index } } + if (text.charAt(index) !== '[') return { value: { content: undefined, end: index, spans: noSpans } } const close = readDirectiveContent(text, index + 1, depth) if (close.fault !== undefined) return { fault: close.fault } - return { value: { content: text.slice(index + 1, close.value), end: close.value + 1 } } + return { value: { content: text.slice(index + 1, close.value.end), end: close.value.end + 1, spans: close.value.spans } } } function readAttributesAt(text: string, index: number, braceClaims: boolean): Read { @@ -231,7 +238,8 @@ function readAttributesAt(text: string, index: number, braceClaims: boolean): Re } // A code span, an escape and a nested directive each bind before the content's own closing bracket. -function readDirectiveContent(text: string, start: number, depth: number): Read { +function readDirectiveContent(text: string, start: number, depth: number): Read { + const spans = new Map() let brackets = 0 let cursor = start while (cursor < text.length && text.charAt(cursor) !== '\n') { @@ -246,13 +254,13 @@ function readDirectiveContent(text: string, start: number, depth: number): Read< cursor = span continue } - const nested = readNestedDirective(text, cursor, depth + 1) + const nested = keepNestedSpan(text, cursor, depth, start, spans) + if (nested?.fault !== undefined) return { fault: nested.fault } if (nested !== undefined) { - if (nested.fault !== undefined) return { fault: nested.fault } - cursor += nested.value.length + cursor = nested.value continue } - if (character === ']' && brackets === 0) return { value: cursor } + if (character === ']' && brackets === 0) return { value: { end: cursor, spans } } if (character === '[') brackets += 1 if (character === ']') brackets -= 1 cursor += 1 @@ -260,6 +268,14 @@ function readDirectiveContent(text: string, start: number, depth: number): Read< return { fault: malformedDirective(`an inline directive [content] is unclosed; ${directiveEscape}`) } } +function keepNestedSpan(text: string, cursor: number, depth: number, start: number, spans: Map): Read | undefined { + const nested = readNestedDirective(text, cursor, depth + 1) + if (nested === undefined) return undefined + if (nested.fault !== undefined) return { fault: nested.fault } + spans.set(cursor - start, nested.value) + return { value: cursor + nested.value.length } +} + // `undefined` where the span crosses the line ending an inline directive may not cross. function readCodeSpanEnd(text: string, index: number): number | undefined { const opener = backtickRun(text, index) diff --git a/src/markdown/parse/inline-content.ts b/src/markdown/parse/inline-content.ts index 25ccfdf..e969b79 100644 --- a/src/markdown/parse/inline-content.ts +++ b/src/markdown/parse/inline-content.ts @@ -1,5 +1,5 @@ import type { AdfMark, AdfNode } from '../../adf/document.ts' -import type { DirectiveSpan } from '../directive-syntax.ts' +import type { DirectiveSpan, NestedSpans } from '../directive-syntax.ts' import type { EmphasisPairing } from '../emphasis-matching.ts' import type { LineContainer } from '../emit/line-escaping.ts' import type { LinkDefinition } from '../link-syntax.ts' @@ -15,7 +15,7 @@ import { normalizeLabel, readInlineTarget, readLabel } from '../link-syntax.ts' import { openingLinkTakesDirective } from '../emit/inline-line.ts' import { readCarriedInline } from '../opaque-carry.ts' import { readDirectiveMark } from './directive-marks.ts' -import { readInlineDirective } from '../directive-syntax.ts' +import { noSpans, readInlineDirective } from '../directive-syntax.ts' import { readInlineDirectiveNode } from './directive-nodes.ts' import { readTextDirective } from '../text-directive.ts' @@ -45,6 +45,7 @@ type Scan = { pending: string pieces: Piece[] source: string + spans: NestedSpans } type SlotContent = { carry: boolean; nodes: AdfNode[] } @@ -54,11 +55,11 @@ const imageAlone = 'an image fits only as a paragraph of its own: this one sits const spellableLink = 'link takes the directive form only where CommonMark cannot spell it: this one it can, as [text](url "title") or ' export function parseInlineContent(source: string, definitions: LinkDefinitions, path: ConvertErrorPath, container: LineContainer): Result { - return parseInline(source, definitions, path, container) + return parseInline(source, definitions, path, container, noSpans) } -function parseInline(source: string, definitions: LinkDefinitions, path: ConvertErrorPath, container: LineContainer | undefined): Result { - const scan: Scan = { container, definitions, openingSpellableLink: false, path, pending: '', pieces: [], source } +function parseInline(source: string, definitions: LinkDefinitions, path: ConvertErrorPath, container: LineContainer | undefined, spans: NestedSpans): Result { + const scan: Scan = { container, definitions, openingSpellableLink: false, path, pending: '', pieces: [], source, spans } let index = 0 while (index < source.length) { switch (source.charAt(index)) { @@ -168,14 +169,20 @@ function openBracket(scan: Scan, index: number): number { } function readDirective(scan: Scan, index: number): Result | undefined { + const held = scan.spans.get(index) + if (held !== undefined) return pushDirective(scan, held, index) const directive = readInlineDirective(scan.source, index) if (directive === undefined) return undefined if (directive.fault !== undefined) return faulted(directive.fault, scan.path) - const piece = directivePiece(scan, directive.value, index) + return pushDirective(scan, directive.value, index) +} + +function pushDirective(scan: Scan, span: DirectiveSpan, index: number): Result { + const piece = directivePiece(scan, span, index) if (!piece.ok) return piece flush(scan, false) scan.pieces.push(piece.value) - return success(index + directive.value.length) + return success(index + span.length) } function directivePiece(scan: Scan, span: DirectiveSpan, index: number): Result { @@ -187,7 +194,7 @@ function directivePiece(scan: Scan, span: DirectiveSpan, index: number): Result< const text = readTextDirective(span) if (text?.fault !== undefined) return faulted(text.fault, scan.path) if (text !== undefined) return success({ kind: 'nodes', nodes: [{ text: text.value, type: 'text' }] }) - const slot = slotContent(scan, span.content) + const slot = slotContent(scan, span) if (!slot.ok) return slot const mark = readDirectiveMark(span.name, span.attributes, scan.path) if (mark !== undefined) return mark.ok ? directiveMarkPiece(scan, span.name, mark.value, slot.value, index) : mark @@ -216,9 +223,9 @@ function refuseSpellableLink(scan: Scan, mark: AdfMark, nodes: readonly AdfNode[ return undefined } -function slotContent(scan: Scan, content: string | undefined): Result { - if (content === undefined) return success(undefined) - const parsed = parseInline(content, scan.definitions, scan.path, undefined) +function slotContent(scan: Scan, span: DirectiveSpan): Result { + if (span.content === undefined) return success(undefined) + const parsed = parseInline(span.content, scan.definitions, scan.path, undefined, span.spans) if (!parsed.ok) return parsed if (parsed.value.image !== undefined) return failure('unmappable-image', imageAlone, scan.path) return success(parsed.value)