From b6ba4404108f7f58797d5e8b1942a46e4138276c Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Tue, 15 Sep 2026 22:28:51 +0200 Subject: [PATCH] 4.3: a bare backtick run escapes wherever a later string of its length forms around an escape --- .../combinations/code-span-joined-run.json | 73 +++++++++++++++++++ .../combinations/code-span-joined-run.md | 5 ++ spec/flavour.md | 6 +- src/markdown/emit/line-escaping.ts | 32 ++++++-- 4 files changed, 106 insertions(+), 10 deletions(-) create mode 100644 corpus/round-trip/combinations/code-span-joined-run.json create mode 100644 corpus/round-trip/combinations/code-span-joined-run.md diff --git a/corpus/round-trip/combinations/code-span-joined-run.json b/corpus/round-trip/combinations/code-span-joined-run.json new file mode 100644 index 0000000..6d8351a --- /dev/null +++ b/corpus/round-trip/combinations/code-span-joined-run.json @@ -0,0 +1,73 @@ +{ + "content": [ + { + "content": [ + { + "text": "`` ``", + "type": "text" + }, + { + "marks": [ + { + "type": "code" + } + ], + "text": "a", + "type": "text" + } + ], + "type": "paragraph" + }, + { + "content": [ + { + "marks": [ + { + "type": "underline" + } + ], + "text": "``", + "type": "text" + }, + { + "text": "``", + "type": "text" + }, + { + "marks": [ + { + "type": "code" + } + ], + "text": "a", + "type": "text" + } + ], + "type": "paragraph" + }, + { + "content": [ + { + "text": "``\\`", + "type": "text" + }, + { + "marks": [ + { + "type": "code" + } + ], + "text": "`", + "type": "text" + }, + { + "text": "`", + "type": "text" + } + ], + "type": "paragraph" + } + ], + "type": "doc", + "version": 1 +} diff --git a/corpus/round-trip/combinations/code-span-joined-run.md b/corpus/round-trip/combinations/code-span-joined-run.md new file mode 100644 index 0000000..a89d1f6 --- /dev/null +++ b/corpus/round-trip/combinations/code-span-joined-run.md @@ -0,0 +1,5 @@ +\`\` \`\``a` + +:underline[\`\`]\`\``a` + +\`\`\\\``` ` ``\` diff --git a/spec/flavour.md b/spec/flavour.md index e407647..ebdf7e4 100644 --- a/spec/flavour.md +++ b/spec/flavour.md @@ -54,8 +54,10 @@ normalizes to it through the round-trip. - Entity references in input decode to their characters; output backslash-escapes only where text would otherwise parse as syntax, scanning the assembled line rather than each text node: escape 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 backtick run escapes whole, and a bare one escapes wherever a later backtick string of its + length forms around an escape in the same inline content — an escaped backtick alone, one joined + to the bare run after it, or a bare run an escape splits off: CommonMark reads no escape inside a + code span, so such a string still closes one. 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. diff --git a/src/markdown/emit/line-escaping.ts b/src/markdown/emit/line-escaping.ts index 66d2879..50ab2ce 100644 --- a/src/markdown/emit/line-escaping.ts +++ b/src/markdown/emit/line-escaping.ts @@ -103,17 +103,33 @@ function escapedIndexes(scan: string, escapings: readonly InlineEscaping[], cont escaped.add(index) } } - escapeLoneBackticks(scan, escapings, escaped) + escapeClosedRuns(scan, escapings, escaped) return escaped } -// CommonMark reads no escape inside a code span, so an escaped backtick still closes one a bare lone backtick before it opens. -function escapeLoneBackticks(scan: string, escapings: readonly InlineEscaping[], escaped: Set): void { - let escapedAfter = false - for (let index = scan.length - 1; index >= 0; index -= 1) { - if (scan.charAt(index) !== '`') continue - if (escapedAfter && escapings[index] !== 'none' && scan.charAt(index - 1) !== '`' && scan.charAt(index + 1) !== '`') escaped.add(index) - if (escaped.has(index)) escapedAfter = true +// CommonMark reads no escape inside a code span, so a backtick string an escape forms or splits off still closes one an earlier bare run opens. +function escapeClosedRuns(scan: string, escapings: readonly InlineEscaping[], escaped: Set): void { + const formed = new Set() + let end = scan.length - 1 + while (end >= 0) { + if (scan.charAt(end) !== '`') { + end -= 1 + continue + } + let start = end + while (scan.charAt(start - 1) === '`') start -= 1 + let segmentEnd = end + for (let index = end; index > start; index -= 1) { + if (!escaped.has(index)) continue + formed.add(segmentEnd - index + 1) + segmentEnd = index - 1 + } + if (segmentEnd !== end || escaped.has(start)) formed.add(segmentEnd - start + 1) + else if (escapings[start] !== 'none' && formed.has(end - start + 1)) { + for (let index = start; index <= end; index += 1) escaped.add(index) + formed.add(1) + } + end = start - 1 } }