您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
This script will automatically fold some of the forum posts based on their content
当前为
// ==UserScript== // @namespace Planetsuzy // @name Planetsuzy Threads - Auto fold posts // @description This script will automatically fold some of the forum posts based on their content // @version 1.0.1 // @license MIT // @icon http://ps.fscache.com/styles/style1/images/statusicon/forum_old.gif // @include /^https?://(www\.)?planetsuzy\.org/showthread\.php/ // @include /^https?:\/\/(www\.)?planetsuzy\.org\/t\d+[\w-]+\.html$/ // @require https://cdnjs.cloudflare.com/ajax/libs/autolinker/1.8.3/Autolinker.min.js // @grant GM_getValue // @grant GM_setValue // ==/UserScript== const decoratePosts = function() { /** * Posts containing only links from these domains will be less visible (values stored in settings). */ const blacklistedDomains = GM_getValue('blacklistedDomains', []); /** * Returns two top levels of the URL's hostname. */ const parseDomain = function (href) { return new URL(href, document.location).hostname.split('.').slice(-2).join('.'); }; /** * Returns 'inner' URL if it wrapped by a link anonymizer. */ const unwrapUrl = function (href) { const baseUrl = new URL(href, document.location); if (baseUrl.pathname === '/' && baseUrl.search.length > 1) { const innerParam = baseUrl.search.slice(1); try { new URL(innerParam); return innerParam; } catch (e) {} } return href; }; /** * Store domain of current document for later. */ const documentDomain = parseDomain(document.location.href); /** * Class representing list of links in each post message sorted by link domains. */ class LinksList { constructor() { this.links = {}; } add(href) { let domain; try { domain = parseDomain(unwrapUrl(href)); } catch (e) { // Invalid link, ignore. return; } // Local links ignored. if (domain !== documentDomain) { if (!(domain in this.links)) { this.links[domain] = []; } this.links[domain].push(href); } } get domains() { return Object.keys(this.links); } }; /** * Class representing lists of elements linked with a tag such as a domain name * with a function to change multiple elements appearance at once. */ class TaggedElementsList { constructor(decoratorFn) { this.elements = {}; this.decoratorFn = decoratorFn; } addElement(tag, element) { if (this.elements[tag] === undefined) { this.elements[tag] = []; } this.elements[tag].push(element); } decorateElement(element) { const args = Array.from(arguments).slice(1); this.decoratorFn.apply(element, args); } decorateElements(tag) { const args = Array.from(arguments).slice(1); const that = this; this.elements[tag].forEach(function (element) { that.decoratorFn.apply(element, args); }); } }; const domainSwitches = new TaggedElementsList(function (isBlacklisted) { this.style.fontWeight = isBlacklisted ? 'normal' : 'bold'; this.style.textDecoration = isBlacklisted ? 'line-through' : 'none'; }); const refreshLabels = new TaggedElementsList(function (isVisible) { this.style.display = isVisible ? 'inline' : 'none'; }); document.querySelectorAll('table[id^=post]') .forEach(function (post) { // Find messages in thread and examine them for any links. const postId = post.getAttribute('id').substring(4); const message = post.querySelector('[id^=post_message_]'); if (message) { const links = new LinksList(); // Find anchors in post message. Array.from(message.querySelectorAll('a[href]')) .filter(function(link) { // Function to determine whether the image element inside anchor can be excluded due to not being a file hoster link. const canExcludeImage = function (image) { const parseUrlSegments = function (href) { // Returns URL pathname except the file name. return new URL(href, document.location).pathname.substring(1).split('/').slice(0, -1); }; const isWorthlessLink = function (href) { const url = new URL(href, document.location); return (url.pathname.length === 1 && url.search.length <= 1) // href is to website root URL || url.hostname.indexOf('.') === -1; // hostname is TLD }; const imageSrc = image.getAttribute('src'); try { const linkHref = unwrapUrl(link.getAttribute('href')); const linkDomain = parseDomain(linkHref); return isWorthlessLink(linkHref) // exclude if linked to the root of a website ("worthless link") || [linkDomain, documentDomain].includes(parseDomain(imageSrc)) // exclude if same domain or local image || parseUrlSegments(imageSrc).includes(linkDomain); // exclude if link domain is contained in URL (eg. 3rd part CDN used for previews) } catch (e) { return false; // invalid link, exclude for sure } }; // Exclude images inside anchors if images hostnames match anchors hostnames (previews) or local images. // May cause false positives when download link contains an image (eg icon) of a file hoster directly from its domain name. const images = link.querySelectorAll('img'); if (images.length) { const hostnamesExcluded = Array.from(images) .reduce(function (acc, cur) { return acc && canExcludeImage(cur) }, true); return !hostnamesExcluded; } return true; }) .forEach(function(link) { const deemphasise = function (element) { element.style.opacity = '0.2'; }; const href = link.getAttribute('href').toString(); links.add(href); try { if (blacklistedDomains.includes(parseDomain(unwrapUrl(href)))) { deemphasise(link); } } catch (e) { if (link.querySelectorAll('*').length === 0) { deemphasise(link); // invalid link containing no other elements such as image previews, deemphasise for sure } } }); // Additionally, parse links in 'pre' elements. message.querySelectorAll('pre') .forEach(function (pre) { Autolinker.parse(message.innerHTML, {urls: true}) .forEach(function (match) { links.add(match.getUrl()); }); }); // Set up additional elements. const postCountElement = post.querySelector('#postcount' + postId); const hiddenPost = links.domains.length && !links.domains.filter(function (domain) { return !blacklistedDomains.includes(domain) }).length; // Collapse/reveal post toggle switches. postCountElement.parentElement.prepend((function () { const labels = ['►', '▼']; const initialState = hiddenPost ? 0 : 1; const el = document.createElement('a'); el.innerHTML = labels[1]; el.setAttribute('data-state', 1); el.style.cursor = 'pointer'; el.addEventListener('click', function (e) { const newState = (parseInt(el.getAttribute('data-state')) + 1) % labels.length; Array.from(postCountElement.closest('tbody').querySelectorAll(':scope > tr')) .filter(function (row) { return row !== postCountElement.closest('tr') }) .forEach(function (row) { row.style.display = newState ? 'table-row' : 'none' }); post.parentElement.querySelectorAll('#post_thanks_box_' + postId) .forEach(function (row) { row.style.display = newState ? 'block' : 'none' }); el.innerHTML = labels[newState]; el.setAttribute('data-state', newState); }); if (!initialState) { el.click(); } return el; })()); // Refresh label that shows up to indicate that a refresh is needed to apply changes. postCountElement.parentElement.prepend((function () { const el = document.createElement('a') el.innerHTML = 'F5 ⟳'; el.style.background = 'white'; el.style.borderRadius = '1px'; el.style.color = '#0b198c'; el.style.cursor = 'pointer'; el.style.fontWeight = 'bold'; el.style.margin = '0 3px'; el.style.padding = '1px 3px'; el.addEventListener('click', function () { window.location.reload(); }); refreshLabels.addElement('F5', el); refreshLabels.decorateElement(el, false); return el; })()); // Domain name toggle switches. postCountElement.parentElement.prepend((function () { const el = document.createElement('span'); if (hiddenPost) { el.appendChild(document.createTextNode('⚠️ ')); } links.domains.forEach(function (domain, i) { const ch = document.createElement('span'); ch.innerHTML = domain; ch.style.cursor = 'pointer'; ch.addEventListener('click', function () { // Update domains blacklist property and redecorate elements. const oldBlacklist = GM_getValue('blacklistedDomains', []); const wasBlacklisted = oldBlacklist.includes(domain); const newBlacklist = wasBlacklisted ? oldBlacklist.filter(function (value) { return value !== domain}) : oldBlacklist.concat([domain]); domainSwitches.decorateElements(domain, !wasBlacklisted); GM_setValue('blacklistedDomains', newBlacklist); const needRefresh = blacklistedDomains.length !== newBlacklist.length || blacklistedDomains.filter(function(a) { return newBlacklist.indexOf(a) === -1; }).length; refreshLabels.decorateElements('F5', needRefresh); }); el.appendChild(ch); el.appendChild(document.createTextNode(i < links.domains.length - 1 ? ', ' : ' ')); domainSwitches.addElement(domain, ch); domainSwitches.decorateElement(ch, blacklistedDomains.includes(domain)); }); return el; })()); } }); }; decoratePosts();