4.3: a paragraph line reading as a link reference definition escapes its opening bracket

This commit is contained in:
2026-09-15 20:45:07 +02:00
parent ec5c92d4fd
commit 4735933d61
6 changed files with 95 additions and 6 deletions
@@ -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
}
@@ -0,0 +1,6 @@
\[:date{timestamp="]:a"}
> \[`]: a`
- \[:date{timestamp="]:a"}\
b
+2
View File
@@ -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 - 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 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. 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 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.
+4 -3
View File
@@ -3,6 +3,7 @@ import { delimiterFlags, isWordCharacter, matchEmphasis, runLength } from '../em
import { backslashEscape, escapesLineClaim, inlineHtmlConstruct, opensBracketedAutolink, opensEmailAutolink, type LinePosition } from '../commonmark-grammar.ts' import { backslashEscape, escapesLineClaim, inlineHtmlConstruct, opensBracketedAutolink, opensEmailAutolink, type LinePosition } from '../commonmark-grammar.ts'
import { isBareDelimiterRow } from '../pipe-table-syntax.ts' import { isBareDelimiterRow } from '../pipe-table-syntax.ts'
import { opensInlineDirective } from '../directive-syntax.ts' import { opensInlineDirective } from '../directive-syntax.ts'
import { opensLinkDefinition } from '../link-reference-definitions.ts'
import { readEntityReference } from '../entity-references.ts' import { readEntityReference } from '../entity-references.ts'
export type EmphasisRole = 'close' | 'open' export type EmphasisRole = 'close' | 'open'
@@ -27,8 +28,7 @@ type EmittedRun = { canClose: boolean; canOpen: boolean; character: string; deli
const delimiters = ['*', '_', '`', '~'] 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 { export function assembleInlineLine(segments: readonly InlineSegment[], container: LineContainer): AssembledLine {
return escape(resolveEmphasis(segments), container) return escape(resolveEmphasis(segments), container)
@@ -90,7 +90,8 @@ function escape(segments: readonly InlineSegment[], container: LineContainer): A
placements.push(output.length) placements.push(output.length)
output += scan.charAt(index) 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 { function unspellableRun(segments: readonly InlineSegment[], output: string, placements: readonly number[]): NodeRange | undefined {
@@ -1,10 +1,14 @@
import type { LinkDefinition, LinkPart } from '../link-syntax.ts' import type { LinkDefinition, LinkPart } from './link-syntax.ts'
import { normalizeLabel, readDestination, readLabel, readTitle, skipLinkWhitespace } from '../link-syntax.ts' import { normalizeLabel, readDestination, readLabel, readTitle, skipLinkWhitespace } from './link-syntax.ts'
type ReadDefinition = { definition: LinkDefinition; label: string; length: number } type ReadDefinition = { definition: LinkDefinition; label: string; length: number }
const restOfLine = /^[ \t]*(?:\n|$)/ const restOfLine = /^[ \t]*(?:\n|$)/
export function opensLinkDefinition(text: string): boolean {
return readDefinition(text) !== undefined
}
export function readLinkDefinitions(definitions: Map<string, LinkDefinition>, text: string): string { export function readLinkDefinitions(definitions: Map<string, LinkDefinition>, text: string): string {
let rest = text let rest = text
let read = readDefinition(rest) let read = readDefinition(rest)
+1 -1
View File
@@ -18,7 +18,7 @@ import {
} from '../commonmark-grammar.ts' } from '../commonmark-grammar.ts'
import { directiveLineEscape, malformedDirective, readDirectiveLine } from '../directive-syntax.ts' import { directiveLineEscape, malformedDirective, readDirectiveLine } from '../directive-syntax.ts'
import { barePipeCells, isDelimiterRow, isPipeAlignment, isPipeDelimiter, malformedPipeTable, pipeCells } from '../pipe-table-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 } & ( export type Block = { position: SourcePosition } & (
| { argument: string | undefined; attributes: DirectiveAttributes; blocks: Block[] | undefined; kind: 'directive'; name: string } | { argument: string | undefined; attributes: DirectiveAttributes; blocks: Block[] | undefined; kind: 'directive'; name: string }