Once `indexOf(closer, X)` returns -1, all subsequent searches (with
monotonically increasing start) also return -1. So tracking absence is
enough; storing positions is unnecessary. Make `blocks` a Set and
delete a kind once its closer is known absent — no parallel `dead`
bookkeeping. Use Jest's per-test timeout for the ReDoS regressions
instead of manual Date.now() bookkeeping.
Co-authored-by: Cursor <[email protected]>
The previous strip_html regex
/<script[\s\S]*?<\/script>|<style[\s\S]*?<\/style>|<[\s\S]*?>|<!--[\s\S]*?-->/g
contains lazy alternatives that backtrack O(n^2) on inputs with many
unclosed `<script` / `<style` openers. A 350KB payload of
`'<script'.repeat(50000)` blocked the Node.js event loop for ~10s, and
cost grew quadratically with input size. memoryLimit only charged
str.length, which does not bound regex CPU.
Replace the regex with an indexOf-based single-pass scan. For each `<`
we:
- if `<script` opener: find next `</script>` and skip the whole block;
cache "no closer after pos k" so subsequent unclosed `<script`
openers do not re-scan the tail.
- same for `<style` / `</style>`.
- otherwise treat as a generic `<...>` tag (matches the original
behavior, where the `<[\s\S]*?>` alternative also caught comments).
- if no closing `>` exists, emit the tail as literal text and stop.
Total work is O(n). All existing strip_html test cases pass unchanged.
Add regression tests covering the PoCs (`<script` / `<style` repeats,
and `<script>foo` repeats with `>` but no `</script>`) plus a
memoryLimit assertion.
Co-authored-by: Cursor <[email protected]>
renderLimit was only checked inside the per-template loop, so
renderTemplates([], ...) skipped it. Empty {% for %} and {% tablerow %}
bodies call that path once per iteration (tablerow still does emitter
work for <tr>/<td>), bypassing the documented time budget. Check the
limiter at renderTemplates entry before the loop.
Add regression tests for empty for-body and empty tablerow-body.
Co-authored-by: Cursor <[email protected]>