From fe47e6bc7ba4f3aebe2caaf6a906d0e68ec38e7a Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Tue, 15 Sep 2026 21:22:34 +0200 Subject: [PATCH] 4.3: a link opening a paragraph that reads as a link reference definition rides the carry --- .../combinations/link-definition-carry.json | 45 +++++++++++++++++++ .../combinations/link-definition-carry.md | 3 ++ spec/flavour.md | 5 ++- src/markdown/emit/inline-line.ts | 2 +- src/markdown/emit/line-escaping.ts | 11 +++-- 5 files changed, 60 insertions(+), 6 deletions(-) create mode 100644 corpus/round-trip/combinations/link-definition-carry.json create mode 100644 corpus/round-trip/combinations/link-definition-carry.md diff --git a/corpus/round-trip/combinations/link-definition-carry.json b/corpus/round-trip/combinations/link-definition-carry.json new file mode 100644 index 0000000..ad3fd23 --- /dev/null +++ b/corpus/round-trip/combinations/link-definition-carry.json @@ -0,0 +1,45 @@ +{ + "content": [ + { + "content": [ + { + "marks": [ + { + "attrs": { + "href": "/u" + }, + "type": "link" + }, + { + "type": "code" + } + ], + "text": "]: a", + "type": "text" + } + ], + "type": "paragraph" + }, + { + "content": [ + { + "attrs": { + "timestamp": "]:a" + }, + "marks": [ + { + "attrs": { + "href": "/u" + }, + "type": "link" + } + ], + "type": "date" + } + ], + "type": "paragraph" + } + ], + "type": "doc", + "version": 1 +} diff --git a/corpus/round-trip/combinations/link-definition-carry.md b/corpus/round-trip/combinations/link-definition-carry.md new file mode 100644 index 0000000..a06706f --- /dev/null +++ b/corpus/round-trip/combinations/link-definition-carry.md @@ -0,0 +1,3 @@ +:adf{json="{\"marks\":[{\"attrs\":{\"href\":\"/u\"},\"type\":\"link\"},{\"type\":\"code\"}],\"text\":\"]: a\",\"type\":\"text\"}"} + +:adf{json="{\"attrs\":{\"timestamp\":\"]:a\"},\"marks\":[{\"attrs\":{\"href\":\"/u\"},\"type\":\"link\"}],\"type\":\"date\"}"} diff --git a/spec/flavour.md b/spec/flavour.md index a98ec2e..e407647 100644 --- a/spec/flavour.md +++ b/spec/flavour.md @@ -56,8 +56,9 @@ normalizes to it through the round-trip. the leading delimiter of a construct that would otherwise open, re-scan from there, and repeat. A backtick run escapes whole, and a lone backtick escapes wherever an escaped one follows it in the same inline content: CommonMark reads no escape inside a code span, so `` \` `` closes one. - A paragraph's opening `[` escapes wherever its line reads as a link reference definition, which - resolves before any inline construct binds: a `]` inside a code span or `{attrs}` counts. + Where a paragraph opens with what reads as a link reference definition, which resolves before + any inline construct binds (a `]` inside a code span or `{attrs}` counts), an opening text `[` + escapes and an opening link's nodes ride the carry. An emphasis delimiter run in text escapes where CommonMark can open **or** close with it, so `*not emphasis*` is `\*not emphasis\*` — no delimiter the emitter did not write reaches the matching below, which is what lets the emitter decide its own pairings. diff --git a/src/markdown/emit/inline-line.ts b/src/markdown/emit/inline-line.ts index 6c0105b..d9c91cb 100644 --- a/src/markdown/emit/inline-line.ts +++ b/src/markdown/emit/inline-line.ts @@ -292,6 +292,6 @@ function emitLink(nodes: readonly AdfNode[], mark: AdfMark, depth: number, range if (!inner.ok) return inner if (inner.value.carry !== undefined) return inner const spelledTarget: InlineSegment = context.bracketed ? { escaping: 'bracketed-link-target', text: escapeUnbalanced(target.value, '[', ']') } : syntax(target.value) - return success({ segments: [syntax('['), ...inner.value.segments, syntax(']('), spelledTarget, syntax(')')] }) + return success({ segments: [{ escaping: 'none', nodes: range, text: '[' }, ...inner.value.segments, syntax(']('), spelledTarget, syntax(')')] }) } diff --git a/src/markdown/emit/line-escaping.ts b/src/markdown/emit/line-escaping.ts index 4f51118..66d2879 100644 --- a/src/markdown/emit/line-escaping.ts +++ b/src/markdown/emit/line-escaping.ts @@ -14,7 +14,8 @@ export type NodeRange = { first: number; last: number } export type InlineSegment = | { emphasis: EmphasisRole; escaping: 'none'; nodes: NodeRange; text: string } - | { emphasis?: undefined; escaping: InlineEscaping; text: string } + | { emphasis?: undefined; escaping: 'none'; nodes: NodeRange; text: string } + | { emphasis?: undefined; escaping: InlineEscaping; nodes?: undefined; text: string } export type AssembledLine = { line: string; unspellableRun: NodeRange | undefined } @@ -75,8 +76,12 @@ function escape(segments: readonly InlineSegment[], container: LineContainer): A placements.push(output.length) output += scan.charAt(index) } - const line = container === 'paragraph' && opensLinkDefinition(output) ? `\\${output}` : output - return { line, unspellableRun: unspellableRun(segments, output, placements) } + if (container === 'paragraph' && opensLinkDefinition(output)) { + const opener = segments[0]?.nodes + if (opener !== undefined) return { line: output, unspellableRun: opener } + return { line: `\\${output}`, unspellableRun: unspellableRun(segments, output, placements) } + } + return { line: output, unspellableRun: unspellableRun(segments, output, placements) } } function escapedIndexes(scan: string, escapings: readonly InlineEscaping[], container: LineContainer): Set {