From 8569ce962a072cfd3943519f81645d6e51d8a03f Mon Sep 17 00:00:00 2001 From: Alex Southwell Date: Mon, 12 Oct 2026 11:38:17 +1100 Subject: [PATCH] perf(web): avoid quadratic markdown link scans (#18387) Co-authored-by: SAPHID Co-authored-by: GPT-6.1 Sol --- packages/shared/src/markdownLinks.test.ts | 19 ++++++ packages/shared/src/markdownLinks.ts | 71 +++++++++++++++++++++-- 2 files changed, 86 insertions(+), 4 deletions(-) diff --git a/packages/shared/src/markdownLinks.test.ts b/packages/shared/src/markdownLinks.test.ts index b16b01008f96e4dacba546c8fd2206a5ebf2919f..94d8945b3fb50e2254278959fc6d36aa9281493d 100644 --- a/packages/shared/src/markdownLinks.test.ts +++ b/packages/shared/src/markdownLinks.test.ts @@ -222,6 +222,25 @@ describe("isWindowsDrivePathHref", () => { }); describe("extractMarkdownLinkHrefs", () => { + it.each([ + ["[".repeat(32_000), []], + ["[](a".repeat(16_000), []], + ["[]( invalid [b](ok)", [")", ["foo"]], + ["[a]( invalid [c](ok)", ["two", "ok"]], + ["```text\n" + "[unfinished item\n".repeat(8_000) + "```", []], + ["[[[nested](src/main.ts) [unfinished", ["src/main.ts"]], + ["[outer [inner] invalid [valid](src/main.ts)", ["src/main.ts"]], + ["[bad](missing title [valid](src/main.ts)", ["src/main.ts"]], + ["```text\n[link](src/main.ts)\n```", ["src/main.ts"]], + ["[empty]() [valid](src/main.ts 'title')", ["src/main.ts"]], + ['[angle]( "title")', ["src/My File.ts"]], + ])("extracts links from incomplete and nested text", (markdown, hrefs) => { + expect(extractMarkdownLinkHrefs(markdown)).toEqual(hrefs); + }); + it("extracts angle-bracketed paths containing spaces", () => { expect( extractMarkdownLinkHrefs( diff --git a/packages/shared/src/markdownLinks.ts b/packages/shared/src/markdownLinks.ts index 4055ccb089673b7afe6c3e2ec8baa5fb992b945d..bb518d1f442099bc028aa3fb349ccf1ec55ea726 100644 --- a/packages/shared/src/markdownLinks.ts +++ b/packages/shared/src/markdownLinks.ts @@ -322,13 +322,76 @@ export function extractInlineCodeSpans(text: string): string[] { return spans; } -const MARKDOWN_LINK_HREF_PATTERN = - /\[[^\]]*]\(\s*(?:<([^>\n]+)>|([^\s)]+))(?:\s+["'][^"']*["'])?\s*\)/g; +/** Reuses delimiter searches when successive candidates fall in the same run. */ +function markdownRunEndFinder(markdown: string, delimiter: RegExp) { + let runStart = -1; + let runEnd = -1; + return (start: number) => { + if (start >= runStart && start <= runEnd) return runEnd; + delimiter.lastIndex = start; + runStart = start; + runEnd = delimiter.exec(markdown)?.index ?? markdown.length; + return runEnd; + }; +} + +function markdownLinkTailScanner(markdown: string) { + const whitespaceEnd = markdownRunEndFinder(markdown, /\S/g); + const titleEnd = markdownRunEndFinder(markdown, /["']/g); + const titleWhitespaceEnd = markdownRunEndFinder(markdown, /\S/g); + return (start: number) => { + const end = whitespaceEnd(start); + if (markdown[end] === ")") return end + 1; + if (end === start || (markdown[end] !== '"' && markdown[end] !== "'")) return null; + const quoteEnd = titleEnd(end + 1); + if (quoteEnd === markdown.length) return null; + const close = titleWhitespaceEnd(quoteEnd + 1); + return markdown[close] === ")" ? close + 1 : null; + }; +} + +function markdownLinkDestinationScanner(markdown: string) { + const whitespaceEnd = markdownRunEndFinder(markdown, /\S/g); + const angleEnd = markdownRunEndFinder(markdown, /[>\n]/g); + const bareEnd = markdownRunEndFinder(markdown, /[\s)]/g); + // Separate caches keep each branch's searches moving forward even when an + // angle destination fails and falls back to an ordinary destination. + const angleTail = markdownLinkTailScanner(markdown); + const bareTail = markdownLinkTailScanner(markdown); + return (start: number) => { + if (markdown[start] !== "(") return null; + const hrefStart = whitespaceEnd(start + 1); + if (markdown[hrefStart] === "<") { + const hrefEnd = angleEnd(hrefStart + 1); + if (hrefEnd > hrefStart + 1 && markdown[hrefEnd] === ">") { + const end = angleTail(hrefEnd + 1); + if (end !== null) return { hrefStart: hrefStart + 1, hrefEnd, end }; + } + } + const hrefEnd = bareEnd(hrefStart); + if (hrefEnd === hrefStart) return null; + const end = bareTail(hrefEnd); + return end === null ? null : { hrefStart, hrefEnd, end }; + }; +} export function extractMarkdownLinkHrefs(markdown: string): string[] { const hrefs: string[] = []; - for (const match of markdown.matchAll(MARKDOWN_LINK_HREF_PATTERN)) { - const href = (match[1] ?? match[2])?.trim(); + const scanDestination = markdownLinkDestinationScanner(markdown); + let cursor = 0; + while (cursor < markdown.length) { + const labelStart = markdown.indexOf("[", cursor); + if (labelStart === -1) break; + const labelEnd = markdown.indexOf("]", labelStart + 1); + if (labelEnd === -1) break; + + // Nested opening brackets share this closing bracket. On failure, skip the + // whole label instead of rescanning its suffix from each opening bracket. + cursor = labelEnd + 1; + const destination = scanDestination(cursor); + if (!destination) continue; + cursor = destination.end; + const href = markdown.slice(destination.hrefStart, destination.hrefEnd).trim(); if (href) hrefs.push(href); } return hrefs; -- 2.54.0