Skip to content

Instantly share code, notes, and snippets.

@rf5860
Last active August 1, 2026 10:27
Show Gist options
  • Select an option

  • Save rf5860/7608533084ea972983515d979c67d786 to your computer and use it in GitHub Desktop.

Select an option

Save rf5860/7608533084ea972983515d979c67d786 to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name Regex Find in Page (Ctrl+/)
// @namespace regex-find
// @version 1.1
// @description Find text on the page using regular expressions. Ctrl+/ to open, Enter/Shift+Enter to navigate, Esc to close.
// @author rjf89
// @updateURL https://gist.github.com/rf5860/7608533084ea972983515d979c67d786/raw/RegexpFind.user.js
// @downloadURL https://gist.github.com/rf5860/7608533084ea972983515d979c67d786/raw/RegexpFind.user.js
// @match *://*/*
// @grant none
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
const HL_CLASS = 'gm-rgx-hl';
const CUR_CLASS = 'gm-rgx-hl-current';
const MAX_MATCHES = 5000;
let ui = null, input, flagsInput, countEl;
let marks = [];
let current = -1;
let debounceTimer = null;
/* ---------- styles ---------- */
const style = document.createElement('style');
style.textContent = `
#gm-rgx-bar {
position: fixed; top: 10px; right: 10px; z-index: 2147483647;
background: #2b2b2b; color: #eee; padding: 8px 10px;
border-radius: 6px; box-shadow: 0 2px 10px rgba(0,0,0,.5);
font: 13px/1.4 sans-serif; display: flex; align-items: center; gap: 6px;
}
#gm-rgx-bar input {
background: #1c1c1c; color: #eee; border: 1px solid #555;
border-radius: 3px; padding: 4px 6px; font: 13px monospace;
}
#gm-rgx-pattern { width: 260px; }
#gm-rgx-flags { width: 40px; }
#gm-rgx-bar input.gm-rgx-error { border-color: #e05555; outline: 1px solid #e05555; }
#gm-rgx-bar button {
background: #444; color: #eee; border: 1px solid #666;
border-radius: 3px; padding: 3px 8px; cursor: pointer;
}
#gm-rgx-bar button:hover { background: #555; }
#gm-rgx-count { min-width: 70px; text-align: center; color: #bbb; }
.${HL_CLASS} { background: #ffe000; color: #000; }
.${CUR_CLASS} { background: #ff8800; color: #000; }
`;
document.documentElement.appendChild(style);
/* ---------- UI ---------- */
function buildUI() {
ui = document.createElement('div');
ui.id = 'gm-rgx-bar';
ui.innerHTML = `
<input id="gm-rgx-pattern" type="text" placeholder="Regular expression…" spellcheck="false">
<input id="gm-rgx-flags" type="text" value="gi" title="Regex flags" spellcheck="false">
<span id="gm-rgx-count">0 / 0</span>
<button id="gm-rgx-prev" title="Previous (Shift+Enter)">▲</button>
<button id="gm-rgx-next" title="Next (Enter)">▼</button>
<button id="gm-rgx-close" title="Close (Esc)">✕</button>
`;
document.body.appendChild(ui);
input = ui.querySelector('#gm-rgx-pattern');
flagsInput = ui.querySelector('#gm-rgx-flags');
countEl = ui.querySelector('#gm-rgx-count');
input.addEventListener('input', scheduleSearch);
flagsInput.addEventListener('input', scheduleSearch);
ui.querySelector('#gm-rgx-next').addEventListener('click', () => move(1));
ui.querySelector('#gm-rgx-prev').addEventListener('click', () => move(-1));
ui.querySelector('#gm-rgx-close').addEventListener('click', closeUI);
ui.addEventListener('keydown', e => {
e.stopPropagation();
if (e.key === 'Enter') { e.preventDefault(); move(e.shiftKey ? -1 : 1); }
if (e.key === 'Escape') { e.preventDefault(); closeUI(); }
});
}
function openUI() {
if (!ui) buildUI();
ui.style.display = 'flex';
input.focus();
input.select();
}
function closeUI() {
if (ui) ui.style.display = 'none';
clearHighlights();
updateCount();
}
/* ---------- searching ---------- */
function scheduleSearch() {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(runSearch, 250);
}
function runSearch() {
clearHighlights();
input.classList.remove('gm-rgx-error');
const pattern = input.value;
if (!pattern) { updateCount(); return; }
let flags = flagsInput.value.replace(/[^dimsy]/g, ''); // strip u/v; we manage those
let regex = null;
// Prefer Unicode mode so quantifiers work on astral chars (emoji, etc.),
// but fall back to non-Unicode mode for patterns that are only valid there.
for (const extra of ['u', '']) {
try {
regex = new RegExp(pattern, 'g' + flags + extra);
break;
} catch (e) { /* try next */ }
}
if (!regex) {
input.classList.add('gm-rgx-error');
countEl.textContent = 'bad regex';
return;
}
// Collect text nodes first (highlighting mutates the DOM)
const walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_TEXT,
{
acceptNode(node) {
const p = node.parentElement;
if (!p) return NodeFilter.FILTER_REJECT;
const tag = p.tagName;
if (tag === 'SCRIPT' || tag === 'STYLE' || tag === 'NOSCRIPT' ||
tag === 'TEXTAREA' || p.closest('#gm-rgx-bar'))
return NodeFilter.FILTER_REJECT;
if (!/\S/.test(node.data)) return NodeFilter.FILTER_REJECT;
return NodeFilter.FILTER_ACCEPT;
}
}
);
const textNodes = [];
while (walker.nextNode()) textNodes.push(walker.currentNode);
outer:
for (const node of textNodes) {
const text = node.data;
regex.lastIndex = 0;
let m, last = 0, frag = null;
while ((m = regex.exec(text)) !== null) {
if (m[0].length === 0) {
// Skip a full code point, not just a code unit,
// so we don't split surrogate pairs
const cp = text.codePointAt(regex.lastIndex);
regex.lastIndex += cp > 0xFFFF ? 2 : 1;
continue;
}
if (!frag) frag = document.createDocumentFragment();
if (m.index > last)
frag.appendChild(document.createTextNode(text.slice(last, m.index)));
const mark = document.createElement('mark');
mark.className = HL_CLASS;
mark.textContent = m[0];
frag.appendChild(mark);
marks.push(mark);
last = m.index + m[0].length;
if (marks.length >= MAX_MATCHES) break;
}
if (frag) {
if (last < text.length)
frag.appendChild(document.createTextNode(text.slice(last)));
node.parentNode.replaceChild(frag, node);
}
if (marks.length >= MAX_MATCHES) break outer;
}
current = -1;
if (marks.length) move(1);
else updateCount();
}
function clearHighlights() {
for (const mark of marks) {
const parent = mark.parentNode;
if (!parent) continue;
parent.replaceChild(document.createTextNode(mark.textContent), mark);
parent.normalize();
}
marks = [];
current = -1;
}
/* ---------- navigation ---------- */
function move(dir) {
if (!marks.length) return;
if (current >= 0) marks[current].classList.remove(CUR_CLASS);
current = (current + dir + marks.length) % marks.length;
const mark = marks[current];
mark.classList.add(CUR_CLASS);
mark.scrollIntoView({ block: 'center', behavior: 'smooth' });
updateCount();
}
function updateCount() {
if (!countEl) return;
countEl.textContent = marks.length
? `${current + 1} / ${marks.length}${marks.length >= MAX_MATCHES ? '+' : ''}`
: '0 / 0';
}
/* ---------- hotkey ---------- */
document.addEventListener('keydown', e => {
if (e.ctrlKey && !e.altKey && !e.metaKey && e.key === '/') {
e.preventDefault();
e.stopPropagation();
openUI();
}
}, true);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment