// ==UserScript== // @name PageGrep // @version 3.4 // @description Grep-style complementary search engine for wiki/static sites // @author Rust1667 // @match https://retrofmhy.pages.dev/* // @match https://fmhy.net/* // @match https://fluffle.cc/* // @match https://rentry.co/* // @match https://rentry.org/* // @match https://www.reddit.com/r/*/wiki/* // @match https://*.wikipedia.org/wiki/* // @match https://github.com/*/*/wiki/* // @grant none // @icon data:image/svg+xml,πŸ” // @namespace http://tampermonkey.net/ // @downloadURL https://update.greasyfork.icu/scripts/568232/PageGrep.user.js // @updateURL https://update.greasyfork.icu/scripts/568232/PageGrep.meta.js // ==/UserScript== (function () { 'use strict'; // ── CONFIG ─────────────────────────────────────────────────────────────────── const CONTENT_SELECTOR = '#mainScroll'; // adjust per site if needed const MAX_RESULTS = 10; // ───────────────────────────────────────────────────────────────────────────── // ── SHADOW DOM HOST ────────────────────────────────────────────────────────── const host = document.createElement('div'); host.id = 'pagegrep-host'; host.style.cssText = 'all:initial;position:fixed;z-index:2147483647;'; document.body.appendChild(host); const shadow = host.attachShadow({ mode: 'open' }); // ── STYLES (scoped inside shadow) ──────────────────────────────────────────── const styleEl = document.createElement('style'); styleEl.textContent = ` *, *::before, *::after { box-sizing: border-box; } /* ── FAB ── */ #wgs-fab { position: fixed; bottom: 24px; right: 24px; width: 42px; height: 42px; border-radius: 50%; background: #1a1a2e; border: 1px solid #3a3a5c; box-shadow: 0 2px 12px rgba(0,0,0,.4); color: #7c83fd; font-size: 18px; cursor: pointer; display: flex; align-items: center; justify-content: center; opacity: 0.35; transition: opacity .2s, box-shadow .2s; user-select: none; touch-action: none; } #wgs-fab:hover { opacity: 1; box-shadow: 0 4px 20px rgba(0,0,0,.55); } #wgs-fab.active { opacity: 1; } /* ── SPOTLIGHT OVERLAY ── */ #wgs-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.45); display: flex; align-items: flex-start; justify-content: center; padding-top: 18vh; opacity: 0; pointer-events: none; transition: opacity .18s; z-index: 1; } #wgs-overlay.open { opacity: 1; pointer-events: all; } #wgs-spotlight { background: var(--wgs-site-bg, #1a1a2e); border: 1px solid #3a3a5c; border-radius: 12px; box-shadow: 0 8px 40px rgba(0,0,0,.6); padding: 10px 14px; display: flex; align-items: center; gap: 8px; width: min(560px, 90vw); font-family: 'Consolas', 'Menlo', monospace; } #wgs-search-icon { color: #7c83fd; font-size: 16px; flex-shrink: 0; opacity: 0.7; } #wgs-input { background: transparent; border: none; outline: none; color: var(--wgs-site-text, #e0e0f0); font-size: 15px; font-family: inherit; width: 100%; caret-color: #7c83fd; } #wgs-input::placeholder { color: #555577; } #wgs-kbd { color: #3a3a5c; font-size: 10px; flex-shrink: 0; white-space: nowrap; } /* ── RESULTS PANEL ── */ #wgs-panel { position: fixed; top: 0; right: 0; width: 38.196601vw; height: 100vh; background: var(--wgs-site-bg, #11111e); border-left: 2px solid #3a3a5c; box-shadow: -6px 0 40px rgba(0,0,0,.6); display: flex; flex-direction: column; font-family: 'Consolas', 'Menlo', monospace; transform: translateX(100%); transition: transform .2s cubic-bezier(.4,0,.2,1); overflow: hidden; z-index: 2; } #wgs-panel.open { transform: translateX(0); } #wgs-panel-header { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; border-bottom: 1px solid #2a2a4e; background: var(--wgs-site-bg, #1a1a2e); flex-shrink: 0; } #wgs-panel-title { color: #7c83fd; font-size: var(--wgs-site-fontsize, 14px); letter-spacing: .08em; text-transform: uppercase; } #wgs-close { background: none; border: none; color: #555577; font-size: 18px; cursor: pointer; line-height: 1; padding: 2px 6px; border-radius: 4px; transition: color .15s, background .15s; } #wgs-close:hover { color: #e0e0f0; background: #2a2a4e; } #wgs-copy { background: none; border: none; color: #555577; font-size: 18px; cursor: pointer; line-height: 1; padding: 2px 6px; border-radius: 4px; transition: color .15s, background .15s; } #wgs-copy:hover { color: #e0e0f0; background: #2a2a4e; } #wgs-results { overflow-y: auto; flex: 1; padding: 8px 0; } #wgs-results::-webkit-scrollbar { width: 6px; } #wgs-results::-webkit-scrollbar-track { background: transparent; } #wgs-results::-webkit-scrollbar-thumb { background: #2a2a4e; border-radius: 3px; } .wgs-result { padding: 9px 16px; border-bottom: 1px solid #1e1e34; transition: background .1s; cursor: pointer; } .wgs-result:hover { background: color-mix(in srgb, var(--wgs-site-bg, #1e1e34) 85%, white 15%); } .wgs-breadcrumb { display: block; margin-bottom: 2px; font-size: calc(var(--wgs-site-fontsize, 14px) * 0.82); line-height: 1.5; } .wgs-breadcrumb-seg { color: #7c83fd; text-decoration: none; font-weight: 600; letter-spacing: .03em; transition: color .1s; } .wgs-breadcrumb-seg:hover { color: #a0a6ff; text-decoration: underline; } .wgs-breadcrumb-sep { color: #3a3a5c; margin: 0 4px; font-size: 10px; } .wgs-line li, .wgs-line ul, .wgs-line ol { list-style: none; margin: 0; padding: 0; } .wgs-line { color: var(--wgs-site-text, #c8c8e0); font-size: var(--wgs-site-fontsize, 14px); line-height: 1.5; word-break: break-word; } .wgs-line a { color: var(--wgs-site-link, #7ca4fd) !important; } .wgs-line a:hover { filter: brightness(1.25); } .wgs-match { background: #3d3500; color: #ffd54f; border-radius: 2px; padding: 0 1px; } .wgs-empty { color: #555577; font-size: 13px; text-align: center; padding: 40px 20px; } .wgs-rank-exact .wgs-breadcrumb-seg { color: #a0ffa0; } .wgs-rank-exact .wgs-match { background: #1a3a00; color: #a0ffa0; } `; shadow.appendChild(styleEl); // Flash styles injected into the HOST page (not shadow) so they apply to page elements let lastFlashTarget = null; // ── BUILD UI ────────────────────────────────────────────────────────────────── // FAB const fab = document.createElement('button'); fab.id = 'wgs-fab'; fab.title = 'PageGrep (Alt+G)'; fab.textContent = 'πŸ”'; shadow.appendChild(fab); // Spotlight overlay const overlay = document.createElement('div'); overlay.id = 'wgs-overlay'; overlay.innerHTML = `
πŸ” Alt+G
`; shadow.appendChild(overlay); // Results panel const panel = document.createElement('div'); panel.id = 'wgs-panel'; panel.innerHTML = `
Results
`; shadow.appendChild(panel); // ── THEME SAMPLING ──────────────────────────────────────────────────────────── function sampleSiteTheme() { const contentRoot = document.querySelector(CONTENT_SELECTOR) || document.body; const docStyle = getComputedStyle(document.documentElement); function cssVar(...names) { for (const name of names) { const val = docStyle.getPropertyValue(name).trim(); if (val) return val; } return null; } function usable(c) { return c && c !== 'rgba(0, 0, 0, 0)' && c !== 'transparent'; } const vpBg = cssVar('--vp-c-bg', '--vp-c-bg-soft', '--c-bg'); const vpText = cssVar('--vp-c-text-1', '--vp-c-text', '--c-text'); const vpLink = cssVar('--vp-c-brand-1', '--vp-c-brand', '--c-brand'); const vpFont = cssVar('--vp-font-family-base'); const dsBg = cssVar('--ifm-background-color', '--ifm-background-surface-color'); const dsText = cssVar('--ifm-font-color-base'); const dsLink = cssVar('--ifm-link-color', '--ifm-color-primary'); const dsFont = cssVar('--ifm-font-size-base'); const gbBg = cssVar('--color-base', '--background'); const gbText = cssVar('--color-text-default', '--text-default'); const gbLink = cssVar('--color-link', '--link'); const mkBg = cssVar('--md-default-bg-color'); const mkText = cssVar('--md-default-fg-color'); const mkLink = cssVar('--md-accent-fg-color', '--md-primary-fg-color'); function getBgFromEl(el) { let node = el; while (node && node !== document.documentElement) { const bg = getComputedStyle(node).backgroundColor; if (usable(bg)) return bg; node = node.parentElement; } return getComputedStyle(document.documentElement).backgroundColor; } function getLinkColorNear(el) { for (const a of (el ? el.querySelectorAll('a') : [])) { const c = getComputedStyle(a).color; if (usable(c)) return c; } let parent = el ? el.parentElement : null; while (parent && parent !== document.body) { for (const a of parent.querySelectorAll('a')) { if (a.closest('#pagegrep-host, nav, header, aside, [role="navigation"]')) continue; const c = getComputedStyle(a).color; if (usable(c)) return c; } parent = parent.parentElement; } return null; } const textEl = contentRoot.querySelector('li, p'); const elemBg = getBgFromEl(contentRoot); const elemText = textEl ? getComputedStyle(textEl).color : null; const elemLink = getLinkColorNear(textEl || contentRoot); const elemFont = textEl ? getComputedStyle(textEl).fontSize : null; const finalBg = vpBg || dsBg || gbBg || mkBg || (usable(elemBg) ? elemBg : null); const finalText = vpText || dsText || gbText || mkText || (usable(elemText) ? elemText : null); const finalLink = vpLink || dsLink || gbLink || mkLink || (usable(elemLink) ? elemLink : null); const finalFont = vpFont || dsFont || elemFont; // Apply to the shadow root's :host via the panel (vars cascade inside shadow) const root = shadow.querySelector('#wgs-panel'); setTimeout(() => { if (finalBg) { root.style.setProperty('--wgs-site-bg', finalBg); overlay.querySelector('#wgs-spotlight').style.setProperty('--wgs-site-bg', finalBg); } if (finalText) { root.style.setProperty('--wgs-site-text', finalText); overlay.querySelector('#wgs-input').style.color = finalText; } if (finalLink) root.style.setProperty('--wgs-site-link', finalLink); if (finalFont) root.style.setProperty('--wgs-site-fontsize', finalFont); }, 0); } sampleSiteTheme(); setTimeout(sampleSiteTheme, 800); new MutationObserver(sampleSiteTheme).observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'data-theme', 'data-dark-mode'] }); const input = shadow.getElementById('wgs-input'); const closeBtn = shadow.getElementById('wgs-close'); const copyBtn = shadow.getElementById('wgs-copy'); const results = shadow.getElementById('wgs-results'); const titleEl = shadow.getElementById('wgs-panel-title'); // ── INDEX ───────────────────────────────────────────────────────────────────── // Each entry: // text – full visible text of the line element (whole
  • ,

    , etc.) // searchText – lowercased: all ancestor heading texts + line text (for matching) // headings – [{level, text, id, node}, …] outermostβ†’innermost let index = null; const LINE_TAGS = new Set(['LI','P','DT','DD','TD','TH','BLOCKQUOTE','FIGCAPTION']); const HEADING_TAGS = new Set(['H1','H2','H3','H4','H5','H6']); function headingLevel(tag) { return parseInt(tag[1], 10); } function buildIndex() { if (index) return; index = []; const root = document.querySelector(CONTENT_SELECTOR) || document.body; // We walk the DOM in tree order using querySelectorAll (DOM order guaranteed). // We maintain a heading stack as we encounter headings. // For LINE_TAGS we emit an index entry β€” but skip ones that only contain // other LINE_TAG descendants (avoid double-indexing outer

  • of nested lists). const headingStack = []; // [{level, text, id, node}] const allEl = root.querySelectorAll('*'); for (const el of allEl) { const tag = el.tagName; // ── Update heading stack ── if (HEADING_TAGS.has(tag)) { const lv = headingLevel(tag); while (headingStack.length && headingStack[headingStack.length - 1].level >= lv) { headingStack.pop(); } // Strip leading decorator symbols common on wiki sites (β–· β–Ί β˜… β€’ etc.) const rawText = el.textContent.trim(); const cleanText = rawText.replace(/^[\s\u00a0\u2000-\u27bf|>\/-]+/g, '').trim() || rawText; headingStack.push({ level: lv, text: cleanText, id: el.id || null, node: el }); continue; } // ── Emit line entry ── if (!LINE_TAGS.has(tag)) continue; // Skip if this element's meaningful text is entirely inside nested line elements // (e.g. a bare
  • wrapping