Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // ==UserScript==
- // @name 2ch.su: Hide posts by keywords + hide replies + hide >>links everywhere
- // @namespace crono-2ch-hide
- // @version 2.1.0
- // @description Hides posts containing keywords, hides posts that reply to keyword-hidden posts, and hides any >>links (including refmap/backlinks) pointing to hidden posts on 2ch.su
- // @match https://2ch.su/*/res/*
- // @match http://2ch.su/*/res/*
- // @run-at document-end
- // @grant GM_getValue
- // @grant GM_setValue
- // @grant GM_registerMenuCommand
- // ==/UserScript==
- (() => {
- "use strict";
- const STORAGE_KEY = "hidden_keywords_v2";
- const DEFAULT_KEYWORDS = ["@monkey"]; // дефолт
- // ---------------- Settings ----------------
- function getKeywords() {
- const raw = GM_getValue(STORAGE_KEY, "");
- if (!raw) return DEFAULT_KEYWORDS.slice();
- return raw
- .split("\n")
- .map((s) => s.trim())
- .filter(Boolean);
- }
- function setKeywords(list) {
- GM_setValue(STORAGE_KEY, list.join("\n"));
- }
- function editKeywords() {
- const current = getKeywords().join("\n");
- const input = prompt(
- "Ключевые слова (по одному в строке).\n" +
- "Совпадение = подстрока (регистр не важен).\n\n" +
- "Пример:\n@monkey\nслово2\nфраза 3",
- current
- );
- if (input === null) return;
- const list = input
- .split("\n")
- .map((s) => s.trim())
- .filter(Boolean);
- setKeywords(list.length ? list : DEFAULT_KEYWORDS.slice());
- applyAll();
- alert("Сохранено и применено.");
- }
- GM_registerMenuCommand("2ch.su: настроить ключевые слова", editKeywords);
- GM_registerMenuCommand("2ch.su: сбросить на дефолт", () => {
- setKeywords(DEFAULT_KEYWORDS.slice());
- applyAll();
- alert("Сброшено на дефолт и применено.");
- });
- // ---------------- Helpers ----------------
- function normalize(s) {
- return (s || "").toLowerCase();
- }
- function matchesAny(text, keywords) {
- const t = normalize(text);
- for (const kw of keywords) {
- if (!kw) continue;
- if (t.includes(normalize(kw))) return true;
- }
- return false;
- }
- // Находим контейнер поста (весь пост)
- function getPostContainer(messageEl) {
- return (
- messageEl.closest(".post") ||
- messageEl.closest(".reply") ||
- messageEl.closest("li") ||
- messageEl.closest("div") ||
- messageEl.parentElement
- );
- }
- // Достаём ID поста
- function getPostIdFromPost(postEl) {
- if (!postEl) return null;
- const isDigits = (v) => typeof v === "string" && /^\d+$/.test(v);
- if (isDigits(postEl.id)) return postEl.id;
- const withId = postEl.querySelector?.("[id]");
- if (withId && isDigits(withId.id)) return withId.id;
- const named = postEl.querySelector?.('a[name]');
- if (named) {
- const nm = named.getAttribute("name");
- if (isDigits(nm)) return nm;
- }
- const a = postEl.querySelector?.('a[href*="#"]');
- if (a) {
- const href = a.getAttribute("href") || "";
- const m = href.match(/#(\d+)\b/);
- if (m) return m[1];
- }
- return null;
- }
- // Достать цитируемые ID из сообщения (>>123)
- function extractQuotedIds(messageEl) {
- const ids = new Set();
- const links = messageEl.querySelectorAll('a[href*="#"], a');
- for (const a of links) {
- const text = (a.textContent || "").trim();
- let m = text.match(/^>>(\d+)\b/);
- if (m) {
- ids.add(m[1]);
- continue;
- }
- const href = a.getAttribute("href") || "";
- m = href.match(/#(\d+)\b/);
- if (m) ids.add(m[1]);
- }
- // запасной вариант, если >>123 не ссылка
- const raw = messageEl.textContent || "";
- const re = />>(\d{3,})\b/g;
- let mm;
- while ((mm = re.exec(raw))) ids.add(mm[1]);
- return ids;
- }
- // универсально: спрятать любые >>ссылки на hiddenIds внутри произвольного DOM-элемента
- function hideQuoteLinksInElement(rootEl, hiddenIds) {
- if (!rootEl || hiddenIds.size === 0) return;
- // 1) скрываем якоря-ссылки
- const anchors = rootEl.querySelectorAll("a");
- for (const a of anchors) {
- const text = (a.textContent || "").trim();
- const href = a.getAttribute("href") || "";
- // нас интересуют именно ссылки вида >>123 или ссылки, ведущие на #123
- let id = null;
- let m = text.match(/^>>(\d+)\b/);
- if (m) id = m[1];
- if (!id) {
- m = href.match(/#(\d+)\b/);
- if (m) {
- // чтобы не зацепить “перmalink” и прочие якоря — проверим, что вид ссылки похож на цитату
- // (обычно у цитат текст тоже содержит >>)
- if (text.includes(">>")) id = m[1];
- else {
- // иногда в рефмапе текст может быть пустым/иконкой — тогда ориентируемся по классу/атрибутам
- // но без жёсткой привязки: просто пропустим, если нет >> в тексте
- id = null;
- }
- }
- }
- if (id && hiddenIds.has(id)) {
- a.dataset.kwHiddenQuote = "1";
- a.style.display = "none";
- // иногда после >>123 стоит <br> или разделитель — тоже уберём, если рядом
- const next = a.nextSibling;
- if (next && next.nodeType === Node.ELEMENT_NODE && next.tagName === "BR") {
- next.dataset.kwHiddenQuote = "1";
- next.style.display = "none";
- }
- }
- }
- // 2) если >>123 встречается как чистый текст — вырежем (не трогаем текст внутри ссылок)
- const walker = document.createTreeWalker(
- rootEl,
- NodeFilter.SHOW_TEXT,
- {
- acceptNode(node) {
- if (!node.nodeValue) return NodeFilter.FILTER_REJECT;
- if (node.parentElement && node.parentElement.closest("a")) return NodeFilter.FILTER_REJECT;
- return NodeFilter.FILTER_ACCEPT;
- },
- }
- );
- const toReplace = [];
- while (walker.nextNode()) {
- const node = walker.currentNode;
- const val = node.nodeValue;
- if (!val.includes(">>")) continue;
- let replaced = val;
- for (const id of hiddenIds) {
- const re = new RegExp(`>>${id}\\b`, "g");
- replaced = replaced.replace(re, "");
- }
- if (replaced !== val) {
- replaced = replaced
- .replace(/[ \t]{2,}/g, " ")
- .replace(/(\n\s*\n)+/g, "\n\n");
- toReplace.push([node, replaced]);
- }
- }
- for (const [node, replaced] of toReplace) node.nodeValue = replaced;
- }
- // ---------------- Core apply ----------------
- function collectPosts(root = document) {
- const messages = root.querySelectorAll("article.post__message, .post__message");
- const posts = [];
- for (const msg of messages) {
- const postEl = getPostContainer(msg);
- if (!postEl) continue;
- const id = getPostIdFromPost(postEl);
- if (!id) continue;
- posts.push({
- id,
- postEl,
- msgEl: msg,
- text: msg.innerText || msg.textContent || "",
- quotedIds: extractQuotedIds(msg),
- });
- }
- return posts;
- }
- function resetAllHiding(root = document) {
- // показать обратно скрытые посты
- const hiddenPosts = root.querySelectorAll('[data-kwHidden="1"]');
- for (const el of hiddenPosts) {
- el.style.display = "";
- el.removeAttribute("data-kwHidden");
- el.removeAttribute("data-kwHiddenReason");
- }
- // показать обратно скрытые цитаты/ссылки
- const hiddenQuotes = root.querySelectorAll('[data-kwHiddenQuote="1"]');
- for (const el of hiddenQuotes) {
- el.style.display = "";
- el.removeAttribute("data-kwHiddenQuote");
- }
- }
- function hidePost(postEl, reason) {
- postEl.dataset.kwHidden = "1";
- postEl.dataset.kwHiddenReason = reason || "";
- postEl.style.display = "none";
- }
- function applyAll() {
- resetAllHiding(document);
- const keywords = getKeywords();
- const posts = collectPosts(document);
- // 1) скрыть посты по ключевым словам
- const keywordHiddenIds = new Set();
- for (const p of posts) {
- if (matchesAny(p.text, keywords)) {
- hidePost(p.postEl, "keyword");
- keywordHiddenIds.add(p.id);
- }
- }
- // 2) скрыть посты, которые отвечают (>>ID) на keyword-hidden посты (только прямые ответы)
- const replyHiddenIds = new Set();
- for (const p of posts) {
- if (p.postEl.dataset.kwHidden === "1") continue;
- for (const qid of p.quotedIds) {
- if (keywordHiddenIds.has(qid)) {
- hidePost(p.postEl, "reply-to-keyword-hidden");
- replyHiddenIds.add(p.id);
- break;
- }
- }
- }
- // 3) скрыть любые >>ссылки на скрытые посты — ВЕЗДЕ внутри видимых постов
- const allHiddenIds = new Set([...keywordHiddenIds, ...replyHiddenIds]);
- for (const p of posts) {
- if (p.postEl.dataset.kwHidden === "1") continue;
- // ключевой фикс: сканируем весь контейнер поста, а не только текст сообщения
- hideQuoteLinksInElement(p.postEl, allHiddenIds);
- }
- }
- // ---------------- Dynamic updates ----------------
- let scheduled = false;
- function scheduleApply() {
- if (scheduled) return;
- scheduled = true;
- setTimeout(() => {
- scheduled = false;
- applyAll();
- }, 120);
- }
- const mo = new MutationObserver((mutations) => {
- for (const m of mutations) {
- if (m.addedNodes && m.addedNodes.length) {
- scheduleApply();
- return;
- }
- }
- });
- mo.observe(document.documentElement, { childList: true, subtree: true });
- applyAll();
- })();
Advertisement
Add Comment
Please, Sign In to add comment