import { isUnicodeWhitespace } from './commonmark-grammar.ts' type DelimiterRun = { canClose: boolean; canOpen: boolean; character: string; length: number } export type EmphasisPairing = { closer: Run; closerOffset: number; opener: Run; openerOffset: number; used: number } type Candidate = { head: number next: Candidate | undefined original: number previous: Candidate | undefined remaining: number run: Run tail: number } const unicodePunctuation = /[\p{P}\p{S}]/u export 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)) } } export function isWordCharacter(character: string): boolean { return character !== '' && !isWhitespace(character) && !isPunctuation(character) } export function matchEmphasis(runs: readonly Run[]): EmphasisPairing[] { const pairings: EmphasisPairing[] = [] const bottoms = new Map | undefined>() let closer = candidates(runs) while (closer !== undefined) { if (!closer.run.canClose) { closer = closer.next continue } const key = `${closer.run.character}${closer.run.canOpen}${closer.original % 3}` const bottom = bottoms.get(key) let opener = closer.previous while (opener !== undefined && opener !== bottom && !pairs(opener, closer)) opener = opener.previous if (opener === undefined || opener === bottom) { bottoms.set(key, closer.previous) const following = closer.next if (!closer.run.canOpen) unlink(closer) closer = following continue } const used = closer.remaining >= 2 && opener.remaining >= 2 ? 2 : 1 opener.remaining -= used opener.tail -= used pairings.push({ closer: closer.run, closerOffset: closer.head, opener: opener.run, openerOffset: opener.tail, used }) closer.head += used closer.remaining -= used opener.next = closer closer.previous = opener if (opener.remaining === 0) unlink(opener) if (closer.remaining > 0) continue const following = closer.next unlink(closer) closer = following } return pairings } export function runLength(text: string, index: number): number { const character = text.charAt(index) let length = 0 while (text.charAt(index + length) === character) length += 1 return length } function candidates(runs: readonly Run[]): Candidate | undefined { let first: Candidate | undefined let previous: Candidate | undefined for (const run of runs) { const candidate: Candidate = { head: 0, next: undefined, original: run.length, previous, remaining: run.length, run, tail: run.length, } if (previous === undefined) first = candidate else previous.next = candidate previous = candidate } return first } function pairs(opener: Candidate, closer: Candidate): boolean { if (!opener.run.canOpen || opener.run.character !== closer.run.character) return false const odd = (closer.run.canOpen || opener.run.canClose) && closer.original % 3 !== 0 && (opener.original + closer.original) % 3 === 0 return !odd } function unlink(candidate: Candidate): void { if (candidate.previous !== undefined) candidate.previous.next = candidate.next if (candidate.next !== undefined) candidate.next.previous = candidate.previous } 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) }