From 507f68aa13c38da8160265c099f5be8bd7aa5ee6 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 14 Sep 2026 21:16:08 +0200 Subject: [PATCH] 4.2: a link target's backtick escapes where it would open a code span inside a directive's content --- .../directive-link-backticks.json | 83 +++++++++++++++++++ .../combinations/directive-link-backticks.md | 5 ++ spec/flavour.md | 6 +- src/markdown/emit/inline-line.ts | 8 +- src/markdown/emit/line-escaping.ts | 15 ++-- 5 files changed, 103 insertions(+), 14 deletions(-) create mode 100644 corpus/round-trip/combinations/directive-link-backticks.json create mode 100644 corpus/round-trip/combinations/directive-link-backticks.md diff --git a/corpus/round-trip/combinations/directive-link-backticks.json b/corpus/round-trip/combinations/directive-link-backticks.json new file mode 100644 index 0000000..00c8b53 --- /dev/null +++ b/corpus/round-trip/combinations/directive-link-backticks.json @@ -0,0 +1,83 @@ +{ + "content": [ + { + "content": [ + { + "marks": [ + { + "attrs": { + "color": "" + }, + "type": "textColor" + }, + { + "attrs": { + "href": "`" + }, + "type": "link" + } + ], + "text": " ", + "type": "text" + }, + { + "text": "`a", + "type": "text" + } + ], + "type": "paragraph" + }, + { + "content": [ + { + "marks": [ + { + "type": "underline" + }, + { + "attrs": { + "href": "c", + "title": "`" + }, + "type": "link" + } + ], + "text": "b", + "type": "text" + }, + { + "marks": [ + { + "type": "code" + } + ], + "text": "c", + "type": "text" + } + ], + "type": "paragraph" + }, + { + "content": [ + { + "marks": [ + { + "type": "underline" + }, + { + "attrs": { + "href": "e`f" + }, + "type": "link" + } + ], + "text": "d", + "type": "text" + } + ], + "type": "paragraph" + } + ], + "type": "doc", + "version": 1 +} diff --git a/corpus/round-trip/combinations/directive-link-backticks.md b/corpus/round-trip/combinations/directive-link-backticks.md new file mode 100644 index 0000000..85d19f6 --- /dev/null +++ b/corpus/round-trip/combinations/directive-link-backticks.md @@ -0,0 +1,5 @@ +:textColor[[ ](\`)]{color=""}`a + +:underline[[b](c "\`")]`c` + +:underline[[d](e`f)] diff --git a/spec/flavour.md b/spec/flavour.md index 9f8a961..d503374 100644 --- a/spec/flavour.md +++ b/spec/flavour.md @@ -132,9 +132,9 @@ 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 (an autolink holding one takes the -`[text](url)` form), 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. +`[text](url)` form) 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. **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/emit/inline-line.ts b/src/markdown/emit/inline-line.ts index e889383..deed5ff 100644 --- a/src/markdown/emit/inline-line.ts +++ b/src/markdown/emit/inline-line.ts @@ -1,6 +1,6 @@ import type { AdfMark, AdfNode } from '../../adf/document.ts' import type { InlineDirective } from '../../adf/inline-directives.ts' -import { assembleInlineLine, type InlineEscaping, type InlineSegment, type LineContainer, type NodeRange } from './line-escaping.ts' +import { assembleInlineLine, isSyntax, type InlineEscaping, type InlineSegment, type LineContainer, type NodeRange } from './line-escaping.ts' import { carriedInline } from '../opaque-carry.ts' import { claimsLine, holdsNullCharacter, isAutolink } from '../commonmark-grammar.ts' import { escapeUnbalanced, spellDestination, spellLinkTarget } from '../link-syntax.ts' @@ -41,7 +41,7 @@ export function emitInlineLine(nodes: readonly AdfNode[], container: LineContain export function tryPipeCell(nodes: readonly AdfNode[], path: ConvertErrorPath): string | undefined { const emitted = emitLine(nodes, 'table-cell', path) if (!emitted.ok) return undefined - if (emitted.value.segments.some((segment) => segment.escaping === 'none' && segment.text.includes('|'))) return undefined + if (emitted.value.segments.some((segment) => isSyntax(segment.escaping) && segment.text.includes('|'))) return undefined return emitted.value.line } @@ -291,7 +291,7 @@ function emitLink(nodes: readonly AdfNode[], mark: AdfMark, depth: number, range const inner = emitRun(nodes, depth + 1, range.first, { ...context, bracketed: true }) if (!inner.ok) return inner if (inner.value.carry !== undefined) return inner - const spelledTarget = context.bracketed ? escapeUnbalanced(target.value, '[', ']') : target.value - return success({ segments: [syntax('['), ...inner.value.segments, syntax(`](${spelledTarget})`)] }) + 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(')')] }) } diff --git a/src/markdown/emit/line-escaping.ts b/src/markdown/emit/line-escaping.ts index 4861771..f73b331 100644 --- a/src/markdown/emit/line-escaping.ts +++ b/src/markdown/emit/line-escaping.ts @@ -7,7 +7,7 @@ import { readEntityReference } from '../entity-references.ts' export type EmphasisRole = 'close' | 'open' -export type InlineEscaping = 'backslash' | 'bracketed' | 'none' +export type InlineEscaping = 'backslash' | 'bracketed' | 'bracketed-link-target' | 'none' export type NodeRange = { first: number; last: number } @@ -77,10 +77,11 @@ function escape(segments: readonly InlineSegment[], container: LineContainer): A const escaping = escapings[index] const escapable = escaping === 'backslash' || escaping === 'bracketed' if ( - escapable && - (claimsLineStart(line, index, container) || - mergesWithSyntax(scan, escapings, index) || - opensConstruct(scan, linkClose, index, escaping === 'bracketed', container, escaped)) + (escapable && + (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)) ) { output += '\\' escaped.add(index) @@ -181,8 +182,8 @@ function touchesSyntax(scan: string, escapings: readonly (InlineEscaping | undef return scan.charAt(cursor) === character && isSyntax(escapings[cursor]) } -function isSyntax(escaping: InlineEscaping | undefined): boolean { - return escaping === 'none' +export function isSyntax(escaping: InlineEscaping | undefined): boolean { + return escaping === 'none' || escaping === 'bracketed-link-target' } function opensConstruct(