diff --git a/src/markdown/commonmark/link-syntax.test.ts b/src/markdown/commonmark/link-syntax.test.ts index 04492bd..34a322c 100644 --- a/src/markdown/commonmark/link-syntax.test.ts +++ b/src/markdown/commonmark/link-syntax.test.ts @@ -18,7 +18,7 @@ test('a target nothing closes reads as none, at every `](`', () => { assert.deepEqual(targets('[a](b'.repeat(3)), [undefined, undefined, undefined]) }) -test('targets sharing a destination end share its title and its failure', () => { +test('targets sharing a destination end share its failure', () => { assert.deepEqual(targets('[a](b[c](d "t" x)'), [undefined, undefined]) assert.deepEqual(targets('[a](b[c](d "t" x'), [undefined, undefined]) }) @@ -28,7 +28,7 @@ test('an unbracketed destination ends at its first unmatched `)`', () => { assert.deepEqual(targets('[a](b\\)c)'), [{ destination: 'b)c', length: 6 }]) }) -test('a target reads its whitespace, a bracketed destination and a title', () => { +test('a target reads whitespace and a title around its destination, bracketed, unbracketed or empty', () => { assert.deepEqual(targets('[a]( b\n "t" ) [c]( \'f\') [g]()'), [ { destination: 'b', length: 10, title: 't' }, { destination: 'd e', length: 11, title: 'f' }, diff --git a/src/markdown/commonmark/link-syntax.ts b/src/markdown/commonmark/link-syntax.ts index 8058a3c..6a6c463 100644 --- a/src/markdown/commonmark/link-syntax.ts +++ b/src/markdown/commonmark/link-syntax.ts @@ -7,17 +7,17 @@ export type LinkDefinition = { destination: string; title?: string } export type LinkPart = { length: number; value: string } -// What follows a destination ending at an index, the same for every destination ending there: the index after the `)`, and the title. +// Every destination ending at one index shares what follows it: the title and the closing `)`. type Tail = { end: number; title: string | undefined } -// `destinationEnds[start]` is where an unbracketed destination starting there stops, for every start a target reads from: one after a `(`, a space or a control character. +// `destinationEnds[start]` is where an unbracketed destination starting there stops, for every start a target can have: the index just after a `(`, a space or a control character. export type TargetMemo = { destinationEnds: Int32Array; tails: Map } // The lossless flavour escapes a quote alone, writing a title holding a backslash or an entity reference as a directive (spec/flavour.md, Marks); the portable flavour writes no directive, so it escapes those too, and a table cell's `|` (README, Portable markdown). export type TitleEscapes = 'lossless' | 'portable' | 'portable-table-cell' const bracketedDestination = /<((?:[^\n<>\\]|\\[^\n])*)>/y -// The longest text an unbracketed destination can take: it stops at a space or a control character. +// The longest text an unbracketed destination can take. const destinationRun = new RegExp(`[^ ${controlCharacterRange}]*`, 'y') // CommonMark: a link label holds at most this many characters between its brackets, an escape counting as two. export const labelLimit = 999 @@ -51,7 +51,7 @@ export function readDestination(text: string, offset: number): LinkPart | undefi return length === 0 ? undefined : { length, value: decodeTextEscapes(run.slice(0, length)) } } -// `offset` sits on the `(` a link text closes into. The definition is read on demand, so a target its link refuses costs no copy. +// `offset` sits on the `(` a link text closes into. The definition is built only when called, so a link the reader then refuses never decodes its destination. export function readInlineTarget(text: string, offset: number, memo: TargetMemo): InlineTarget | undefined { const start = skipLinkWhitespace(text, offset + 1) const end = destinationEnd(text, start, memo) @@ -67,7 +67,7 @@ export function readInlineTarget(text: string, offset: number, memo: TargetMemo) return { definition, length: tail.end - offset } } -// Every unbracketed destination's end in one pass, with a stack of the starts still open. The text's own start is one, which `readDestination` reads. +// Every unbracketed destination's end, in one pass. Index 0 is a start too, for `readDestination`, which reads from its text's first character. export function targetMemo(text: string): TargetMemo { const destinationEnds = new Int32Array(text.length + 1) const open = [0] @@ -123,7 +123,7 @@ export function readTitle(text: string, offset: number): LinkPart | undefined { // The label, the destination and the title each take at most one line ending with them. export function skipLinkWhitespace(text: string, offset: number): number { linkWhitespace.lastIndex = offset - // The pattern matches empty, so the test always moves `lastIndex` past the whitespace. + // The pattern matches empty, so the test never fails, which would reset `lastIndex` to 0. linkWhitespace.test(text) return linkWhitespace.lastIndex } diff --git a/src/markdown/parse/inline-content.ts b/src/markdown/parse/inline-content.ts index 1ca7cb1..bda03dd 100644 --- a/src/markdown/parse/inline-content.ts +++ b/src/markdown/parse/inline-content.ts @@ -87,7 +87,6 @@ type Scan = { pieces: Piece[] source: string spans: NestedSpans - // Built at the first inline target the scan reads. targets: TargetMemo | undefined } diff --git a/todo.md b/todo.md index 21495da..d03dc01 100644 --- a/todo.md +++ b/todo.md @@ -72,10 +72,11 @@ ### 151. Read a link destination holding an unbalanced unescaped parenthesis as no destination, in every markdown reader. CommonMark lets an unbracketed destination hold a parenthesis only escaped or in a balanced pair, -but `[a](b(c "t")` reads as a link to `b(c` titled `t`, where cmark and commonmark.js read text. -`readDestination` and `targetMemo` (`src/markdown/commonmark/link-syntax.ts`) stop at a space with -parentheses still open and keep the destination. The writers escape an unbalanced parenthesis, so -no written markdown changes. Found while shipping item 140, 2026-10-28. +but `[a](b(c "t")` reads as a link to `b(c` titled `t`; cmark and commonmark.js read it as text. +`targetMemo` (`src/markdown/commonmark/link-syntax.ts`), which `readDestination` reads through, +stops at a space with parentheses still open and keeps the destination. The writers escape an +unbalanced parenthesis, so markdown they wrote reads the same after the fix. Found while shipping +item 140, 2026-10-28. ### 95. Find the code-span closers an escape forms, the mark runs a line cannot spell and the text `[` that opens link text by reading the line back, in place of `escapeClosedRuns`, `unspellableRuns` and `lastLinkClose` predicting them.