Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // ==UserScript==
- // @name Warosu Post Filter
- // @namespace https://github.com/fadhilnorraji/warosu-filter
- // @version 1.0.0
- // @description 4chan-X-style regex post filter for warosu.org. Hides or collapses posts client-side based on comment/name/tripcode/subject/filename/post-ID rules. Does not touch the site itself.
- // @author you
- // @match https://warosu.org/*
- // @match http://warosu.org/*
- // @grant GM_getValue
- // @grant GM_setValue
- // @grant GM_registerMenuCommand
- // @grant GM_addStyle
- // @grant GM_setClipboard
- // @run-at document-idle
- // @noframes
- // ==/UserScript==
- (function () {
- 'use strict';
- const STORAGE_KEY = 'warosuFilterRules_v1';
- const FIELD_OPTIONS = ['any', 'comment', 'name', 'tripcode', 'subject', 'filename', 'id'];
- // ------------------------------------------------------------------
- // storage
- // ------------------------------------------------------------------
- function loadRules() {
- try {
- const raw = GM_getValue(STORAGE_KEY, '[]');
- const parsed = JSON.parse(raw);
- return Array.isArray(parsed) ? parsed : [];
- } catch (e) {
- console.error('[Warosu Filter] failed to load rules, resetting', e);
- return [];
- }
- }
- function saveRules(nextRules) {
- rules = nextRules;
- GM_setValue(STORAGE_KEY, JSON.stringify(rules));
- }
- function uid() {
- return 'r_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
- }
- let rules = loadRules();
- const revealed = new Set(); // post ids the user has manually un-hidden this pageload
- // ------------------------------------------------------------------
- // rule compilation
- // ------------------------------------------------------------------
- function compileRule(r) {
- if (!r.enabled || !r.pattern) return { ...r, re: null };
- try {
- return { ...r, re: new RegExp(r.pattern, r.flags || 'i'), error: null };
- } catch (e) {
- return { ...r, re: null, error: String(e.message || e) };
- }
- }
- function testRule(re, text) {
- re.lastIndex = 0; // guard against 'g' flag statefulness
- return re.test(text);
- }
- // ------------------------------------------------------------------
- // page helpers
- // ------------------------------------------------------------------
- function getCurrentBoard() {
- const m = location.pathname.match(/^\/([a-zA-Z0-9]+)\//);
- return m ? m[1] : '';
- }
- function getContainer(postEl) {
- // OP posts are <div class="comment" id="p...">, self-contained.
- // Replies are <td class="comment reply" id="p...">, wrapped in their
- // own <table><tr><td class="doubledash">>></td><td>...</td></tr></table>.
- if (postEl.tagName === 'DIV') return postEl;
- return postEl.closest('table') || postEl;
- }
- function extractFields(postEl, id) {
- const nameEl = postEl.querySelector('.postername');
- const tripEl = postEl.querySelector('.postertrip, .trip');
- const subjEl = postEl.querySelector('.filetitle');
- const fileEl = postEl.querySelector('.fileinfo');
- const bq = postEl.querySelector('blockquote');
- const name = nameEl ? nameEl.textContent.trim() : '';
- const tripcode = tripEl ? tripEl.textContent.trim() : '';
- const subject = subjEl ? subjEl.textContent.trim() : '';
- const comment = bq ? bq.textContent.trim() : '';
- let filename = '';
- if (fileEl) {
- const m = fileEl.textContent.match(/,\s*([^,]+)\s*$/);
- filename = (m ? m[1] : fileEl.textContent).trim();
- }
- const any = [id, name, tripcode, subject, filename, comment].join('\n');
- return { id, name, tripcode, subject, filename, comment, any };
- }
- // ------------------------------------------------------------------
- // hide / show / stub
- // ------------------------------------------------------------------
- function removeStub(id) {
- const stub = document.querySelector(`.wf-stub[data-wf-for="${id}"]`);
- if (stub) stub.remove();
- }
- function showPost(postEl) {
- const id = postEl.id.slice(1);
- getContainer(postEl).style.display = '';
- removeStub(id);
- }
- function hidePost(postEl, rule, id) {
- const container = getContainer(postEl);
- if (rule.mode === 'stub') {
- container.style.display = 'none';
- let stub = document.querySelector(`.wf-stub[data-wf-for="${id}"]`);
- if (!stub) {
- stub = document.createElement('div');
- stub.className = 'wf-stub';
- stub.dataset.wfFor = id;
- container.parentNode.insertBefore(stub, container);
- }
- stub.textContent = '';
- const label = rule.label || rule.pattern;
- const span = document.createElement('span');
- span.textContent = `Filtered post No.${id} (matched "${label}") `;
- const btn = document.createElement('a');
- btn.href = '#';
- btn.className = 'wf-stub-show';
- btn.textContent = '[Show]';
- btn.addEventListener('click', (e) => {
- e.preventDefault();
- revealed.add(id);
- applyFilters();
- });
- stub.append(span, btn);
- } else {
- removeStub(id);
- container.style.display = 'none';
- }
- }
- // ------------------------------------------------------------------
- // main pass
- // ------------------------------------------------------------------
- function applyFilters() {
- const compiled = rules.map(compileRule).filter((r) => r.enabled && r.re);
- if (!compiled.length) {
- document.querySelectorAll('div.comment[id^="p"], td.comment.reply[id^="p"]').forEach(showPost);
- document.querySelectorAll('.wf-stub').forEach((s) => s.remove());
- return;
- }
- const board = getCurrentBoard();
- const posts = document.querySelectorAll('div.comment[id^="p"], td.comment.reply[id^="p"]');
- posts.forEach((postEl) => {
- const id = postEl.id.slice(1);
- if (revealed.has(id)) {
- showPost(postEl);
- return;
- }
- const fields = extractFields(postEl, id);
- let matched = null;
- for (const r of compiled) {
- if (r.boards) {
- const list = r.boards.split(',').map((s) => s.trim()).filter(Boolean);
- if (list.length && !list.includes(board)) continue;
- }
- const text = fields[r.field] ?? fields.any;
- if (testRule(r.re, text)) {
- matched = r;
- break;
- }
- }
- if (matched) {
- hidePost(postEl, matched, id);
- } else {
- showPost(postEl);
- }
- });
- }
- // Re-run on DOM changes (defensive: covers any future live-update
- // behaviour warosu might add, and the quote-preview popup clones).
- let applyScheduled = false;
- function scheduleApply() {
- if (applyScheduled) return;
- applyScheduled = true;
- requestAnimationFrame(() => {
- applyScheduled = false;
- applyFilters();
- });
- }
- const observer = new MutationObserver((mutations) => {
- for (const m of mutations) {
- if (m.addedNodes.length) {
- scheduleApply();
- break;
- }
- }
- });
- // ------------------------------------------------------------------
- // settings panel
- // ------------------------------------------------------------------
- function addStyle(css) {
- if (typeof GM_addStyle === 'function') {
- GM_addStyle(css);
- } else {
- const style = document.createElement('style');
- style.textContent = css;
- document.head.appendChild(style);
- }
- }
- addStyle(`
- #wf-toggle-btn {
- position: fixed;
- right: 12px;
- bottom: 12px;
- z-index: 999999;
- background: #2c2c2c;
- color: #fff;
- border: 1px solid #555;
- border-radius: 4px;
- padding: 6px 10px;
- font: 12px/1.2 sans-serif;
- cursor: pointer;
- opacity: 0.85;
- }
- #wf-toggle-btn:hover { opacity: 1; }
- #wf-overlay {
- position: fixed;
- inset: 0;
- background: rgba(0,0,0,0.5);
- z-index: 1000000;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- #wf-panel {
- background: #fff;
- color: #111;
- width: min(920px, 95vw);
- max-height: 88vh;
- overflow: auto;
- border-radius: 6px;
- padding: 16px 18px 18px;
- font: 13px/1.4 sans-serif;
- box-shadow: 0 4px 24px rgba(0,0,0,0.4);
- }
- #wf-panel h2 { margin: 0 0 10px; font-size: 16px; }
- #wf-panel table { width: 100%; border-collapse: collapse; margin-bottom: 10px; }
- #wf-panel th { text-align: left; font-size: 11px; color: #555; padding: 2px 4px; }
- #wf-panel td { padding: 3px 4px; vertical-align: middle; }
- #wf-panel input[type=text] { width: 100%; box-sizing: border-box; font: 12px monospace; padding: 3px 4px; }
- #wf-panel input.wf-pattern { font-family: monospace; }
- #wf-panel select { font: 12px sans-serif; }
- #wf-panel .wf-err { color: #c0392b; font-size: 11px; }
- #wf-panel .wf-row-actions { display: flex; gap: 8px; margin: 8px 0 14px; }
- #wf-panel button { cursor: pointer; padding: 5px 10px; font: 12px sans-serif; }
- #wf-panel .wf-del { color: #c0392b; border: none; background: none; font-size: 14px; }
- #wf-panel .wf-footer { display: flex; justify-content: space-between; align-items: center; gap: 10px; margin-top: 10px; border-top: 1px solid #ddd; padding-top: 10px; }
- #wf-panel .wf-footer-left, #wf-panel .wf-footer-right { display: flex; gap: 8px; }
- #wf-panel textarea { width: 100%; height: 120px; font: 11px monospace; box-sizing: border-box; }
- #wf-panel .wf-io { margin-top: 12px; display: none; }
- #wf-panel .wf-io.wf-open { display: block; }
- #wf-panel .wf-hint { color: #666; font-size: 11px; margin: 4px 0 12px; }
- .wf-stub {
- border: 1px dashed #999;
- background: rgba(128,128,128,0.08);
- padding: 4px 8px;
- margin: 2px 0;
- font-size: 12px;
- color: #666;
- }
- .wf-stub a { margin-left: 6px; }
- `);
- function fieldSelect(value) {
- const sel = document.createElement('select');
- for (const f of FIELD_OPTIONS) {
- const opt = document.createElement('option');
- opt.value = f;
- opt.textContent = f;
- if (f === value) opt.selected = true;
- sel.appendChild(opt);
- }
- return sel;
- }
- function buildRow(rule) {
- const tr = document.createElement('tr');
- tr.dataset.id = rule.id;
- const tdOn = document.createElement('td');
- const cb = document.createElement('input');
- cb.type = 'checkbox';
- cb.checked = rule.enabled !== false;
- cb.className = 'wf-f-enabled';
- tdOn.appendChild(cb);
- const tdField = document.createElement('td');
- const sel = fieldSelect(rule.field || 'comment');
- sel.className = 'wf-f-field';
- tdField.appendChild(sel);
- const tdPattern = document.createElement('td');
- const pat = document.createElement('input');
- pat.type = 'text';
- pat.className = 'wf-f-pattern wf-pattern';
- pat.placeholder = 'regex, e.g. \\bkeyword\\b';
- pat.value = rule.pattern || '';
- tdPattern.appendChild(pat);
- const err = document.createElement('div');
- err.className = 'wf-err';
- tdPattern.appendChild(err);
- const tdFlags = document.createElement('td');
- const flags = document.createElement('input');
- flags.type = 'text';
- flags.className = 'wf-f-flags';
- flags.placeholder = 'i';
- flags.value = rule.flags || 'i';
- flags.style.width = '40px';
- tdFlags.appendChild(flags);
- const tdBoards = document.createElement('td');
- const boards = document.createElement('input');
- boards.type = 'text';
- boards.className = 'wf-f-boards';
- boards.placeholder = 'all boards';
- boards.value = rule.boards || '';
- boards.style.width = '70px';
- tdBoards.appendChild(boards);
- const tdMode = document.createElement('td');
- const mode = document.createElement('select');
- mode.className = 'wf-f-mode';
- for (const m of ['hide', 'stub']) {
- const opt = document.createElement('option');
- opt.value = m;
- opt.textContent = m === 'hide' ? 'hide' : 'collapse';
- if ((rule.mode || 'hide') === m) opt.selected = true;
- mode.appendChild(opt);
- }
- tdMode.appendChild(mode);
- const tdLabel = document.createElement('td');
- const label = document.createElement('input');
- label.type = 'text';
- label.className = 'wf-f-label';
- label.placeholder = 'note (optional)';
- label.value = rule.label || '';
- tdLabel.appendChild(label);
- const tdDel = document.createElement('td');
- const del = document.createElement('button');
- del.className = 'wf-del';
- del.textContent = '✕';
- del.title = 'Delete rule';
- del.addEventListener('click', () => tr.remove());
- tdDel.appendChild(del);
- tr.append(tdOn, tdField, tdPattern, tdFlags, tdBoards, tdMode, tdLabel, tdDel);
- return tr;
- }
- function readRow(tr) {
- const pattern = tr.querySelector('.wf-f-pattern').value;
- const errBox = tr.querySelector('.wf-err');
- let error = null;
- if (pattern) {
- try {
- new RegExp(pattern, tr.querySelector('.wf-f-flags').value || 'i');
- } catch (e) {
- error = String(e.message || e);
- }
- }
- errBox.textContent = error || '';
- return {
- id: tr.dataset.id,
- enabled: tr.querySelector('.wf-f-enabled').checked,
- field: tr.querySelector('.wf-f-field').value,
- pattern,
- flags: tr.querySelector('.wf-f-flags').value || 'i',
- boards: tr.querySelector('.wf-f-boards').value.trim(),
- mode: tr.querySelector('.wf-f-mode').value,
- label: tr.querySelector('.wf-f-label').value.trim(),
- error,
- };
- }
- function openPanel() {
- if (document.getElementById('wf-overlay')) return;
- const overlay = document.createElement('div');
- overlay.id = 'wf-overlay';
- const panel = document.createElement('div');
- panel.id = 'wf-panel';
- panel.innerHTML = `
- <h2>Warosu Post Filter</h2>
- <div class="wf-hint">
- Rules run client-side only, in your browser. "hide" removes the post entirely;
- "collapse" replaces it with a small clickable placeholder, like 4chan X.
- Leave "boards" empty to apply everywhere, or list board codes like <code>jp,vt</code>.
- </div>
- <table>
- <thead>
- <tr>
- <th>On</th><th>Field</th><th>Pattern (regex)</th><th>Flags</th>
- <th>Boards</th><th>Mode</th><th>Note</th><th></th>
- </tr>
- </thead>
- <tbody id="wf-rows"></tbody>
- </table>
- <div class="wf-row-actions">
- <button id="wf-add">+ Add rule</button>
- <button id="wf-save">Save & apply</button>
- <button id="wf-close">Close</button>
- </div>
- <div class="wf-footer">
- <div class="wf-footer-left">
- <button id="wf-export">Export rules</button>
- <button id="wf-import">Import rules</button>
- </div>
- </div>
- <div class="wf-io" id="wf-io-box">
- <textarea id="wf-io-text" spellcheck="false"></textarea>
- <div class="wf-row-actions">
- <button id="wf-io-copy">Copy to clipboard</button>
- <button id="wf-io-apply">Apply pasted JSON</button>
- <button id="wf-io-cancel">Cancel</button>
- </div>
- </div>
- `;
- overlay.appendChild(panel);
- document.body.appendChild(overlay);
- const rowsBody = panel.querySelector('#wf-rows');
- rules.forEach((r) => rowsBody.appendChild(buildRow(r)));
- panel.querySelector('#wf-add').addEventListener('click', () => {
- rowsBody.appendChild(
- buildRow({ id: uid(), enabled: true, field: 'comment', pattern: '', flags: 'i', mode: 'hide' })
- );
- });
- panel.querySelector('#wf-save').addEventListener('click', () => {
- const rows = [...rowsBody.querySelectorAll('tr')];
- const next = rows.map(readRow).filter((r) => r.pattern);
- if (next.some((r) => r.error)) return; // errors shown inline, block save
- saveRules(next.map(({ error, ...rest }) => rest));
- revealed.clear();
- applyFilters();
- });
- panel.querySelector('#wf-close').addEventListener('click', () => overlay.remove());
- overlay.addEventListener('click', (e) => {
- if (e.target === overlay) overlay.remove();
- });
- const ioBox = panel.querySelector('#wf-io-box');
- const ioText = panel.querySelector('#wf-io-text');
- panel.querySelector('#wf-export').addEventListener('click', () => {
- ioText.value = JSON.stringify(rules, null, 2);
- ioBox.classList.add('wf-open');
- });
- panel.querySelector('#wf-import').addEventListener('click', () => {
- ioText.value = '';
- ioBox.classList.add('wf-open');
- });
- panel.querySelector('#wf-io-cancel').addEventListener('click', () => {
- ioBox.classList.remove('wf-open');
- });
- panel.querySelector('#wf-io-copy').addEventListener('click', () => {
- if (typeof GM_setClipboard === 'function') GM_setClipboard(ioText.value);
- else ioText.select();
- });
- panel.querySelector('#wf-io-apply').addEventListener('click', () => {
- try {
- const parsed = JSON.parse(ioText.value);
- if (!Array.isArray(parsed)) throw new Error('Expected a JSON array of rules');
- rowsBody.textContent = '';
- parsed.forEach((r) => rowsBody.appendChild(buildRow({ ...r, id: r.id || uid() })));
- ioBox.classList.remove('wf-open');
- } catch (e) {
- alert('Invalid JSON: ' + (e.message || e));
- }
- });
- }
- // ------------------------------------------------------------------
- // entry points
- // ------------------------------------------------------------------
- function addToggleButton() {
- if (document.getElementById('wf-toggle-btn')) return;
- const btn = document.createElement('button');
- btn.id = 'wf-toggle-btn';
- btn.textContent = 'Filters';
- btn.title = 'Warosu Post Filter settings';
- btn.addEventListener('click', openPanel);
- document.body.appendChild(btn);
- }
- if (typeof GM_registerMenuCommand === 'function') {
- GM_registerMenuCommand('Warosu Filter: manage rules', openPanel);
- }
- function init() {
- addToggleButton();
- applyFilters();
- observer.observe(document.body, { childList: true, subtree: true });
- }
- if (document.body) init();
- else document.addEventListener('DOMContentLoaded', init, { once: true });
- })();
Advertisement
Add Comment
Please, Sign In to add comment