Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions bench/emphasis-runs.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,11 @@ for (const opener of ["*a ", "_a "]) {
assert.ok(took < BUDGET_MS, `${JSON.stringify(opener)}×10000 took ${took} ms`);
});
}

test(`"["×25000 parses inside ${BUDGET_MS} ms`, () => {
const source = "[".repeat(25000);
const started = performance.now();
E.parse(source);
const took = Math.round(performance.now() - started);
assert.ok(took < BUDGET_MS, `"["×25000 took ${took} ms`);
});
35 changes: 33 additions & 2 deletions markdown/parserInlineMatchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,19 @@ function skipCodeSpan(text: string, cursor: number): number {
return close === -1 ? cursor + ticks : close + ticks;
}

const MAX_CACHED_EMPHASIS = 4;
const failedOpenersCache = new Map<string, { star: Set<number>; underscore: Set<number> }>();

function getFailedOpeners(text: string): { star: Set<number>; underscore: Set<number> } {
let entry = failedOpenersCache.get(text);
if (!entry) {
if (failedOpenersCache.size >= MAX_CACHED_EMPHASIS) failedOpenersCache.clear();
entry = { star: new Set(), underscore: new Set() };
failedOpenersCache.set(text, entry);
}
return entry;
}

function matchStackedEmphasis(
text: string,
pos: number,
Expand All @@ -526,6 +539,13 @@ function matchStackedEmphasis(
if (isWhitespaceCode(text.charCodeAt(pos + openLen))) return null; // opener can't precede space
if (!isStar && pos > 0 && isWordCode(text.charCodeAt(pos - 1))) return null; // no intraword _

const failedSet = isStar ? getFailedOpeners(text).star : getFailedOpeners(text).underscore;
if (failedSet.has(pos)) return null;
if (text.indexOf(marker, pos + openLen) === -1) {
failedSet.add(pos);
return null;
}

// Zero-alloc fast path for the dominant single-close case; `extraSegments`
// only materializes when a run closes in parts ("***a** b*").
let firstContent: string | null = null;
Expand All @@ -536,6 +556,7 @@ function matchStackedEmphasis(
let cursor = segStart;
let matchEnd = -1;
let depth = 0;
const openersSeen: number[] = [];
// The next "\\" and "`" at or after `cursor`, rescanned only once `cursor` passes
// them (`cursor` never moves back). Rescanning at every candidate made a run of
// unclosed openers cubic: "*a "×10000 took 45831 ms, 754 ms cached (node 22).
Expand Down Expand Up @@ -572,6 +593,7 @@ function matchStackedEmphasis(

if (canClose && cursor > segStart && depth > 0) {
depth -= 1; // pairs with an inner opener; stays in the content
openersSeen.pop();
cursor += runLen;
continue;
}
Expand All @@ -590,11 +612,20 @@ function matchStackedEmphasis(
segStart = cursor;
continue;
}
if (canOpen) depth += 1;
if (canOpen) {
depth += 1;
openersSeen.push(cursor);
}
cursor += runLen;
}

if (firstContent === null || firstContent.length === 0) return null;
if (firstContent === null || firstContent.length === 0) {
failedSet.add(pos);
for (let i = 0; i < openersSeen.length; i++) {
failedSet.add(openersSeen[i]);
}
return null;
}

let node = wrapEmphasis(marker, firstTake, parseInline(firstContent));
let consumed = firstTake;
Expand Down
28 changes: 21 additions & 7 deletions markdown/parserInlineUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,30 @@ export function appendChar(nodes: InlineNode[], ch: string): void {
}
}

const MAX_CACHED_BRACKETS = 4;
const bracketCache = new Map<string, Map<number, number>>();

export function findClosingBracket(text: string, openPos: number): number {
let depth = 0;
for (let i = openPos; i < text.length; i++) {
if (text[i] === '[') depth++;
else if (text[i] === ']') {
depth--;
if (depth === 0) return i;
if (text.indexOf(']', openPos) === -1) return -1;
let map = bracketCache.get(text);
if (!map) {
if (bracketCache.size >= MAX_CACHED_BRACKETS) bracketCache.clear();
map = new Map();
const stack: number[] = [];
const len = text.length;
for (let i = 0; i < len; i++) {
const code = text.charCodeAt(i);
if (code === 91) {
stack.push(i);
} else if (code === 93) {
if (stack.length > 0) {
map.set(stack.pop()!, i);
}
}
}
bracketCache.set(text, map);
}
return -1;
return map.get(openPos) ?? -1;
}

export function slugify(text: string): string {
Expand Down
Loading