Emitter 2c: the inline node directives, the directive marks and the carried whitespace
CI / gate (push) Successful in 5s

This commit is contained in:
2026-08-26 09:06:13 +02:00
parent c7e51b1731
commit efc267db2b
10 changed files with 355 additions and 121 deletions
+27 -23
View File
@@ -1,8 +1,12 @@
import { escapesLineClaim, isUnicodeWhitespace, opensBracketedAutolink, startsEntityReference, type LinePosition } from './commonmark-grammar.ts'
export type EmphasisRole = 'close' | 'open'
export type InlineEscaping = 'attribute' | 'backslash' | 'bracketed' | 'none'
export type InlineSegment =
| { kind: 'emphasis-close' | 'emphasis-open'; mark: string; text: string }
| { kind: 'link-text' | 'literal' | 'syntax'; text: string }
| { emphasis: EmphasisRole; escaping: 'none'; mark: string; text: string }
| { emphasis?: undefined; escaping: InlineEscaping; text: string }
export type AssembledLine = { line: string; unspellableMark: string | undefined }
@@ -36,8 +40,8 @@ function resolveEmphasis(segments: readonly InlineSegment[]): InlineSegment[] {
for (let index = 0; index < resolved.length; index += 1) {
const segment = resolved[index]
if (segment === undefined) continue
if (segment.kind === 'emphasis-open') open.push(index)
if (segment.kind !== 'emphasis-close') continue
if (segment.emphasis === 'open') open.push(index)
if (segment.emphasis !== 'close') continue
const openerIndex = open.pop()
const opener = openerIndex === undefined ? undefined : resolved[openerIndex]
if (openerIndex === undefined || opener === undefined) continue
@@ -53,15 +57,15 @@ function resolveEmphasis(segments: readonly InlineSegment[]): InlineSegment[] {
function escape(segments: readonly InlineSegment[], container: LineContainer): AssembledLine {
const scan = segments.map((segment) => segment.text).join('')
const kinds: InlineSegment['kind'][] = []
for (const segment of segments) for (let index = 0; index < segment.text.length; index += 1) kinds.push(segment.kind)
const escapings: InlineEscaping[] = []
for (const segment of segments) for (let index = 0; index < segment.text.length; index += 1) escapings.push(segment.escaping)
const escaped = new Set<number>()
const placements: number[] = []
let output = ''
for (let index = 0; index < scan.length; index += 1) {
const kind = kinds[index]
const escapable = kind === 'literal' || kind === 'link-text'
if (escapable && (mergesWithSyntax(scan, kinds, index) || opensConstruct(scan, index, kind === 'link-text', container, escaped))) {
const escaping = escapings[index]
const escapable = escaping === 'backslash' || escaping === 'bracketed'
if (escapable && (mergesWithSyntax(scan, escapings, index) || opensConstruct(scan, index, escaping === 'bracketed', container, escaped))) {
output += '\\'
escaped.add(index)
}
@@ -87,8 +91,8 @@ function delimiterRuns(segments: readonly InlineSegment[], placements: readonly
for (const segment of segments) {
const start = placements[cursor] ?? 0
cursor += segment.text.length
if (segment.kind !== 'emphasis-close' && segment.kind !== 'emphasis-open') continue
const closes = segment.kind === 'emphasis-close'
if (segment.emphasis === undefined) continue
const closes = segment.emphasis === 'close'
const end = start + segment.text.length
const previous = runs[runs.length - 1]
if (previous !== undefined && previous.end === start && previous.character === segment.text.charAt(0)) {
@@ -108,28 +112,28 @@ function delimiterRuns(segments: readonly InlineSegment[], placements: readonly
return runs
}
function mergesWithSyntax(scan: string, kinds: readonly (InlineSegment['kind'] | undefined)[], index: number): boolean {
function mergesWithSyntax(scan: string, escapings: readonly (InlineEscaping | undefined)[], index: number): boolean {
const character = scan.charAt(index)
if (character === '!') return scan.charAt(index + 1) === '[' && isSyntax(kinds[index + 1])
if (character === '!') return scan.charAt(index + 1) === '[' && isSyntax(escapings[index + 1])
if (!delimiters.includes(character)) return false
return touchesSyntax(scan, kinds, index, -1) || touchesSyntax(scan, kinds, index, 1)
return touchesSyntax(scan, escapings, index, -1) || touchesSyntax(scan, escapings, index, 1)
}
function touchesSyntax(scan: string, kinds: readonly (InlineSegment['kind'] | undefined)[], index: number, step: number): boolean {
function touchesSyntax(scan: string, escapings: readonly (InlineEscaping | undefined)[], index: number, step: number): boolean {
const character = scan.charAt(index)
let cursor = index + step
while (scan.charAt(cursor) === character && !isSyntax(kinds[cursor])) cursor += step
return scan.charAt(cursor) === character && isSyntax(kinds[cursor])
while (scan.charAt(cursor) === character && !isSyntax(escapings[cursor])) cursor += step
return scan.charAt(cursor) === character && isSyntax(escapings[cursor])
}
function isSyntax(kind: InlineSegment['kind'] | undefined): boolean {
return kind === 'emphasis-close' || kind === 'emphasis-open' || kind === 'syntax'
function isSyntax(escaping: InlineEscaping | undefined): boolean {
return escaping === 'attribute' || escaping === 'none'
}
function opensConstruct(scan: string, index: number, inLinkText: boolean, container: LineContainer, escaped: ReadonlySet<number>): boolean {
function opensConstruct(scan: string, index: number, inBrackets: boolean, container: LineContainer, escaped: ReadonlySet<number>): boolean {
if (container === 'heading' && closesHeading(scan, index)) return true
if (container === 'paragraph' && claimsLineStart(scan, index)) return true
return claimsCharacter(scan, index, inLinkText, container, escaped)
return claimsCharacter(scan, index, inBrackets, container, escaped)
}
function claimsLineStart(scan: string, index: number): boolean {
@@ -145,10 +149,10 @@ function closesHeading(scan: string, index: number): boolean {
return index === 0 || /[ \t]/.test(scan.charAt(index - 1))
}
function claimsCharacter(scan: string, index: number, inLinkText: boolean, container: LineContainer, escaped: ReadonlySet<number>): boolean {
function claimsCharacter(scan: string, index: number, inBrackets: boolean, container: LineContainer, escaped: ReadonlySet<number>): boolean {
const character = scan.charAt(index)
const rest = scan.slice(index)
if (inLinkText && (character === '[' || character === ']')) return true
if (inBrackets && (character === '[' || character === ']')) return true
if (character === '|') return container === 'table-cell'
if (character === '\\') return asciiPunctuation.test(scan.charAt(index + 1))
if (character === '&') return startsEntityReference(rest)