diff --git a/src/filters/html.ts b/src/filters/html.ts index bb603b1dc..90a83f096 100644 --- a/src/filters/html.ts +++ b/src/filters/html.ts @@ -42,36 +42,35 @@ export function newline_to_br (this: FilterImpl, v: string) { return str.replace(/\r?\n/gm, '
\n') } +// Linear-time replacement for the previous backtracking regex +// /||<[\s\S]*?>|/g +// which is O(n^2) in V8 on inputs with many unclosed openers. JS regex has no atomic +// groups / possessive quantifiers, so unrolled-loop patterns don't help asymptotically. +// We scan forward with indexOf and cache the next known closer per kind so unclosed +// openers don't re-scan the tail. Each character is visited O(1) times. +const STRIP_BLOCKS = [[''], ['']] as const + export function strip_html (this: FilterImpl, v: string) { const str = stringify(v) this.context.memoryLimit.use(str.length) - // Single-pass linear strip. The previous regex - // /||<[\s\S]*?>|/g - // backtracks O(n^2) on inputs with many unclosed openers (e.g. `' i) out += str.slice(i, lt) + if (lt < 0) return out + str.slice(i) + out += str.slice(i, lt) let end = -1 - if (str.startsWith('', lt + 7) - if (scriptEnd >= 0) end = scriptEnd + 9 - } else if (str.startsWith('', lt + 6) - if (styleEnd >= 0) end = styleEnd + 8 - } - if (end < 0) { - const gt = str.indexOf('>', lt + 1) - if (gt < 0) { out += str.slice(lt); break } - end = gt + 1 + for (let k = 0; k < STRIP_BLOCKS.length; k++) { + const [opener, closer] = STRIP_BLOCKS[k] + if (!str.startsWith(opener, lt)) continue + const from = lt + opener.length + if (closers[k] !== -1 && closers[k] < from) closers[k] = str.indexOf(closer, from) + if (closers[k] >= 0) end = closers[k] + closer.length + break } + if (end < 0) end = str.indexOf('>', lt + 1) + 1 + if (end <= 0) return out + str.slice(lt) i = end } return out