Give CommonMark's emphasis section one home, and split the collision property
CI / gate (push) Successful in 5s
CI / gate (push) Successful in 5s
This commit is contained in:
+59
-51
@@ -1,5 +1,5 @@
|
||||
import { escapesLineClaim, isUnicodeWhitespace, opensBracketedAutolink, startsEntityReference, type LinePosition } from './commonmark-grammar.ts'
|
||||
import { unmatchedPair, type EmphasisDelimiter, type EmphasisRun } from './emphasis-matching.ts'
|
||||
import { escapesLineClaim, opensBracketedAutolink, startsEntityReference, type LinePosition } from './commonmark-grammar.ts'
|
||||
import { delimiterFlags, isWordCharacter, matchEmphasis } from './emphasis-matching.ts'
|
||||
|
||||
export type EmphasisRole = 'close' | 'open'
|
||||
|
||||
@@ -15,7 +15,9 @@ export type AssembledLine = { line: string; unspellableRun: NodeRange | undefine
|
||||
|
||||
export type LineContainer = 'heading' | 'paragraph' | 'table-cell'
|
||||
|
||||
type DelimiterGroup = { character: string; delimiters: EmphasisDelimiter[]; end: number; start: number }
|
||||
type EmittedDelimiter = { closes: boolean; offset: number; pair: number; width: number }
|
||||
|
||||
type EmittedRun = { canClose: boolean; canOpen: boolean; character: string; delimiters: EmittedDelimiter[]; length: number; start: number }
|
||||
|
||||
const delimiters = ['*', '_', '`', '~']
|
||||
|
||||
@@ -23,7 +25,6 @@ const asciiPunctuation = /[!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]/
|
||||
const htmlConstructs = [/^<[!?]/, /^<\/?[A-Za-z][A-Za-z0-9-]*(?:[\s/>]|$)/, /^<[^\s<>@]+@[^\s<>@]+>/]
|
||||
const inlineDirectiveOpener = /^:[a-z][A-Za-z0-9]*[[{]/
|
||||
const followsLinkText = /[([:]/
|
||||
const unicodePunctuation = /[\p{P}\p{S}]/u
|
||||
|
||||
export function assembleInlineLine(segments: readonly InlineSegment[], container: LineContainer): AssembledLine {
|
||||
return escape(resolveEmphasis(segments), container)
|
||||
@@ -79,18 +80,43 @@ function escape(segments: readonly InlineSegment[], container: LineContainer): A
|
||||
}
|
||||
|
||||
function unspellableRun(segments: readonly InlineSegment[], output: string, placements: readonly number[]): NodeRange | undefined {
|
||||
const { nodes, runs } = emphasisRuns(segments, placements, output)
|
||||
for (const run of runs) {
|
||||
for (const delimiter of run.delimiters) {
|
||||
if (!(delimiter.closes ? run.canClose : run.canOpen)) return nodes[delimiter.pair]
|
||||
}
|
||||
}
|
||||
const unmatched = unmatchedPair(runs)
|
||||
return unmatched === undefined ? undefined : nodes[unmatched]
|
||||
const { nodes, runs } = emittedRuns(segments, placements, output)
|
||||
const pair = misflanked(runs) ?? unpaired(runs)
|
||||
return pair === undefined ? undefined : nodes[pair]
|
||||
}
|
||||
|
||||
function emphasisRuns(segments: readonly InlineSegment[], placements: readonly number[], output: string): { nodes: NodeRange[]; runs: EmphasisRun[] } {
|
||||
const groups: DelimiterGroup[] = []
|
||||
function misflanked(runs: readonly EmittedRun[]): number | undefined {
|
||||
for (const run of runs) {
|
||||
for (const delimiter of run.delimiters) {
|
||||
if (!(delimiter.closes ? run.canClose : run.canOpen)) return delimiter.pair
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function unpaired(runs: readonly EmittedRun[]): number | undefined {
|
||||
const matched = new Set<number>()
|
||||
for (const pairing of matchEmphasis(runs)) {
|
||||
const opened = delimiterAt(pairing.opener, false, pairing.openerOffset, pairing.used)
|
||||
const closed = delimiterAt(pairing.closer, true, pairing.closerOffset, pairing.used)
|
||||
if (opened !== undefined && closed !== undefined && opened.pair === closed.pair) matched.add(opened.pair)
|
||||
}
|
||||
// The last opener left unpaired is the innermost: the smallest carry that changes the line.
|
||||
let innermost: number | undefined
|
||||
for (const run of runs) {
|
||||
for (const delimiter of run.delimiters) {
|
||||
if (!delimiter.closes && !matched.has(delimiter.pair)) innermost = delimiter.pair
|
||||
}
|
||||
}
|
||||
return innermost
|
||||
}
|
||||
|
||||
function delimiterAt(run: EmittedRun, closes: boolean, offset: number, width: number): EmittedDelimiter | undefined {
|
||||
return run.delimiters.find((delimiter) => delimiter.closes === closes && delimiter.offset === offset && delimiter.width === width)
|
||||
}
|
||||
|
||||
function emittedRuns(segments: readonly InlineSegment[], placements: readonly number[], output: string): { nodes: NodeRange[]; runs: EmittedRun[] } {
|
||||
const runs: EmittedRun[] = []
|
||||
const nodes: NodeRange[] = []
|
||||
const open: number[] = []
|
||||
let cursor = 0
|
||||
@@ -99,30 +125,33 @@ function emphasisRuns(segments: readonly InlineSegment[], placements: readonly n
|
||||
cursor += segment.text.length
|
||||
if (segment.emphasis === undefined) continue
|
||||
const closes = segment.emphasis === 'close'
|
||||
const end = start + segment.text.length
|
||||
const pair = closes ? (open.pop() ?? nodes.length) : nodes.length
|
||||
if (!closes) {
|
||||
nodes.push(segment.nodes)
|
||||
open.push(pair)
|
||||
}
|
||||
const delimiter = { closes, end, pair, start }
|
||||
const previous = groups[groups.length - 1]
|
||||
if (previous !== undefined && previous.end === start && previous.character === segment.text.charAt(0)) {
|
||||
previous.delimiters.push(delimiter)
|
||||
previous.end = end
|
||||
const width = segment.text.length
|
||||
const previous = runs[runs.length - 1]
|
||||
if (previous !== undefined && previous.start + previous.length === start && previous.character === segment.text.charAt(0)) {
|
||||
previous.delimiters.push({ closes, offset: start - previous.start, pair, width })
|
||||
previous.length += width
|
||||
continue
|
||||
}
|
||||
groups.push({ character: segment.text.charAt(0), delimiters: [delimiter], end, start })
|
||||
runs.push({
|
||||
canClose: false,
|
||||
canOpen: false,
|
||||
character: segment.text.charAt(0),
|
||||
delimiters: [{ closes, offset: 0, pair, width }],
|
||||
length: width,
|
||||
start,
|
||||
})
|
||||
}
|
||||
return { nodes, runs: groups.map((group) => ({ ...group, ...delimiterFlags(group.character, charAt(output, group.start - 1), output.charAt(group.end)) })) }
|
||||
}
|
||||
|
||||
// spec/flavour.md, Canonical form: CommonMark's own can-open and can-close, which `~` follows too.
|
||||
function delimiterFlags(character: string, before: string, after: string): { canClose: boolean; canOpen: boolean } {
|
||||
const left = isLeftFlanking(before, after)
|
||||
const right = isRightFlanking(before, after)
|
||||
if (character !== '_') return { canClose: right, canOpen: left }
|
||||
return { canClose: right && (!left || isPunctuation(after)), canOpen: left && (!right || isPunctuation(before)) }
|
||||
for (const run of runs) {
|
||||
const flags = delimiterFlags(run.character, charAt(output, run.start - 1), output.charAt(run.start + run.length))
|
||||
run.canClose = flags.canClose
|
||||
run.canOpen = flags.canOpen
|
||||
}
|
||||
return { nodes, runs }
|
||||
}
|
||||
|
||||
function mergesWithSyntax(scan: string, escapings: readonly (InlineEscaping | undefined)[], index: number): boolean {
|
||||
@@ -228,29 +257,8 @@ function runLength(scan: string, index: number): number {
|
||||
return length
|
||||
}
|
||||
|
||||
function isLeftFlanking(before: string, after: string): boolean {
|
||||
if (isWhitespace(after)) return false
|
||||
if (!isPunctuation(after)) return true
|
||||
return isWhitespace(before) || isPunctuation(before)
|
||||
}
|
||||
|
||||
function isRightFlanking(before: string, after: string): boolean {
|
||||
if (isWhitespace(before)) return false
|
||||
if (!isPunctuation(before)) return true
|
||||
return isWhitespace(after) || isPunctuation(after)
|
||||
}
|
||||
|
||||
function isPunctuation(character: string): boolean {
|
||||
return character !== '' && unicodePunctuation.test(character)
|
||||
}
|
||||
|
||||
function isWhitespace(character: string): boolean {
|
||||
return character === '' || isUnicodeWhitespace(character)
|
||||
}
|
||||
|
||||
function isWordCharacter(character: string): boolean {
|
||||
return character !== '' && !isUnicodeWhitespace(character) && !unicodePunctuation.test(character)
|
||||
}
|
||||
|
||||
function charAt(text: string, index: number): string {
|
||||
return index < 0 ? '' : text.charAt(index)
|
||||
|
||||
Reference in New Issue
Block a user