export interface LinkContext { mark: string; park: (html: string) => string; safeUrl: (url: string) => string; } const DOUBLE_QUOTE = """; const SINGLE_QUOTE = "'"; const SPACE = /\s/; interface ScanMaps { bracket: Int32Array; paren: Int32Array; wordEnd: Int32Array; } function skipPlaceholder(text: string, index: number, mark: string, to: number): number { const close = text.indexOf(mark, index + 1); return close === -1 || close + 1 > to ? to : close + 1; } function buildScanMaps(text: string, mark: string): ScanMaps { const n = text.length; const bracket = new Int32Array(n).fill(-1); const paren = new Int32Array(n).fill(-1); const wordEnd = new Int32Array(n + 1); const bracketStack: number[] = []; for (let i = 0; i < n; ) { const char = text[i]; if (char === mark) { i = skipPlaceholder(text, i, mark, n); continue; } if (char === "\\") { i += 2; continue; } if (char === "[") { bracketStack.push(i); i++; continue; } if (char === "]") { const open = bracketStack.pop(); if (open !== undefined) bracket[open] = i; i++; continue; } i++; } const parenStack: number[] = []; for (let i = 0; i < n; ) { const char = text[i]; if (char === "\\") { i += 2; continue; } if (char === "(") { parenStack.push(i); i++; continue; } if (char === ")") { const open = parenStack.pop(); if (open !== undefined) paren[open] = i; i++; continue; } i++; } wordEnd[n] = n; for (let i = n - 1; i >= 0; i--) { const code = text.charCodeAt(i); if (code === 92 && i + 1 < n) wordEnd[i] = wordEnd[i + 2 > n ? n : i + 2]; else if (isSpace(code, text[i])) wordEnd[i] = i; else wordEnd[i] = wordEnd[i + 1]; } return { bracket, paren, wordEnd }; } function isSpace(code: number, char: string): boolean { if (code < 128) return code === 32 || (code >= 9 && code <= 13); return SPACE.test(char); } interface Destination { url: string; title?: string; end: number; } function readTitle(text: string, index: number, to: number): { value: string; end: number } | null { for (const [open, close] of [ [DOUBLE_QUOTE, DOUBLE_QUOTE], [SINGLE_QUOTE, SINGLE_QUOTE], ["(", ")"], ]) { if (index + open.length > to || !text.startsWith(open, index)) continue; const from = index + open.length; const found = text.indexOf(close, from); if (found === -1 || found + close.length > to) return null; return { value: text.slice(from, found), end: found + close.length }; } return null; } function unescapeDestination(text: string, from: number, to: number): string { let url = ""; let i = from; while (i < to) { if (text[i] === "\\" && i + 1 < to) { url += text[i + 1]; i += 2; continue; } url += text[i]; i++; } return url; } function readDestination( text: string, parenIndex: number, maps: ScanMaps, to: number, ): Destination | null { const close = maps.paren[parenIndex]; if (close === -1 || close >= to) return null; const from = parenIndex + 1; const stop = Math.min(maps.wordEnd[from], close); const url = unescapeDestination(text, from, stop); if (stop === close) return { url, end: close + 1 }; let after = stop; while (after < to && (text[after] === " " || text[after] === "\t")) after++; if (after < to && text[after] === ")") return { url, end: after + 1 }; const title = readTitle(text, after, to); if (!title) return null; let titleClose = title.end; while (titleClose < to && (text[titleClose] === " " || text[titleClose] === "\t")) titleClose++; if (titleClose >= to || text[titleClose] !== ")") return null; return { url, title: title.value, end: titleClose + 1 }; } function attribute(name: string, value: string | undefined): string { return value === undefined ? "" : ` ${name}="${value}"`; } export function renderLinks(text: string, context: LinkContext, textOnly = false): string { if (!text.includes("[")) return text; return scanLinks(text, context, textOnly, buildScanMaps(text, context.mark), 0, text.length); } function scanLinks( text: string, context: LinkContext, textOnly: boolean, maps: ScanMaps, from: number, to: number, ): string { let out = ""; let i = from; while (i < to) { const char = text[i]; if (char === context.mark) { const end = skipPlaceholder(text, i, context.mark, to); out += text.slice(i, end); i = end; continue; } if (char === "\\" && i + 1 < to) { out += text.slice(i, i + 2); i += 2; continue; } const isImage = char === "!" && i + 1 < to && text[i + 1] === "["; if (char !== "[" && !isImage) { out += char; i++; continue; } const bracket = isImage ? i + 1 : i; const close = maps.bracket[bracket]; if (close === -1 || close >= to || close + 1 >= to || text[close + 1] !== "(") { out += char; i++; continue; } const destination = readDestination(text, close + 1, maps, to); if (!destination) { out += char; i++; continue; } const url = context.safeUrl(destination.url); if (isImage) { const alt = scanLinks(text, context, true, maps, bracket + 1, close); const tag = `${alt}`; out += textOnly ? alt : context.park(tag); } else if (textOnly) { out += scanLinks(text, context, true, maps, bracket + 1, close); } else { const open = ``; out += context.park(open) + scanLinks(text, context, false, maps, bracket + 1, close) + context.park(""); } i = destination.end; } return out; }