4.3: a link opening a paragraph that reads as a link reference definition rides the carry

This commit is contained in:
2026-09-15 21:22:34 +02:00
parent aeb54d98d1
commit fe47e6bc7b
5 changed files with 60 additions and 6 deletions
@@ -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
}
@@ -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\"}"}
+3 -2
View File
@@ -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. 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 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. 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 Where a paragraph opens with what reads as a link reference definition, which resolves before
resolves before any inline construct binds: a `]` inside a code span or `{attrs}` counts. 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 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 `*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. matching below, which is what lets the emitter decide its own pairings.
+1 -1
View File
@@ -292,6 +292,6 @@ function emitLink(nodes: readonly AdfNode[], mark: AdfMark, depth: number, range
if (!inner.ok) return inner if (!inner.ok) return inner
if (inner.value.carry !== undefined) 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) 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(')')] })
} }
+8 -3
View File
@@ -14,7 +14,8 @@ export type NodeRange = { first: number; last: number }
export type InlineSegment = export type InlineSegment =
| { emphasis: EmphasisRole; escaping: 'none'; nodes: NodeRange; text: string } | { 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 } export type AssembledLine = { line: string; unspellableRun: NodeRange | undefined }
@@ -75,8 +76,12 @@ function escape(segments: readonly InlineSegment[], container: LineContainer): A
placements.push(output.length) placements.push(output.length)
output += scan.charAt(index) output += scan.charAt(index)
} }
const line = container === 'paragraph' && opensLinkDefinition(output) ? `\\${output}` : output if (container === 'paragraph' && opensLinkDefinition(output)) {
return { line, unspellableRun: unspellableRun(segments, output, placements) } 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<number> { function escapedIndexes(scan: string, escapings: readonly InlineEscaping[], container: LineContainer): Set<number> {