diff --git a/corpus/round-trip/combinations/link-definition-line.json b/corpus/round-trip/combinations/link-definition-line.json new file mode 100644 index 0000000..fd16f9f --- /dev/null +++ b/corpus/round-trip/combinations/link-definition-line.json @@ -0,0 +1,76 @@ +{ + "content": [ + { + "content": [ + { + "text": "[", + "type": "text" + }, + { + "attrs": { + "timestamp": "]:a" + }, + "type": "date" + } + ], + "type": "paragraph" + }, + { + "content": [ + { + "content": [ + { + "text": "[", + "type": "text" + }, + { + "marks": [ + { + "type": "code" + } + ], + "text": "]: a", + "type": "text" + } + ], + "type": "paragraph" + } + ], + "type": "blockquote" + }, + { + "content": [ + { + "content": [ + { + "content": [ + { + "text": "[", + "type": "text" + }, + { + "attrs": { + "timestamp": "]:a" + }, + "type": "date" + }, + { + "type": "hardBreak" + }, + { + "text": "b", + "type": "text" + } + ], + "type": "paragraph" + } + ], + "type": "listItem" + } + ], + "type": "bulletList" + } + ], + "type": "doc", + "version": 1 +} diff --git a/corpus/round-trip/combinations/link-definition-line.md b/corpus/round-trip/combinations/link-definition-line.md new file mode 100644 index 0000000..b5c5a28 --- /dev/null +++ b/corpus/round-trip/combinations/link-definition-line.md @@ -0,0 +1,6 @@ +\[:date{timestamp="]:a"} + +> \[`]: a` + +- \[:date{timestamp="]:a"}\ + b diff --git a/spec/flavour.md b/spec/flavour.md index c0f802b..e80d8fb 100644 --- a/spec/flavour.md +++ b/spec/flavour.md @@ -54,6 +54,8 @@ 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 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. 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/line-escaping.ts b/src/markdown/emit/line-escaping.ts index eeaa7ec..4671702 100644 --- a/src/markdown/emit/line-escaping.ts +++ b/src/markdown/emit/line-escaping.ts @@ -3,6 +3,7 @@ import { delimiterFlags, isWordCharacter, matchEmphasis, runLength } from '../em import { backslashEscape, escapesLineClaim, inlineHtmlConstruct, opensBracketedAutolink, opensEmailAutolink, type LinePosition } from '../commonmark-grammar.ts' import { isBareDelimiterRow } from '../pipe-table-syntax.ts' import { opensInlineDirective } from '../directive-syntax.ts' +import { opensLinkDefinition } from '../link-reference-definitions.ts' import { readEntityReference } from '../entity-references.ts' export type EmphasisRole = 'close' | 'open' @@ -27,8 +28,7 @@ type EmittedRun = { canClose: boolean; canOpen: boolean; character: string; deli const delimiters = ['*', '_', '`', '~'] -// The `:` keeps a `[label]: url` line escaped: unescaped, the parser swallows it as a link reference definition. -const followsLinkText = /[([:]/ +const followsLinkText = /[([]/ export function assembleInlineLine(segments: readonly InlineSegment[], container: LineContainer): AssembledLine { return escape(resolveEmphasis(segments), container) @@ -90,7 +90,8 @@ function escape(segments: readonly InlineSegment[], container: LineContainer): A placements.push(output.length) output += scan.charAt(index) } - return { line: output, unspellableRun: unspellableRun(segments, output, placements) } + const spelled = container === 'paragraph' && opensLinkDefinition(output) ? `\\${output}` : output + return { line: spelled, unspellableRun: unspellableRun(segments, output, placements) } } function unspellableRun(segments: readonly InlineSegment[], output: string, placements: readonly number[]): NodeRange | undefined { diff --git a/src/markdown/parse/link-reference-definitions.ts b/src/markdown/link-reference-definitions.ts similarity index 90% rename from src/markdown/parse/link-reference-definitions.ts rename to src/markdown/link-reference-definitions.ts index 8edbefd..d69d90f 100644 --- a/src/markdown/parse/link-reference-definitions.ts +++ b/src/markdown/link-reference-definitions.ts @@ -1,10 +1,14 @@ -import type { LinkDefinition, LinkPart } from '../link-syntax.ts' -import { normalizeLabel, readDestination, readLabel, readTitle, skipLinkWhitespace } from '../link-syntax.ts' +import type { LinkDefinition, LinkPart } from './link-syntax.ts' +import { normalizeLabel, readDestination, readLabel, readTitle, skipLinkWhitespace } from './link-syntax.ts' type ReadDefinition = { definition: LinkDefinition; label: string; length: number } const restOfLine = /^[ \t]*(?:\n|$)/ +export function opensLinkDefinition(text: string): boolean { + return readDefinition(text) !== undefined +} + export function readLinkDefinitions(definitions: Map, text: string): string { let rest = text let read = readDefinition(rest) diff --git a/src/markdown/parse/blocks.ts b/src/markdown/parse/blocks.ts index 7527d38..db40412 100644 --- a/src/markdown/parse/blocks.ts +++ b/src/markdown/parse/blocks.ts @@ -18,7 +18,7 @@ import { } from '../commonmark-grammar.ts' import { directiveLineEscape, malformedDirective, readDirectiveLine } from '../directive-syntax.ts' import { barePipeCells, isDelimiterRow, isPipeAlignment, isPipeDelimiter, malformedPipeTable, pipeCells } from '../pipe-table-syntax.ts' -import { readLinkDefinitions } from './link-reference-definitions.ts' +import { readLinkDefinitions } from '../link-reference-definitions.ts' export type Block = { position: SourcePosition } & ( | { argument: string | undefined; attributes: DirectiveAttributes; blocks: Block[] | undefined; kind: 'directive'; name: string }