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.9.2
// @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 *://*/*
// @exclude *://www.google.*/*
// @exclude *://search.yahoo.com/*
// @exclude *://www.bing.com/*
// @exclude *://duckduckgo.com/*
// @exclude *://search.brave.com/*
// @exclude *://*.yandex.*/*
// @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==
(() => {
'use strict';
/* ---------- Early exit if site is excluded ---------- */
const HOST = location.hostname;
const EX_KEY = 'la_excluded_sites';
const excluded = JSON.parse(localStorage.getItem(EX_KEY) || '[]');
if (excluded.includes(HOST)) return;
/* ---------- Config ---------- */
const MIN_WIDTH_PX = 250;
const MAX_WIDTH_PCT = 30; // sidebar width cap (% of viewport)
const TRANS_MS = 320; // animation
const FETCH_CONCURRENCY = 4;
const OPEN_DELAY_MS = 350; // ms between tab opens
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]));
const queuedFetch = createQueue(FETCH_CONCURRENCY);
/* ---------- Extract & dedupe ---------- */
function extractUniqueMatches(){
const text=(document.body.innerText||'').replace(/\s+/g,' ');
const acts = [...text.matchAll(actRegex)].map(m=>(m[0]||'').trim());
const rules= [...text.matchAll(ruleRegex)].map(m=>(m[0]||'').trim());
const seen=new Map();
[...acts,...rules].forEach(r=>{
const k=r.toLowerCase();
if(!seen.has(k)) seen.set(k,r);
});
return [...seen.values()];
}
/* ---------- Limited-concurrency queue ---------- */
function createQueue(max){
const q=[]; let running=0;
const next=()=>{
if(running>=max || !q.length) return;
const {query,res} = q.shift(); running++;
fetchPdf(query).then(r=>res(r)).finally(()=>{running--; next();});
};
return query=>new Promise(res=>{q.push({query,res}); next();});
}
function fetchPdf(query){
return new Promise(resolve=>{
const g = `https://www.google.com/search?q=${encodeURIComponent(query+' pdf')}`;
GM_xmlhttpRequest({
method:'GET', url:g, headers:{'User-Agent':navigator.userAgent},
onload:r=>{
const html=r.responseText||'';
const m=html.match(/https?:\/\/[^"'>\s]+?\.pdf\b/i);
resolve(m?{url:m[0].replace(/\u0026/g,'&'),type:'pdf'}:{url:g,type:'search'});
},
onerror: ()=>resolve({url:g,type:'search'}),
timeout:15000
});
});
}
/* ---------- UI ---------- */
function createSidebar(matches){
/* container */
const box=document.createElement('div');
Object.assign(box.style,{position:'fixed',top:'50%',right:'0',transform:'translateY(-50%)',zIndex:2147483647});
document.body.appendChild(box);
/* style */
const css=document.createElement('style');
css.textContent=`
:root{--bg:#fff;--fg:#0b1220;--accent:#ff8a00;--muted:rgba(11,18,32,.6);--shadow:rgba(12,16,20,.12)}
@media(prefers-color-scheme:dark){:root{--bg:#07101a;--fg:#e6eef8;--accent:#ffb86b;--muted:rgba(230,238,248,.7);--shadow:rgba(0,0,0,.6)}}
#la-sidebar{position:fixed;right:0;top:50%;transform:translate(100%,-50%);opacity:0;
transition:transform ${TRANS_MS}ms cubic-bezier(.2,.9,.2,1),opacity ${TRANS_MS}ms;
width:min(${MAX_WIDTH_PCT}vw,100%);min-width:${MIN_WIDTH_PX}px;max-height:80vh;
background:var(--bg);color:var(--fg);border-radius:12px 0 0 12px;box-shadow:0 12px 30px var(--shadow);
display:flex;flex-direction:column;overflow:hidden}
#la-sidebar.open{transform:translate(0,-50%);opacity:1}
#la-header{display:flex;justify-content:space-between;align-items:center;padding:12px 14px;border-bottom:1px solid rgba(0,0,0,.06)}
#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(--accent)}
#la-controls{display:flex;gap:8px;align-items:center}
.btn{background:transparent;border:1px solid rgba(0,0,0,.12);padding:6px 10px;border-radius:8px;color:var(--fg);cursor:pointer}
.btn[disabled]{opacity:.45;cursor:not-allowed}
.btn-red{border-color:rgba(255,0,0,.25)}
#la-list{flex:1 1 auto;overflow-y:auto;padding:10px}
.la-item{padding:8px 10px;border-radius:8px;margin-bottom:8px}
.la-item:hover{background:rgba(0,0,0,.03)}
.la-item a{color:var(--accent);text-decoration:none;font-weight:600}
.la-meta{font-size:12px;color:var(--muted);margin-top:6px}
#la-footer{padding:8px 12px;border-top:1px solid rgba(0,0,0,.05);font-size:12px;color:var(--muted);display:flex;justify-content:space-between}
#la-tab{position:absolute;right:0;top:50%;transform:translateY(-50%);width:36px;height:76px;border-radius:8px 0 0 8px;
background:var(--accent);color:#fff;display:flex;align-items:center;justify-content:center;font-weight:800;cursor:pointer;box-shadow:0 8px 20px var(--shadow)}
#la-accordion{padding:10px;border-top:1px solid rgba(0,0,0,.04)}
#la-accordion-toggle{display:flex;gap:8px;cursor:pointer;user-select:none}
#la-accordion-content{overflow:hidden;max-height:0;transition:max-height ${TRANS_MS}ms cubic-bezier(.2,.9,.2,1);padding-top:8px}
`;
document.head.appendChild(css);
/* sidebar */
const panel=document.createElement('aside'); panel.id='la-sidebar'; box.appendChild(panel);
/* header */
const header=document.createElement('div'); header.id='la-header'; panel.appendChild(header);
header.innerHTML=`<div id="la-title"><span class="la-dot"></span><span>Acts & Rules</span></div>`;
const ctr=document.createElement('div'); ctr.id='la-controls'; header.appendChild(ctr);
const openAll=document.createElement('button'); openAll.className='btn'; openAll.textContent='Open All PDFs (0)'; openAll.disabled=true; ctr.appendChild(openAll);
const closeBtn=document.createElement('button'); closeBtn.className='btn'; closeBtn.textContent='✕'; ctr.appendChild(closeBtn);
/* exclude button */
const exBtn=document.createElement('button'); exBtn.className='btn btn-red'; ctr.appendChild(exBtn);
let excludedSites=[...excluded]; let isExcluded=excludedSites.includes(HOST);
updateExBtn();
function updateExBtn(){ exBtn.textContent=isExcluded?'Re-enable Site':'Exclude Site'; }
exBtn.addEventListener('click',()=>{
if(isExcluded){ excludedSites=excludedSites.filter(h=>h!==HOST); }
else { excludedSites.push(HOST); }
localStorage.setItem(EX_KEY,JSON.stringify(excludedSites));
isExcluded=!isExcluded; updateExBtn(); alert('Reload to apply.'); });
/* list, accordion, footer, tab */
const list=document.createElement('div'); list.id='la-list'; panel.appendChild(list);
const accWrap=document.createElement('div'); accWrap.id='la-accordion'; panel.appendChild(accWrap);
accWrap.innerHTML=`<div id="la-accordion-toggle"><span id="la-arrow">►</span><span>View individual PDFs</span></div><div id="la-accordion-content"></div>`;
const accToggle=accWrap.firstElementChild, accContent=accWrap.lastElementChild;
const foot=document.createElement('div'); foot.id='la-footer'; foot.innerHTML='<span>Alt+Shift+L to toggle</span><span style="font-size:11px;opacity:.6">Polite queue</span>'; panel.appendChild(foot);
const tab=document.createElement('div'); tab.id='la-tab'; tab.textContent='‹'; box.appendChild(tab);
/* PDF tracking */
const pdfList=[]; const nodes=[];
function showNo(){
list.innerHTML='<div class="la-item"><div class="title">No acts/rules found.</div></div>';
}
async function fill(matches){
list.innerHTML='';
if(!matches.length) return showNo();
matches.forEach(m=>{
const n=document.createElement('div'); n.className='la-item';
n.innerHTML=`<div class="title">${escapeHtml(m)}</div><div class="la-meta">Looking up PDF…</div>`;
list.appendChild(n); nodes.push({q:m,el:n});
});
for(const it of nodes){
const res=await queuedFetch(it.q);
it.el.innerHTML='';
it.el.innerHTML=`<a href="${res.url}" target="_blank" rel="noopener">${escapeHtml(it.q)}</a><div class="la-meta">${
res.type==='pdf'?'Direct PDF':'Google search'}</div>`;
if(res.type==='pdf') pdfList.push({q:it.q,url:res.url});
updateBatch(); }
buildAcc();
}
function updateBatch(){
openAll.textContent=`Open All PDFs (${pdfList.length})`;
openAll.disabled=!pdfList.length;
}
function buildAcc(){
accContent.innerHTML='';
if(!pdfList.length) return accContent.textContent='No direct PDFs found.';
pdfList.forEach(p=>{
const r=document.createElement('div'); r.className='la-item';
r.innerHTML=`<a href="${p.url}" target="_blank" rel="noopener">${escapeHtml(p.q)}</a>`;
accContent.appendChild(r); });
}
/* panel toggle */
let open=false;
function show(){ open=true; panel.classList.add('open'); tab.style.display='none'; }
function hide(){ open=false; panel.classList.remove('open'); tab.style.display='flex'; }
tab.onclick=show; closeBtn.onclick=hide;
/* accordion */
let accOpen=false; accToggle.onclick=()=>{
accOpen=!accOpen;
accToggle.firstElementChild.style.transform=accOpen?'rotate(90deg)':'none';
accContent.style.maxHeight=accOpen?accContent.scrollHeight+'px':'0';
};
/* batch open */
openAll.onclick=async()=>{
if(!pdfList.length) return;
if(!confirm(`Open ${pdfList.length} PDF(s) in new tabs?`)) return;
for(const p of pdfList){
window.open(p.url,'_blank','noopener');
await new Promise(r=>setTimeout(r,OPEN_DELAY_MS));
}
};
/* responsive width
function setW(){
const vw=Math.max(document.documentElement.clientWidth,window.innerWidth);
panel.style.width=Math.max(MIN_WIDTH_PX,Math.floor(vw*MAX_WIDTH_PCT/100))+'px';
}
setW(); window.addEventListener('resize',setW);*/
/* kick off */
fill(matches);
}
/* ---------- boot ---------- */
const matches=extractUniqueMatches();
(document.readyState==='complete'||document.readyState==='interactive')
? setTimeout(()=>createSidebar(matches),400)
: window.addEventListener('load',()=>setTimeout(()=>createSidebar(matches),300));
})();