Regex Lab: hits, groups, replacement preview, and the zero-width trap
A regex tester has to do three things well: show where matches land and what each group captured, preview the replacement, and never hang on a zero-width match.
Why build one
There are plenty of online regex testers, but they usually want your text on someone else’s server, and they rarely explain why something is highlighted. I wanted a local page that lays the matching details out.
Three things the result must state
First, the span: start and end offsets for every match. Without offsets you cannot draw the highlight in the right place.
Second, the groups: not just the overall match but each capture group with its index. Groups that did not participate must read undefined, not an empty string, because the two behave completely differently downstream.
Third, the replacement preview: the full resulting text, not just a coloured match. The most common regex replacement bug is a wrong group reference, and only the output reveals it.
Zero-width matches must not hang
This is the only real trap in the implementation. When matching globally with the g flag, if a match has zero length — anchors, or a pure lookahead — lastIndex never advances and the loop never ends. The page freezes.
So every match has to push the cursor forward explicitly:
const step = match[0].length === 0 ? 1 : 0;
re.lastIndex = match.index + match[0].length + step;
There is also a cap on the number of matches, so catastrophic backtracking cannot take the browser down. A bad pattern should not equal a dead page.
Highlighting by segments, not by string replacement
The input is cut into an array of unmatched and matched segments, and the template only renders segments. Nothing is injected into HTML, so markup characters in the user’s text cannot break the output.
Templates and local execution
Patterns for email, URL, IPv4 and dates ship as templates so you do not start from a blank field. All parsing happens in the browser, and the logic module is a pure function with no dependencies and no DOM, so the project’s self-test suite runs it directly — which happens to cover the zero-width and cap branches above.

Comments
…