From 5f8c7cf2ebd76818d9c77a2ea1c7fc09faa9e3cc 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=20principles=20round=201:=200=20?= =?UTF-8?q?=F0=9F=94=B4,=202=20=F0=9F=9F=A1;=20read=20an=20inline=20direct?= =?UTF-8?q?ive=20from=20the=20InlineSource=20holding=20its=20text=20and=20?= =?UTF-8?q?closer,=20and=20name=20the=20quote=20edges=20apart=20from=20the?= =?UTF-8?q?=20quotes=20passed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/markdown/directive-syntax.test.ts | 9 ++------- src/markdown/directive-syntax.ts | 5 +++-- src/markdown/inline-tokens.ts | 3 ++- src/markdown/parse/blocks.ts | 25 +++++++++++++------------ src/markdown/parse/inline-content.ts | 2 +- 5 files changed, 21 insertions(+), 23 deletions(-) diff --git a/src/markdown/directive-syntax.test.ts b/src/markdown/directive-syntax.test.ts index e36be24..dc3f296 100644 --- a/src/markdown/directive-syntax.test.ts +++ b/src/markdown/directive-syntax.test.ts @@ -2,8 +2,8 @@ import assert from 'node:assert/strict' import test from 'node:test' import type { DirectiveAttributes, DirectiveLine } from './directive-syntax.ts' -import { backtickCloser } from './commonmark/backtick-runs.ts' import { claimDirectivePrefix, readDirectiveLine, readInlineDirective } from './directive-syntax.ts' +import { inlineSource } from './inline-tokens.ts' import { largestNesting } from '../nesting.ts' // A pair the input spells bare decodes to itself; a quoted one names its spelling beside the decoding. @@ -25,7 +25,7 @@ function fault(line: string): string { function inline(text: string): unknown { const prefix = claimDirectivePrefix(text, 0) if (prefix === undefined) return 'unclaimed' - const read = readInlineDirective(text, prefix, backtickCloser(text)) + const read = readInlineDirective(inlineSource(text), prefix) if (read.fault !== undefined) return read.fault.message return { attributes: read.value.attributes, content: read.value.content, length: read.value.length, name: read.value.name } } @@ -157,8 +157,3 @@ test('refuses inline directives nested deeper than the parser carries', () => { spans(nest(largestNesting), 'underline', nest(largestNesting - 1)) assert.equal(inline(nest(largestNesting + 1)), `the input nests inline directives deeper than the ${largestNesting} levels the parser carries`) }) - -test('reads a directive whose content holds many unclosed backtick runs in time linear in its length', () => { - const runs = Array.from({ length: 3000 }, (_, index) => '`'.repeat(index + 1)).join(' ') - spans(`!adf:x[${runs}]`, 'x', runs) -}) diff --git a/src/markdown/directive-syntax.ts b/src/markdown/directive-syntax.ts index 418c7f9..61b5f6b 100644 --- a/src/markdown/directive-syntax.ts +++ b/src/markdown/directive-syntax.ts @@ -1,6 +1,7 @@ import type { AttributeKind, VocabularyPair, VocabularyValue } from '../adf/attribute-vocabulary.ts' import type { CodeSpanCloser } from './commonmark/backtick-runs.ts' import type { ConvertFault } from '../result.ts' +import type { InlineSource } from './inline-tokens.ts' import type { JsonValue } from '../json-value.ts' import { backslashEscape, holdsNullCharacter } from './commonmark/grammar.ts' import { backtickRun } from './commonmark/backtick-runs.ts' @@ -101,8 +102,8 @@ export function readDirectiveLine(line: string): Read | undefined return line.charAt(directivePrefix.length) === '/' ? readCloserLine(line) : readOpenerLine(line) } -export function readInlineDirective(text: string, prefix: ClaimedPrefix, codeSpanCloser: CodeSpanCloser): Read { - return readClaimedDirective(text, prefix.index, 1, codeSpanCloser) +export function readInlineDirective(inline: InlineSource, prefix: ClaimedPrefix): Read { + return readClaimedDirective(inline.text, prefix.index, 1, inline.codeSpanCloser) } export function readSoleStringAttribute(span: DirectiveSpan, key: string): Read { diff --git a/src/markdown/inline-tokens.ts b/src/markdown/inline-tokens.ts index b17627d..9c5b213 100644 --- a/src/markdown/inline-tokens.ts +++ b/src/markdown/inline-tokens.ts @@ -1,7 +1,8 @@ import type { ClaimedPrefix } from './directive-syntax.ts' +import type { CodeSpanCloser } from './commonmark/backtick-runs.ts' import type { Claims } from './portable/conventions.ts' import { backslashEscape, inlineHtmlConstruct, readBracketedAutolink, readEmailAutolink } from './commonmark/grammar.ts' -import { backtickCloser, backtickRun, type CodeSpanCloser } from './commonmark/backtick-runs.ts' +import { backtickCloser, backtickRun } from './commonmark/backtick-runs.ts' import { claimDirectivePrefix } from './directive-syntax.ts' import { delimiterFlags, runLength } from './commonmark/emphasis-matching.ts' import { highlightDelimiter, highlightFlanking } from './portable/conventions.ts' diff --git a/src/markdown/parse/blocks.ts b/src/markdown/parse/blocks.ts index 94b555a..de84f73 100644 --- a/src/markdown/parse/blocks.ts +++ b/src/markdown/parse/blocks.ts @@ -74,7 +74,7 @@ type ContainerStack = { open: readonly OpenContainer[] push: (container: OpenContainer) => void // The index in `edges` of each blockquote. - quotes: readonly number[] + quoteEdges: readonly number[] } type Walk = ParsedBlocks & { @@ -114,7 +114,7 @@ function containerStack(): ContainerStack { const depthsOf = (name: string): number[] => entryOf(directiveDepths, name, () => []) const edges: { container: EdgeContainer; depth: number }[] = [] const open: OpenContainer[] = [] - const quotes: number[] = [] + const quoteEdges: number[] = [] return { directiveDepth: (name) => directiveDepths.get(name)?.at(-1), drop: (depth) => { @@ -123,7 +123,7 @@ function containerStack(): ContainerStack { if (container.kind === 'directive') depthsOf(container.name).pop() else { edges.pop() - if (container.kind === 'blockquote') quotes.pop() + if (container.kind === 'blockquote') quoteEdges.pop() } } return dropped @@ -134,11 +134,11 @@ function containerStack(): ContainerStack { const depth = open.push(container) - 1 if (container.kind === 'directive') depthsOf(container.name).push(depth) else { - if (container.kind === 'blockquote') quotes.push(edges.length) + if (container.kind === 'blockquote') quoteEdges.push(edges.length) edges.push({ container, depth }) } }, - quotes, + quoteEdges, } } @@ -168,15 +168,15 @@ function swallowsLines(leaf: OpenLeaf | undefined): boolean { // A directive container has no continuation marker, so every line continues it. function matchContainers(walk: Walk, line: Line): { depth: number; rest: Line } { if (isBlankLine(line.text)) return matchBlankLine(walk, line, 0, 0) - let quotes = 0 + let quotesPassed = 0 let rest = line for (const [index, { container, depth }] of walk.stack.edges.entries()) { const next = continuesContainer(container, rest) if (next === undefined) return { depth, rest } rest = next if (container.kind !== 'blockquote') continue - quotes += 1 - if (isBlankLine(rest.text)) return matchBlankLine(walk, rest, index + 1, quotes) + quotesPassed += 1 + if (isBlankLine(rest.text)) return matchBlankLine(walk, rest, index + 1, quotesPassed) } return { depth: walk.stack.open.length, rest } } @@ -184,11 +184,12 @@ function matchContainers(walk: Walk, line: Line): { depth: number; rest: Line } // A blank line continues every list item up to the next blockquote, so it is matched in one step rather than one per item. // A list item begins with at most one blank line: an empty one gives the second up. Only the innermost item is empty: // opening a container inside an item gives it a block. -function matchBlankLine(walk: Walk, line: Line, from: number, quote: number): { depth: number; rest: Line } { - const { edges, quotes } = walk.stack +function matchBlankLine(walk: Walk, line: Line, from: number, quotesPassed: number): { depth: number; rest: Line } { + const { edges, quoteEdges } = walk.stack const last = edges.length - 1 - const emptyItem = last >= from && edges[last]?.container.blocks.length === 0 && walk.leaf === undefined - const stop = Math.min(quotes[quote] ?? edges.length, emptyItem ? last : edges.length) + const innermost = edges[last]?.container + const emptyItem = last >= from && innermost?.kind === 'item' && innermost.blocks.length === 0 && walk.leaf === undefined + const stop = Math.min(quoteEdges[quotesPassed] ?? edges.length, emptyItem ? last : edges.length) return { depth: edges[stop]?.depth ?? walk.stack.open.length, rest: stop === from ? line : { column: line.column, text: '' } } } diff --git a/src/markdown/parse/inline-content.ts b/src/markdown/parse/inline-content.ts index 5a4ee89..e5de746 100644 --- a/src/markdown/parse/inline-content.ts +++ b/src/markdown/parse/inline-content.ts @@ -173,7 +173,7 @@ function readLineEnding(scan: Scan, index: number): number { function readDirective(scan: Scan, inline: InlineSource, prefix: ClaimedPrefix): Result { const held = scan.spans.get(prefix.index) if (held !== undefined) return pushDirective(scan, held, prefix.index) - const directive = readInlineDirective(scan.source, prefix, inline.codeSpanCloser) + const directive = readInlineDirective(inline, prefix) if (directive.fault !== undefined) return faulted(directive.fault, scan.path) return pushDirective(scan, directive.value, prefix.index) }