Detect legal acts/rules, dedupe mentions, find PDFs (or fallback to Google). Smooth slide-out sidebar, draggable (vertical), batch open PDFs, keyboard toggle (Alt+Shift+L). Theme-aware.
当前为
// ==UserScript==
// @name Legal Acts Finder — Polished Slide Sidebar
// @namespace http://tampermonkey.net/
// @version 1.7
// @description Detect legal acts/rules, dedupe mentions, find PDFs (or fallback to Google). Smooth slide-out sidebar, draggable (vertical), batch open PDFs, keyboard toggle (Alt+Shift+L). Theme-aware.
// @author iamnobody
// @license MIT
// @match *://*/*
// @grant GM_xmlhttpRequest
// @icon https://greasyfork.org/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MTg5MTc1LCJwdXIiOiJibG9iX2lkIn19--c218824699773e9e6d58fe11cc76cdbb165a2e65/1000031087.jpg?locale=en
// @banner https://greasyfork.org/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MTg5MTczLCJwdXIiOiJibG9iX2lkIn19--77a89502797ffc05cd152a04c877a3b3de4c24be/1000031086.jpg?locale=en
// ==/UserScript==
(function () {
'use strict';
/* ---------- Config ---------- */
const MIN_WIDTH_PX = 250;
const MAX_WIDTH_PCT = 30; // max width as % of viewport width
const TRANS_MS = 320;
const FETCH_CONCURRENCY = 4;
const OPEN_DELAY_MS = 350; // delay between opening tabs
const TOGGLE_SHORTCUT = { altKey: true, shiftKey: true, key: 'L' }; // Alt+Shift+L
/* ---------- Regexes ---------- */
const actRegex = /(\b[A-Z]?[a-zA-Z&\-\s]{2,}?\s+act\s+of\s+\d{4}\b)|(\b[A-Z]?[a-zA-Z&\-\s]{2,}?\s+act,\s+\d{4}\b)|(\b[A-Z]?[a-zA-Z&\-\s]{2,}?\s+act\s+of\s+year\s+\d{4}\b)/gi;
const ruleRegex = /\bsection\s+\w+\s+of\s+\w+\s+act,\s+\d{4}\b/gi;
/* ---------- Helpers ---------- */
const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
const escapeHtml = s => String(s).replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));
/* ---------- Extract & Dedupe ---------- */
function extractUniqueMatches() {
const text = (document.body.innerText || document.body.textContent || '').replace(/\s+/g, ' ');
const acts = Array.from(text.matchAll(actRegex)).map(m => (m[0] || '').trim());
const rules = Array.from(text.matchAll(ruleRegex)).map(m => (m[0] || '').trim());
const merged = acts.concat(rules);
const seen = new Map();
for (const raw of merged) {
const key = raw.toLowerCase();
if (!seen.has(key)) seen.set(key, raw);
}
return Array.from(seen.values());
}
/* ---------- Fetch queue (limited concurrency) ---------- */
function createQueue(concurrency = FETCH_CONCURRENCY) {
const q = [];
let running = 0;
const runNext = () => {
if (running >= concurrency || q.length === 0) return;
const { query, resolve } = q.shift();
running++;
fetchPdfForQuery(query).then(res => {
running--;
resolve(res);
runNext();
}).catch(() => {
running--;
resolve({ url: `https://www.google.com/search?q=${encodeURIComponent(query + ' pdf')}`, type: 'search' });
runNext();
});
};
return query => new Promise(res => { q.push({ query, resolve: res }); runNext(); });
}
async function fetchPdfForQuery(query) {
return new Promise(resolve => {
const googleUrl = `https://www.google.com/search?q=${encodeURIComponent(query + ' pdf')}`;
try {
GM_xmlhttpRequest({
method: 'GET',
url: googleUrl,
headers: { 'User-Agent': navigator.userAgent },
onload(response) {
const html = response && response.responseText ? response.responseText : '';
const pdfMatch = html.match(/https?:\/\/[^"'>\s]+?\.pdf\b/gi);
if (pdfMatch && pdfMatch.length) {
const pdfUrl = pdfMatch[0].replace(/\\u0026/g, '&');
resolve({ url: pdfUrl, type: 'pdf' });
} else {
resolve({ url: googleUrl, type: 'search' });
}
},
onerror() { resolve({ url: googleUrl, type: 'search' }); },
timeout: 15000
});
} catch (err) {
resolve({ url: googleUrl, type: 'search' });
}
});
}
const queuedFetch = createQueue();
/* ---------- Build UI ---------- */
function createSidebar(matches) {
// container
const container = document.createElement('div');
container.id = 'la-container';
container.style.position = 'fixed';
container.style.top = '50%';
container.style.right = '0';
container.style.transform = 'translateY(-50%)';
container.style.zIndex = '2147483647';
document.body.appendChild(container);
// style
const css = document.createElement('style');
css.textContent = `
:root{--la-bg:#ffffff;--la-fg:#0b1220;--la-accent:#ff8a00;--la-muted:rgba(11,18,32,0.6);--la-shadow:rgba(12,16,20,0.12)}
@media(prefers-color-scheme:dark){:root{--la-bg:#07101a;--la-fg:#e6eef8;--la-accent:#ffb86b;--la-muted:rgba(230,238,248,0.7);--la-shadow:rgba(0,0,0,0.6)}}
#la-sidebar{position:fixed;right:0;top:50%;transform:translate(100%,-50%);transition:transform ${TRANS_MS}ms cubic-bezier(.2,.9,.2,1),opacity ${TRANS_MS}ms;opacity:0;width:min(${MAX_WIDTH_PCT}vw,100%);max-width:calc(${MAX_WIDTH_PCT}vw);min-width:${MIN_WIDTH_PX}px;border-radius:12px 0 0 12px;box-shadow:0 12px 30px var(--la-shadow);background:linear-gradient(180deg,var(--la-bg),var(--la-bg));color:var(--la-fg);backdrop-filter:blur(6px);display:flex;flex-direction:column;max-height:80vh;overflow:hidden}
#la-sidebar.open{transform:translate(0,-50%);opacity:1}
#la-header{display:flex;align-items:center;justify-content:space-between;padding:12px 14px;border-bottom:1px solid rgba(0,0,0,0.06);cursor:grab}
#la-title{display:flex;gap:8px;align-items:center;font-weight:600;font-size:15px}
.la-dot{width:10px;height:10px;border-radius:50%;background:var(--la-accent);display:inline-block}
#la-controls{display:flex;gap:8px;align-items:center}
#la-openall{background:transparent;border:1px solid rgba(0,0,0,0.08);padding:6px 10px;border-radius:8px;color:var(--la-fg);cursor:pointer;transition:transform 120ms}
#la-openall[disabled]{opacity:0.45;cursor:not-allowed}
#la-close{background:transparent;border:0;font-size:18px;cursor:pointer;padding:6px 8px;border-radius:6px;color:var(--la-fg)}
#la-list{padding:10px;overflow-y:auto;flex:1 1 auto}
.la-item{padding:8px 10px;border-radius:8px;margin-bottom:8px;transition:background 160ms}
.la-item:hover{background:rgba(0,0,0,0.03)}
.la-item a{color:var(--la-accent);text-decoration:none;font-weight:600;display:block}
.la-meta{font-size:12px;color:var(--la-muted);margin-top:6px}
#la-footer{padding:8px 12px;border-top:1px solid rgba(0,0,0,0.05);font-size:12px;color:var(--la-muted);display:flex;justify-content:space-between;align-items:center}
#la-tab{position:absolute;right:0;top:50%;transform:translateY(-50%);width:36px;height:76px;border-radius:8px 0 0 8px;background:var(--la-accent);color:#fff;display:flex;align-items:center;justify-content:center;font-weight:800;cursor:pointer;box-shadow:0 8px 20px var(--la-shadow)}
#la-tab:hover{filter:brightness(1.03)}
#la-accordion{padding:10px;border-top:1px solid rgba(0,0,0,0.04)}
#la-accordion-toggle{display:flex;align-items:center;gap:8px;cursor:pointer;user-select:none}
#la-accordion-arrow{transition:transform ${TRANS_MS}ms}
#la-accordion-content{overflow:hidden;max-height:0;transition:max-height ${TRANS_MS}ms cubic-bezier(.2,.9,.2,1);padding-top:8px}
.la-pdf-item{padding:8px;border-radius:8px;margin-bottom:8px;background:transparent}
@media(max-width:520px){#la-sidebar{min-width:220px;max-height:70vh}#la-tab{height:64px;width:34px}}`;
document.head.appendChild(css);
// sidebar
const panel = document.createElement('aside');
panel.id = 'la-sidebar';
container.appendChild(panel);
// header
const header = document.createElement('div');
header.id = 'la-header';
panel.appendChild(header);
const title = document.createElement('div');
title.id = 'la-title';
title.innerHTML = `<span class="la-dot" aria-hidden="true"></span><span>Acts & Rules</span>`;
header.appendChild(title);
const controls = document.createElement('div');
controls.id = 'la-controls';
header.appendChild(controls);
const openAllBtn = document.createElement('button');
openAllBtn.id = 'la-openall';
openAllBtn.textContent = 'Open All PDFs (0)';
openAllBtn.disabled = true;
controls.appendChild(openAllBtn);
const closeBtn = document.createElement('button');
closeBtn.id = 'la-close';
closeBtn.title = 'Close';
closeBtn.textContent = '✕';
controls.appendChild(closeBtn);
// list area (all matches)
const listWrap = document.createElement('div');
listWrap.id = 'la-list';
panel.appendChild(listWrap);
// accordion for individual PDFs
const accordionWrap = document.createElement('div');
accordionWrap.id = 'la-accordion';
panel.appendChild(accordionWrap);
const accToggle = document.createElement('div');
accToggle.id = 'la-accordion-toggle';
accToggle.innerHTML = `<span id="la-accordion-arrow">►</span><span>View individual PDFs</span>`;
accordionWrap.appendChild(accToggle);
const accContent = document.createElement('div');
accContent.id = 'la-accordion-content';
accordionWrap.appendChild(accContent);
// footer
const footer = document.createElement('div');
footer.id = 'la-footer';
footer.innerHTML = `<div style="opacity:0.85">Tip: Press Alt+Shift+L to toggle</div><div style="opacity:0.6;font-size:11px">Polite fetch queue</div>`;
panel.appendChild(footer);
// collapsed tab
const tab = document.createElement('div');
tab.id = 'la-tab';
tab.textContent = '‹';
container.appendChild(tab);
/* ---------- Populate & fetch ---------- */
const pdfEntries = []; // { query, url }
const allItems = []; // DOM mapping
function showNoMatches() {
listWrap.innerHTML = '';
const none = document.createElement('div');
none.className = 'la-item';
none.innerHTML = `<div class="title">No legal acts or rules detected on this page.</div><div class="la-meta">Try a web search for relevant PDFs: <a href="${`https://www.google.com/search?q=${encodeURIComponent((document.title||location.hostname)+' law act pdf')}`}" target="_blank" rel="noopener noreferrer">Search</a></div>`;
listWrap.appendChild(none);
}
async function processMatches(matches) {
listWrap.innerHTML = '';
if (!matches || matches.length === 0) {
showNoMatches();
return;
}
// placeholder items (loading)
for (const m of matches) {
const item = document.createElement('div');
item.className = 'la-item';
item.innerHTML = `<div class="title">${escapeHtml(m)}</div><div class="la-meta">Looking up PDF…</div>`;
listWrap.appendChild(item);
allItems.push({ query: m, el: item });
}
// fetch each via queue
for (const it of allItems) {
try {
const res = await queuedFetch(it.query);
it.el.innerHTML = '';
const a = document.createElement('a');
a.href = res.url;
a.target = '_blank';
a.rel = 'noopener noreferrer';
a.textContent = it.query;
it.el.appendChild(a);
const meta = document.createElement('div');
meta.className = 'la-meta';
if (res.type === 'pdf') {
meta.textContent = 'Direct PDF found — opens in new tab';
// save pdf entry for batch and accordion
pdfEntries.push({ query: it.query, url: res.url });
} else {
meta.innerHTML = `No direct PDF found — opens Google search. Click to refine search.`;
}
it.el.appendChild(meta);
} catch (err) {
it.el.innerHTML = `<div class="title">${escapeHtml(it.query)}</div><div class="la-meta">Lookup failed — <a href="${`https://www.google.com/search?q=${encodeURIComponent(it.query+' pdf')}`}" target="_blank" rel="noopener noreferrer">Search</a></div>`;
}
updatePdfControls();
}
buildAccordion();
}
function updatePdfControls() {
const n = pdfEntries.length;
openAllBtn.textContent = `Open All PDFs (${n})`;
openAllBtn.disabled = n === 0;
}
function buildAccordion() {
accContent.innerHTML = '';
if (pdfEntries.length === 0) {
const hint = document.createElement('div');
hint.className = 'la-meta';
hint.style.padding = '6px 0';
hint.textContent = 'No direct PDFs found. Use the Act links above to search.';
accContent.appendChild(hint);
return;
}
for (const p of pdfEntries) {
const row = document.createElement('div');
row.className = 'la-pdf-item';
const a = document.createElement('a');
a.href = p.url;
a.target = '_blank';
a.rel = 'noopener noreferrer';
a.textContent = p.query;
row.appendChild(a);
accContent.appendChild(row);
}
}
/* ---------- Toggle, Drag, Accordion, Keyboard ---------- */
let isOpen = false;
let dragging = false;
let dragStartY = 0;
let currentCenterY = null; // px
function computeWidth() {
const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);
const px = Math.max(MIN_WIDTH_PX, Math.floor((MAX_WIDTH_PCT / 100) * vw));
panel.style.width = Math.min(px + 'px', '100%');
}
computeWidth();
window.addEventListener('resize', computeWidth);
function openPanel() {
isOpen = true;
panel.classList.add('open');
tab.style.display = 'none';
// position panel; if we have a custom center use it, else center
if (currentCenterY !== null) {
container.style.top = currentCenterY + 'px';
container.style.transform = 'none';
panel.style.transform = 'translate(0, -50%)';
} else {
container.style.top = '50%';
container.style.transform = 'translateY(-50%)';
panel.style.transform = 'translate(0, -50%)';
}
}
function closePanel() {
isOpen = false;
panel.classList.remove('open');
tab.style.display = 'flex';
currentCenterY = null;
container.style.top = '50%';
container.style.transform = 'translateY(-50%)';
panel.style.transform = 'translate(100%,-50%)';
}
tab.addEventListener('click', e => { e.stopPropagation(); openPanel(); });
closeBtn.addEventListener('click', e => { e.stopPropagation(); closePanel(); });
// drag only when open
header.addEventListener('mousedown', e => {
if (!isOpen) return;
dragging = true;
dragStartY = e.clientY;
const rect = container.getBoundingClientRect();
currentCenterY = rect.top + rect.height / 2;
container.style.transform = 'none';
container.style.top = currentCenterY + 'px';
header.style.cursor = 'grabbing';
document.body.style.userSelect = 'none';
});
window.addEventListener('mousemove', e => {
if (!dragging) return;
const dy = e.clientY - dragStartY;
const newCenter = currentCenterY + dy;
const panelRect = panel.getBoundingClientRect();
const half = panelRect.height / 2;
const min = half + 8;
const max = window.innerHeight - half - 8;
const clamped = clamp(newCenter, min, max);
currentCenterY = clamped;
container.style.top = clamped + 'px';
dragStartY = e.clientY;
});
window.addEventListener('mouseup', () => {
if (!dragging) return;
dragging = false;
header.style.cursor = 'grab';
document.body.style.userSelect = '';
});
// accordion toggle
let accOpen = false;
const accArrow = document.getElementById ? null : null; // placeholder to avoid lint warnings
accToggle.addEventListener('click', () => {
accOpen = !accOpen;
const arrow = accToggle.querySelector('#la-accordion-arrow');
if (accOpen) {
arrow.style.transform = 'rotate(90deg)';
accContent.style.maxHeight = Math.min(accContent.scrollHeight + 40, window.innerHeight * 0.4) + 'px';
} else {
arrow.style.transform = 'rotate(0deg)';
accContent.style.maxHeight = '0';
}
});
// keyboard toggle Alt+Shift+L (ignore when typing in inputs/textareas or contentEditable)
window.addEventListener('keydown', e => {
if (!(e.altKey && e.shiftKey && (e.key.toUpperCase() === TOGGLE_SHORTCUT.key))) return;
const active = document.activeElement;
const editable = active && (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA' || active.isContentEditable);
if (editable) return;
e.preventDefault();
if (isOpen) closePanel(); else openPanel();
});
/* ---------- Batch open and per-PDF open ---------- */
openAllBtn.addEventListener('click', async () => {
const n = pdfEntries.length;
if (!n) return;
const confirmMsg = `Open ${n} PDF(s) in new tabs? (This will open ${n} tabs)`;
if (!confirm(confirmMsg)) return;
for (let i = 0; i < pdfEntries.length; i++) {
const url = pdfEntries[i].url;
window.open(url, '_blank', 'noopener');
// delay to reduce popup-blocker risk
await new Promise(res => setTimeout(res, OPEN_DELAY_MS));
}
});
// per-PDF items already link to PDFs in accContent; no extra handlers needed.
return { panel, container, tab, processMatches, openPanel, closePanel, updatePdfControls };
}
/* ---------- Init ---------- */
function init() {
try {
const matches = extractUniqueMatches();
const sidebar = createSidebar(matches);
sidebar.processMatches(matches);
// nothing else; user interacts
} catch (err) {
console.error('Legal Acts Finder error:', err);
}
}
if (document.readyState === 'complete' || document.readyState === 'interactive') {
setTimeout(init, 400);
} else {
window.addEventListener('load', () => setTimeout(init, 300));
}
})();