let lastMatches = [];
const buildCaptures = (matches) => {
const $wrap = $('#regex-captures-wrap');
const $head = $('#regex-captures-head');
const $body = $('#regex-captures-body');
if (!matches.length) {
$wrap.addClass('hidden');
$head.html('');
$body.html('');
return;
}
const hasNamed = !!(matches[0].named && Object.keys(matches[0].named).length);
const groupCount = matches.reduce((max, m) => Math.max(max, m.groups.length), 0);
if (!hasNamed && groupCount === 0) {
$wrap.addClass('hidden');
$head.html('');
$body.html('');
return;
}
const columns = hasNamed
? Object.keys(matches[0].named)
: Array.from({ length: groupCount }, (_, i) => `Group ${i + 1}`);
$head.html(`<tr>
<th class="px-3 py-2">#</th>
<th class="px-3 py-2">Full match</th>
${columns.map(c => `<th class="px-3 py-2">${escHtml(c)}</th>`).join('')}
</tr>`);
$body.html(matches.map((m, i) => {
const cellVals = hasNamed ? columns.map(c => m.named[c]) : m.groups;
const cells = cellVals.map(v =>
`<td class="px-3 py-2 text-gray-300">${v === undefined ? '<span class="text-gray-600 italic">—</span>' : escHtml(String(v))}</td>`
).join('');
return `<tr class="hover:bg-gray-800/40">
<td class="px-3 py-2 text-violet-400 font-bold">${i + 1}</td>
<td class="px-3 py-2 text-violet-300 truncate max-w-[200px]">${escHtml(m.value || '(empty)')}</td>
${cells}
</tr>`;
}).join(''));
$wrap.removeClass('hidden');
};
const runReplace = () => {
const pattern = $('#regex-pattern').val();
const replacement = $('#regex-replacement').val();
const testStr = $('#regex-input').val();
const $out = $('#regex-replace-output');
if (!pattern) { $out.val(testStr); return; }
let flags = '';
if ($('#rflag-g').is(':checked')) flags += 'g';
if ($('#rflag-i').is(':checked')) flags += 'i';
if ($('#rflag-m').is(':checked')) flags += 'm';
if ($('#rflag-s').is(':checked')) flags += 's';
try {
const re = new RegExp(pattern, flags);
$out.val(testStr.replace(re, replacement));
} catch (e) {
$out.val('Error: ' + e.message);
}
};
const runRegex = () => {
const pattern = $('#regex-pattern').val();
const testStr = $('#regex-input').val();
const $hl = $('#regex-highlight');
const $count = $('#regex-match-count');
const $status = $('#regex-status');
const $list = $('#regex-match-list');
if (!pattern) {
$hl.text(testStr);
$count.text('');
$status.html('');
$list.html('');
lastMatches = [];
buildCaptures([]);
return;
}
let flags = '';
if ($('#rflag-g').is(':checked')) flags += 'g';
if ($('#rflag-i').is(':checked')) flags += 'i';
if ($('#rflag-m').is(':checked')) flags += 'm';
if ($('#rflag-s').is(':checked')) flags += 's';
let baseRegex;
try {
baseRegex = new RegExp(pattern, flags);
$status.html('<span class="text-green-400 font-mono">✓ valid</span>');
} catch (e) {
$status.html(`<span class="text-red-400">${escHtml(e.message)}</span>`);
$hl.text(testStr || '');
$count.text('');
$list.html('');
lastMatches = [];
buildCaptures([]);
return;
}
const globalRegex = new RegExp(baseRegex.source, flags.includes('g') ? flags : flags + 'g');
globalRegex.lastIndex = 0;
const matches = [];
let m;
while ((m = globalRegex.exec(testStr)) !== null) {
matches.push({ index: m.index, value: m[0], groups: Array.from(m).slice(1), named: m.groups || null });
if (m[0].length === 0) globalRegex.lastIndex++; }
lastMatches = matches;
buildCaptures(matches);
let html = '', last = 0;
matches.forEach((match, i) => {
html += escHtml(testStr.slice(last, match.index));
html += `<mark style="background:rgba(139,92,246,0.30);color:#c4b5fd;border-radius:2px;padding:0 1px" title="Match ${i + 1}">${escHtml(match.value)}</mark>`;
last = match.index + match.value.length;
});
html += escHtml(testStr.slice(last));
$hl.html(html || '<span class="text-gray-600 italic text-xs">Start typing a pattern...</span>');
const n = matches.length;
$count.text(n ? `${n} match${n !== 1 ? 'es' : ''}` : 'no matches');
if (n === 0) {
$list.html('<div class="text-xs text-gray-600 italic px-2 py-1">No matches found.</div>');
} else {
$list.html(matches.map((match, i) => {
const groups = match.groups.filter(g => g !== undefined)
.map((g, gi) => `<span class="text-blue-400 ml-2">$${gi + 1}:<span class="text-gray-300">${escHtml(String(g))}</span></span>`).join('');
return `<div class="flex items-center gap-2 px-2 py-1 bg-gray-800/50 rounded-lg text-xs font-mono">
<span class="text-violet-500 font-bold w-5 text-right flex-shrink-0">${i + 1}</span>
<span class="text-gray-600">@${match.index}</span>
<span class="text-violet-300 truncate max-w-xs">${escHtml(match.value || '(empty)')}</span>
${groups}
</div>`;
}).join(''));
}
};
const runAll = () => { runRegex(); runReplace(); };
$('#regex-pattern, #regex-input').on('input', runAll);
$('#rflag-g, #rflag-i, #rflag-m, #rflag-s').on('change', runAll);
$('#regex-replacement').on('input', runReplace);
$('#regex-copy-btn').on('click', function() {
if (lastMatches.length) window.copyToClipboard(lastMatches.map(m => m.value).join('\n'), $(this));
});
$('#regex-replace-copy-btn').on('click', function() {
const text = $('#regex-replace-output').val();
if (text) window.copyToClipboard(text, $(this));
});
$('.regex-mode-btn').on('click', function() {
const mode = $(this).data('mode');
$('.regex-mode-btn').removeClass('active text-violet-400 border-violet-500').addClass('text-gray-500 border-transparent hover:text-gray-300');
$(this).addClass('active text-violet-400 border-violet-500').removeClass('text-gray-500 border-transparent hover:text-gray-300');
$('.regex-mode-view').addClass('hidden');
$('#regex-mode-' + mode).removeClass('hidden');
});