Guest User

Untitled

a guest
Jan 18th, 2026
48
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 10.46 KB | None | 0 0
  1. // ==UserScript==
  2. // @name 2ch.su: Hide posts by keywords + hide replies + hide >>links everywhere
  3. // @namespace crono-2ch-hide
  4. // @version 2.1.0
  5. // @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
  6. // @match https://2ch.su/*/res/*
  7. // @match http://2ch.su/*/res/*
  8. // @run-at document-end
  9. // @grant GM_getValue
  10. // @grant GM_setValue
  11. // @grant GM_registerMenuCommand
  12. // ==/UserScript==
  13.  
  14. (() => {
  15. "use strict";
  16.  
  17. const STORAGE_KEY = "hidden_keywords_v2";
  18. const DEFAULT_KEYWORDS = ["@monkey"]; // дефолт
  19.  
  20. // ---------------- Settings ----------------
  21. function getKeywords() {
  22. const raw = GM_getValue(STORAGE_KEY, "");
  23. if (!raw) return DEFAULT_KEYWORDS.slice();
  24. return raw
  25. .split("\n")
  26. .map((s) => s.trim())
  27. .filter(Boolean);
  28. }
  29.  
  30. function setKeywords(list) {
  31. GM_setValue(STORAGE_KEY, list.join("\n"));
  32. }
  33.  
  34. function editKeywords() {
  35. const current = getKeywords().join("\n");
  36. const input = prompt(
  37. "Ключевые слова (по одному в строке).\n" +
  38. "Совпадение = подстрока (регистр не важен).\n\n" +
  39. "Пример:\n@monkey\nслово2\nфраза 3",
  40. current
  41. );
  42. if (input === null) return;
  43.  
  44. const list = input
  45. .split("\n")
  46. .map((s) => s.trim())
  47. .filter(Boolean);
  48.  
  49. setKeywords(list.length ? list : DEFAULT_KEYWORDS.slice());
  50. applyAll();
  51. alert("Сохранено и применено.");
  52. }
  53.  
  54. GM_registerMenuCommand("2ch.su: настроить ключевые слова", editKeywords);
  55. GM_registerMenuCommand("2ch.su: сбросить на дефолт", () => {
  56. setKeywords(DEFAULT_KEYWORDS.slice());
  57. applyAll();
  58. alert("Сброшено на дефолт и применено.");
  59. });
  60.  
  61. // ---------------- Helpers ----------------
  62. function normalize(s) {
  63. return (s || "").toLowerCase();
  64. }
  65.  
  66. function matchesAny(text, keywords) {
  67. const t = normalize(text);
  68. for (const kw of keywords) {
  69. if (!kw) continue;
  70. if (t.includes(normalize(kw))) return true;
  71. }
  72. return false;
  73. }
  74.  
  75. // Находим контейнер поста (весь пост)
  76. function getPostContainer(messageEl) {
  77. return (
  78. messageEl.closest(".post") ||
  79. messageEl.closest(".reply") ||
  80. messageEl.closest("li") ||
  81. messageEl.closest("div") ||
  82. messageEl.parentElement
  83. );
  84. }
  85.  
  86. // Достаём ID поста
  87. function getPostIdFromPost(postEl) {
  88. if (!postEl) return null;
  89. const isDigits = (v) => typeof v === "string" && /^\d+$/.test(v);
  90.  
  91. if (isDigits(postEl.id)) return postEl.id;
  92.  
  93. const withId = postEl.querySelector?.("[id]");
  94. if (withId && isDigits(withId.id)) return withId.id;
  95.  
  96. const named = postEl.querySelector?.('a[name]');
  97. if (named) {
  98. const nm = named.getAttribute("name");
  99. if (isDigits(nm)) return nm;
  100. }
  101.  
  102. const a = postEl.querySelector?.('a[href*="#"]');
  103. if (a) {
  104. const href = a.getAttribute("href") || "";
  105. const m = href.match(/#(\d+)\b/);
  106. if (m) return m[1];
  107. }
  108.  
  109. return null;
  110. }
  111.  
  112. // Достать цитируемые ID из сообщения (>>123)
  113. function extractQuotedIds(messageEl) {
  114. const ids = new Set();
  115. const links = messageEl.querySelectorAll('a[href*="#"], a');
  116.  
  117. for (const a of links) {
  118. const text = (a.textContent || "").trim();
  119. let m = text.match(/^>>(\d+)\b/);
  120. if (m) {
  121. ids.add(m[1]);
  122. continue;
  123. }
  124. const href = a.getAttribute("href") || "";
  125. m = href.match(/#(\d+)\b/);
  126. if (m) ids.add(m[1]);
  127. }
  128.  
  129. // запасной вариант, если >>123 не ссылка
  130. const raw = messageEl.textContent || "";
  131. const re = />>(\d{3,})\b/g;
  132. let mm;
  133. while ((mm = re.exec(raw))) ids.add(mm[1]);
  134.  
  135. return ids;
  136. }
  137.  
  138. // универсально: спрятать любые >>ссылки на hiddenIds внутри произвольного DOM-элемента
  139. function hideQuoteLinksInElement(rootEl, hiddenIds) {
  140. if (!rootEl || hiddenIds.size === 0) return;
  141.  
  142. // 1) скрываем якоря-ссылки
  143. const anchors = rootEl.querySelectorAll("a");
  144. for (const a of anchors) {
  145. const text = (a.textContent || "").trim();
  146. const href = a.getAttribute("href") || "";
  147.  
  148. // нас интересуют именно ссылки вида >>123 или ссылки, ведущие на #123
  149. let id = null;
  150.  
  151. let m = text.match(/^>>(\d+)\b/);
  152. if (m) id = m[1];
  153.  
  154. if (!id) {
  155. m = href.match(/#(\d+)\b/);
  156. if (m) {
  157. // чтобы не зацепить “перmalink” и прочие якоря — проверим, что вид ссылки похож на цитату
  158. // (обычно у цитат текст тоже содержит >>)
  159. if (text.includes(">>")) id = m[1];
  160. else {
  161. // иногда в рефмапе текст может быть пустым/иконкой — тогда ориентируемся по классу/атрибутам
  162. // но без жёсткой привязки: просто пропустим, если нет >> в тексте
  163. id = null;
  164. }
  165. }
  166. }
  167.  
  168. if (id && hiddenIds.has(id)) {
  169. a.dataset.kwHiddenQuote = "1";
  170. a.style.display = "none";
  171.  
  172. // иногда после >>123 стоит <br> или разделитель — тоже уберём, если рядом
  173. const next = a.nextSibling;
  174. if (next && next.nodeType === Node.ELEMENT_NODE && next.tagName === "BR") {
  175. next.dataset.kwHiddenQuote = "1";
  176. next.style.display = "none";
  177. }
  178. }
  179. }
  180.  
  181. // 2) если >>123 встречается как чистый текст — вырежем (не трогаем текст внутри ссылок)
  182. const walker = document.createTreeWalker(
  183. rootEl,
  184. NodeFilter.SHOW_TEXT,
  185. {
  186. acceptNode(node) {
  187. if (!node.nodeValue) return NodeFilter.FILTER_REJECT;
  188. if (node.parentElement && node.parentElement.closest("a")) return NodeFilter.FILTER_REJECT;
  189. return NodeFilter.FILTER_ACCEPT;
  190. },
  191. }
  192. );
  193.  
  194. const toReplace = [];
  195. while (walker.nextNode()) {
  196. const node = walker.currentNode;
  197. const val = node.nodeValue;
  198. if (!val.includes(">>")) continue;
  199.  
  200. let replaced = val;
  201. for (const id of hiddenIds) {
  202. const re = new RegExp(`>>${id}\\b`, "g");
  203. replaced = replaced.replace(re, "");
  204. }
  205. if (replaced !== val) {
  206. replaced = replaced
  207. .replace(/[ \t]{2,}/g, " ")
  208. .replace(/(\n\s*\n)+/g, "\n\n");
  209. toReplace.push([node, replaced]);
  210. }
  211. }
  212. for (const [node, replaced] of toReplace) node.nodeValue = replaced;
  213. }
  214.  
  215. // ---------------- Core apply ----------------
  216. function collectPosts(root = document) {
  217. const messages = root.querySelectorAll("article.post__message, .post__message");
  218. const posts = [];
  219.  
  220. for (const msg of messages) {
  221. const postEl = getPostContainer(msg);
  222. if (!postEl) continue;
  223.  
  224. const id = getPostIdFromPost(postEl);
  225. if (!id) continue;
  226.  
  227. posts.push({
  228. id,
  229. postEl,
  230. msgEl: msg,
  231. text: msg.innerText || msg.textContent || "",
  232. quotedIds: extractQuotedIds(msg),
  233. });
  234. }
  235. return posts;
  236. }
  237.  
  238. function resetAllHiding(root = document) {
  239. // показать обратно скрытые посты
  240. const hiddenPosts = root.querySelectorAll('[data-kwHidden="1"]');
  241. for (const el of hiddenPosts) {
  242. el.style.display = "";
  243. el.removeAttribute("data-kwHidden");
  244. el.removeAttribute("data-kwHiddenReason");
  245. }
  246.  
  247. // показать обратно скрытые цитаты/ссылки
  248. const hiddenQuotes = root.querySelectorAll('[data-kwHiddenQuote="1"]');
  249. for (const el of hiddenQuotes) {
  250. el.style.display = "";
  251. el.removeAttribute("data-kwHiddenQuote");
  252. }
  253. }
  254.  
  255. function hidePost(postEl, reason) {
  256. postEl.dataset.kwHidden = "1";
  257. postEl.dataset.kwHiddenReason = reason || "";
  258. postEl.style.display = "none";
  259. }
  260.  
  261. function applyAll() {
  262. resetAllHiding(document);
  263.  
  264. const keywords = getKeywords();
  265. const posts = collectPosts(document);
  266.  
  267. // 1) скрыть посты по ключевым словам
  268. const keywordHiddenIds = new Set();
  269. for (const p of posts) {
  270. if (matchesAny(p.text, keywords)) {
  271. hidePost(p.postEl, "keyword");
  272. keywordHiddenIds.add(p.id);
  273. }
  274. }
  275.  
  276. // 2) скрыть посты, которые отвечают (>>ID) на keyword-hidden посты (только прямые ответы)
  277. const replyHiddenIds = new Set();
  278. for (const p of posts) {
  279. if (p.postEl.dataset.kwHidden === "1") continue;
  280. for (const qid of p.quotedIds) {
  281. if (keywordHiddenIds.has(qid)) {
  282. hidePost(p.postEl, "reply-to-keyword-hidden");
  283. replyHiddenIds.add(p.id);
  284. break;
  285. }
  286. }
  287. }
  288.  
  289. // 3) скрыть любые >>ссылки на скрытые посты — ВЕЗДЕ внутри видимых постов
  290. const allHiddenIds = new Set([...keywordHiddenIds, ...replyHiddenIds]);
  291.  
  292. for (const p of posts) {
  293. if (p.postEl.dataset.kwHidden === "1") continue;
  294.  
  295. // ключевой фикс: сканируем весь контейнер поста, а не только текст сообщения
  296. hideQuoteLinksInElement(p.postEl, allHiddenIds);
  297. }
  298. }
  299.  
  300. // ---------------- Dynamic updates ----------------
  301. let scheduled = false;
  302. function scheduleApply() {
  303. if (scheduled) return;
  304. scheduled = true;
  305. setTimeout(() => {
  306. scheduled = false;
  307. applyAll();
  308. }, 120);
  309. }
  310.  
  311. const mo = new MutationObserver((mutations) => {
  312. for (const m of mutations) {
  313. if (m.addedNodes && m.addedNodes.length) {
  314. scheduleApply();
  315. return;
  316. }
  317. }
  318. });
  319.  
  320. mo.observe(document.documentElement, { childList: true, subtree: true });
  321.  
  322. applyAll();
  323. })();
Advertisement
Add Comment
Please, Sign In to add comment