From 342c2027d8e586eccc9f2f27cef57365c381578d Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 14 Sep 2026 23:24:21 +0200 Subject: [PATCH] 4.2: a link target's would-be directive escapes inside a directive's content --- .../combinations/directive-link-colon.json | 107 ++++++++++++++++++ .../combinations/directive-link-colon.md | 9 ++ spec/flavour.md | 12 +- src/markdown/directive-syntax.ts | 5 + src/markdown/emit/inline-line.ts | 4 +- src/markdown/emit/line-escaping.ts | 3 +- 6 files changed, 131 insertions(+), 9 deletions(-) create mode 100644 corpus/round-trip/combinations/directive-link-colon.json create mode 100644 corpus/round-trip/combinations/directive-link-colon.md diff --git a/corpus/round-trip/combinations/directive-link-colon.json b/corpus/round-trip/combinations/directive-link-colon.json new file mode 100644 index 0000000..2cc9c86 --- /dev/null +++ b/corpus/round-trip/combinations/directive-link-colon.json @@ -0,0 +1,107 @@ +{ + "content": [ + { + "content": [ + { + "marks": [ + { + "type": "underline" + }, + { + "attrs": { + "href": ":a{" + }, + "type": "link" + } + ], + "text": "x", + "type": "text" + } + ], + "type": "paragraph" + }, + { + "content": [ + { + "marks": [ + { + "type": "underline" + }, + { + "attrs": { + "href": "y", + "title": ":a{" + }, + "type": "link" + } + ], + "text": "x", + "type": "text" + } + ], + "type": "paragraph" + }, + { + "content": [ + { + "marks": [ + { + "type": "underline" + }, + { + "attrs": { + "href": ":a[]{" + }, + "type": "link" + } + ], + "text": "x", + "type": "text" + } + ], + "type": "paragraph" + }, + { + "content": [ + { + "marks": [ + { + "type": "underline" + }, + { + "attrs": { + "href": "https://example.com/?q=:emoji{" + }, + "type": "link" + } + ], + "text": "x", + "type": "text" + } + ], + "type": "paragraph" + }, + { + "content": [ + { + "marks": [ + { + "type": "underline" + }, + { + "attrs": { + "href": "ab:c{" + }, + "type": "link" + } + ], + "text": "ab:c{", + "type": "text" + } + ], + "type": "paragraph" + } + ], + "type": "doc", + "version": 1 +} diff --git a/corpus/round-trip/combinations/directive-link-colon.md b/corpus/round-trip/combinations/directive-link-colon.md new file mode 100644 index 0000000..ecf5427 --- /dev/null +++ b/corpus/round-trip/combinations/directive-link-colon.md @@ -0,0 +1,9 @@ +:underline[[x](\:a{)] + +:underline[[x](y "\:a{")] + +:underline[[x](\:a[]{)] + +:underline[[x](https://example.com/?q=\:emoji{)] + +:underline[[ab\:c{](ab\:c{)] diff --git a/spec/flavour.md b/spec/flavour.md index d3506f5..c0f802b 100644 --- a/spec/flavour.md +++ b/spec/flavour.md @@ -48,8 +48,8 @@ normalizes to it through the round-trip. title; title in double quotes. A backslash escapes a parenthesis the destination leaves unbalanced, and a quote inside the title; a balanced pair stays bare. `` autolink form only when the text equals the destination and the destination is a valid CommonMark autolink - (absolute URI) — inside an inline directive's `[content]`, one holding no backtick and no - unbalanced bracket. + (absolute URI) — inside an inline directive's `[content]`, one holding no backtick, no + unbalanced bracket and no inline directive opener. - Paragraphs on one line — no soft wrapping; a soft line break in input becomes a single space. - 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 @@ -132,10 +132,10 @@ each a named error naming the spelling to write instead. **Escaping**: the emitter backslash-escapes whatever literal text would otherwise parse as directive syntax — the leading `:` of a would-be directive, `]` inside content, a bracket a link's -destination and title inside content leave unbalanced and a backtick there that would open a code -span, a `{` right after a directive's closing `]`, which would otherwise be read as the attributes -it has none of; outside code spans and code blocks, a backslash before `:` in input yields a -literal colon. +destination and title inside content leave unbalanced, a backtick there that would open a code span +and a `:` there that would open an inline directive, a `{` right after a directive's closing `]`, +which would otherwise be read as the attributes it has none of; outside code spans and code blocks, +a backslash before `:` in input yields a literal colon. **Malformed directives are error results**, named: an unclosed container at end of input, a body fence line of the container's length or longer, a bare colon-run line outside any container or diff --git a/src/markdown/directive-syntax.ts b/src/markdown/directive-syntax.ts index 87c4976..3c0ec9e 100644 --- a/src/markdown/directive-syntax.ts +++ b/src/markdown/directive-syntax.ts @@ -56,6 +56,11 @@ export function attributeValue(text: string, kind: AttributeKind): AttributeRead return overNested(parsed) ? { refusal: 'nesting' } : { value: { kind, value: parsed } } } +export function holdsInlineDirectiveOpener(text: string): boolean { + for (let index = text.indexOf(':'); index !== -1; index = text.indexOf(':', index + 1)) if (opensInlineDirective(text, index)) return true + return false +} + export function isBareToken(text: string): boolean { return bareToken.test(text) } diff --git a/src/markdown/emit/inline-line.ts b/src/markdown/emit/inline-line.ts index ec3bf1f..6c0105b 100644 --- a/src/markdown/emit/inline-line.ts +++ b/src/markdown/emit/inline-line.ts @@ -6,13 +6,13 @@ import { claimsLine, holdsNullCharacter, isAutolink } from '../commonmark-gramma import { escapeUnbalanced, spellDestination, spellLinkTarget } from '../link-syntax.ts' import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts' import { holdsEntityReference } from '../entity-references.ts' +import { holdsInlineDirectiveOpener, slotLineEndingFault, spellLeafDirective } from '../directive-syntax.ts' import { inlineDirective } from '../../adf/inline-directives.ts' import { largestNesting } from '../../nesting.ts' import { longestBacktickRun } from '../backtick-runs.ts' import { markSpelling, spellMarkAttributes } from '../mark-spellings.ts' import { nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts' import { sameMark } from '../../adf/editor-normal.ts' -import { slotLineEndingFault, spellLeafDirective } from '../directive-syntax.ts' import { spellInlineNodeAttributes } from './inline-directive-spelling.ts' import { spellTextDirective } from '../text-directive.ts' @@ -284,7 +284,7 @@ function emitLink(nodes: readonly AdfNode[], mark: AdfMark, depth: number, range if (typeof href !== 'string') return success({ carry: range }) const node = nodes[0] const bare = nodes.length === 1 && node !== undefined && node.type === 'text' && node.text === href && nodeMarks(node).length === depth + 1 - const autolinkHolds = !context.bracketed || (!href.includes('`') && escapeUnbalanced(href, '[', ']') === href) + const autolinkHolds = !context.bracketed || (!href.includes('`') && !holdsInlineDirectiveOpener(href) && escapeUnbalanced(href, '[', ']') === href) if (bare && autolinkHolds && title === undefined && isAutolink(href) && !holdsEntityReference(href)) return success({ segments: [syntax(`<${href}>`)] }) const target = spellLinkTarget(href, typeof title === 'string' ? title : undefined, path) if (!target.ok) return target diff --git a/src/markdown/emit/line-escaping.ts b/src/markdown/emit/line-escaping.ts index f73b331..eeaa7ec 100644 --- a/src/markdown/emit/line-escaping.ts +++ b/src/markdown/emit/line-escaping.ts @@ -81,7 +81,8 @@ function escape(segments: readonly InlineSegment[], container: LineContainer): A (claimsLineStart(line, index, container) || mergesWithSyntax(scan, escapings, index) || opensConstruct(scan, linkClose, index, escaping === 'bracketed', container, escaped))) || - (escaping === 'bracketed-link-target' && scan.charAt(index) === '`' && opensCodeSpan(scan, index, escaped)) + (escaping === 'bracketed-link-target' && + ((scan.charAt(index) === '`' && opensCodeSpan(scan, index, escaped)) || (scan.charAt(index) === ':' && opensInlineDirective(scan, index)))) ) { output += '\\' escaped.add(index)