Bonus
Regex tester
Pattern, flags and highlighted matches
Pattern, flags and highlighted matches
Build a regular expression tester with Tailwind CSS. Use a max-w-2xl card with a heading and a muted line. The pattern row is a bordered field with monospaced slashes rendered as muted addons on either side of the input, and a small flags input after the closing slash. Under it, a textarea of test text pre-filled with a few lines. Below, a result panel showing the subject text with every match wrapped in a highlighted span, then a match list where each entry shows an index, the matched text in monospace and any capture groups as small pills. A status line reports the match count or a red parse error. Then add JavaScript that compiles the pattern on every keystroke inside a try-catch, escapes the subject before injecting highlights, always forces the global flag for the highlight pass, guards against zero-length matches looping forever, and lists up to twenty matches. <section class="mx-auto max-w-2xl px-6 py-14">
<div class="rounded-2xl border border-neutral-200 bg-white p-7">
<h1 class="text-xl font-semibold tracking-tight text-neutral-900">Regex tester</h1>
<p class="mt-1.5 text-sm text-neutral-500">Everything runs in your browser. Nothing is sent anywhere.</p>
<div class="mt-6 flex items-center gap-2 rounded-xl border border-neutral-200 px-3 transition focus-within:border-neutral-900 focus-within:ring-4 focus-within:ring-neutral-900/10">
<span class="font-mono text-sm text-neutral-400">/</span>
<input id="rxPattern" value="(\w+)@(\w+\.\w+)" spellcheck="false"
class="min-w-0 flex-1 py-2.5 font-mono text-sm outline-none" />
<span class="font-mono text-sm text-neutral-400">/</span>
<input id="rxFlags" value="gi" spellcheck="false"
class="w-14 py-2.5 font-mono text-sm outline-none" />
</div>
<textarea id="rxSubject" rows="5" spellcheck="false"
class="mt-3 w-full resize-none rounded-xl border border-neutral-200 px-3.5 py-2.5 font-mono text-sm outline-none transition focus:border-neutral-900">Write to [email protected] or [email protected].
Billing goes to [email protected] instead.
Not an address: @nothing, plain text, 42.</textarea>
<p id="rxStatus" class="mt-3 text-sm text-neutral-500"></p>
<div id="rxHighlight" class="mt-3 whitespace-pre-wrap rounded-xl border border-neutral-200 bg-neutral-50 p-4 font-mono text-sm leading-relaxed text-neutral-700"></div>
<div id="rxMatches" class="mt-4 space-y-2"></div>
</div>
</section> var pattern = document.getElementById("rxPattern");
var flags = document.getElementById("rxFlags");
var subject = document.getElementById("rxSubject");
var status = document.getElementById("rxStatus");
var highlight = document.getElementById("rxHighlight");
var matches = document.getElementById("rxMatches");
function escapeHtml(text) {
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
}
function run() {
var source = subject.value;
var re;
try {
// The highlight pass always needs /g, whatever the person typed.
var used = flags.value.indexOf("g") === -1 ? flags.value + "g" : flags.value;
re = new RegExp(pattern.value, used);
} catch (err) {
status.textContent = err.message;
status.className = "mt-3 text-sm text-red-600";
highlight.textContent = source;
matches.innerHTML = "";
return;
}
var found = [];
var out = "";
var last = 0;
var m;
while ((m = re.exec(source)) !== null) {
found.push(m);
out += escapeHtml(source.slice(last, m.index));
out += '<mark class="rounded bg-amber-200 px-0.5 text-neutral-900">' + escapeHtml(m[0]) + "</mark>";
last = m.index + m[0].length;
// A zero-length match would spin here forever otherwise.
if (m[0] === "") re.lastIndex += 1;
if (found.length > 500) break;
}
out += escapeHtml(source.slice(last));
highlight.innerHTML = out;
status.textContent = found.length === 0 ? "No matches" : found.length + (found.length === 1 ? " match" : " matches");
status.className = "mt-3 text-sm " + (found.length === 0 ? "text-neutral-500" : "text-green-700");
matches.innerHTML = found
.slice(0, 20)
.map(function (item, i) {
var groups = item
.slice(1)
.map(function (g, n) {
return '<span class="rounded bg-neutral-100 px-1.5 py-0.5 font-mono text-[11px] text-neutral-600">$' + (n + 1) + " " + escapeHtml(g === undefined ? "—" : g) + "</span>";
})
.join(" ");
return (
'<div class="rounded-xl border border-neutral-200 px-4 py-3">' +
'<div class="flex items-center gap-3">' +
'<span class="font-mono text-xs text-neutral-400">' + i + "</span>" +
'<span class="min-w-0 flex-1 truncate font-mono text-sm text-neutral-900">' + escapeHtml(item[0]) + "</span>" +
'<span class="shrink-0 font-mono text-xs text-neutral-400">at ' + item.index + "</span>" +
"</div>" +
(groups ? '<div class="mt-2 flex flex-wrap gap-1.5">' + groups + "</div>" : "") +
"</div>"
);
})
.join("");
}
[pattern, flags, subject].forEach(function (el) {
el.addEventListener("input", run);
});
run();