bit-twiddler 0.2.0

Cross-platform developer toolbox: bit manipulation, hashing, YAML/JSON/SQL, QR, Markdown, cron, and 40+ more tools — Tauri v2, no Node.js
  // REGEX TESTER
  // ============================================================
  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;
    }

    // Collect all matches using a global copy to allow looping
    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++; // avoid infinite loop on zero-width matches
    }
    lastMatches = matches;
    buildCaptures(matches);

    // Build highlighted HTML
    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');

    // Match detail list
    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');
  });

  // ============================================================