Weed Out Reddit Posts

Remove unwanted posts from (new) Reddit

// ==UserScript==
// @name         Weed Out Reddit Posts
// @namespace    github.com/JasonAMelancon
// @version      2025-09-03
// @description  Remove unwanted posts from (new) Reddit
// @author       Jason Melancon
// @license      GNU AGPLv3
// @match        http*://www.reddit.com/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// ==/UserScript==

(function() {
    "use strict";

    /* REMOVE UNWANTED ELEMENTS */

    let regexList = GM_getValue("regexList", /*default = */[]);
    let logRemovals = GM_getValue("logRemovals", /*default = */true);
    let namedSubredditLists = {};
    let subredditList = []; // parsed anew each line of options; restricts post removal to these subreddits
    let articles = document.querySelectorAll("article");

    // try to get named lists of subreddits from options
    function parseSubredditLists(lines) {
        for (let line of lines) {
            let matches = [];
            if (matches = line.match(/(\w+)\s*=\s*\{(.*)}/)) {
                let stringSplit = matches[2].split(/[,;| ]/).map(x => x.trim()).filter(x => !!x);
                namedSubredditLists["_" + matches[1]] = stringSplit;
            }
        }
    }

    // assemble list of subreddits to use with the current RegExp in options
    function parseRegExpLine(line) {
        // already handled by previous parse
        if (/\w+\s*=\s*{.*}/.test(line)) {
            return null;
        }

        let matches, pattern;
        if (matches = line.match(/^\s*(.+?)(?:\s*{(.*)})?\s*$/)) {
            pattern = matches[1];
            if (matches[2] && matches[2].trim() !== "") {
                subredditList = matches[2].split(/[,;| ]/).map(x => x.trim()).filter(x => !!x);
                // console.log(`matches[2] = ${matches[2]}`); // DEBUG
            } else {
                // console.log("emptying subredditList"); // DEBUG
                subredditList = [];
            }
            // replace named list with its contents
            let token;
            let subredditListLen = subredditList.length;
            for (let i = 0; i < subredditListLen; i++) {
                token = "_" + subredditList[i];
                if (namedSubredditLists[token]) {
                    subredditList.splice(i, 1);
                    subredditList.push(...namedSubredditLists[token]);
                    // you can't put one named group inside another, so stop
                    // before you reach the replaced ones
                    i--;
                    subredditListLen--;
                }
            }
        }
        return pattern;
    }

    function removeArticles(articles, optionsLines) {
        for (let i = 0; i < articles.length; i++) {
            let postTitle = articles[i].getAttribute("aria-label");
            let pattern = "";
            for (let line of optionsLines) {
                if (!(pattern = parseRegExpLine(line))) {
                    continue;
                }
                try {
                    new RegExp(pattern); // validate RegExp
                } catch (_) {
                    let err = `Invalid RegExp: ${pattern}`;
                    console.log(err);
                    alert(err);
                    return;
                }
                // console.log(`article ${i}: ${postTitle}|${pattern}|{${subredditList}}`); // DEBUG
                if (postTitle.match(pattern)) {
                    // console.log(`Match! ${postTitle} == ${pattern}`); // DEBUG
                    let subreddit = articles[i].querySelector("shreddit-post").getAttribute("subreddit-name");
                    if (subredditList.length > 0) {
                        if (!subredditList.map(x => x.toLowerCase()).includes(subreddit.toLowerCase())) {
                            // console.log(`Not removed: sub = ${subreddit}`) // DEBUG
                            continue;
                        }
                    }
                    const hr = articles[i].nextElementSibling;
                    if (hr && hr.tagName == "HR") {
                        hr.remove();
                    }
                    articles[i].remove();
                    if (logRemovals) {
                        console.log(`Userscript removed "${postTitle}" in r/${subreddit}`);
                    }
                    break;
                }
            }
        }
    }

    // get named lists of subreddits
    parseSubredditLists(regexList);

    // remove articles from initial page load, before scrolling
    removeArticles(articles, regexList);

    // remove articles that appear when scrolling
    new MutationObserver(mutationList => {
        // console.log(`${mutationList.length} new mutations`); // DEUG
        for (let mutation of mutationList) {
            if (mutation.type == "childList") {
                const additions = Array.from(mutation.addedNodes);
                articles = additions.reduce((accumulator, currentNode) => {
                    // added nodes could be articles, elements that contain articles, or neither
                    if (currentNode.nodeType === Node.ELEMENT_NODE) {
                        if (currentNode.tagName === "ARTICLE") {
                            return accumulator.concat(currentNode); // added node is article
                        }
                        const containedArticles = Array.from(currentNode.querySelectorAll("article"));
                        return accumulator.concat(containedArticles); // added node has possible descendent articles
                    }
                    return accumulator; // added node is not an element
                }, []);
                // console.log(`${articles.length} new articles`); // DEBUG
                if (articles.length == 0) {
                    continue;
                }
                regexList = GM_getValue("regexList", /*default = */[]); // refresh in case user updated options
                removeArticles(articles, regexList);
            }
        }
    }).observe(document.body, { childList: true, subtree: true });

    /* SET SCRIPT OPTIONS */

    // create the options page
    const optionsHtml = `
        <!DOCTYPE html>
        <html>
        <head>
            <title>Script Options</title>
            <style>
                #options label {
                    all: unset;
                    font-size: 9pt;
                }
                #options label[for="regexList"] {
                    display: block;
                    margin-bottom: 10px;
                }
                #options textarea {
                    background-color: black;
                    display: block;
                    margin-bottom: 5px;
                    font-size: 9pt;
                    font-family: 'Lucida Console', Monaco, monospace;
                }
                #options button {
                    margin-top: 10px;
                    border-radius: 4px;
                    padding-left: 10px;
                    padding-right: 10px;
                }
                #options input[type="checkbox"],
                #options input[type="checkbox"] + label {
                    margin-top: 12px;
                    display: inline-block;
                }
                #options input[type="checkbox"] {
                    margin-left: 0px;
                    position: relative;
                    top: -6px;
                }
                #options p {
                    font-size: 8pt;
                    margin-top: 5pt;
                    margin-bottom: 5pt;
                }
                #options form div {
                    max-width: 350px;
                    box-sizing: border-box;
                }
                #options div:has( > code) {
                    background-color: black;
                }
                #options code {
                    all: unset;
                    font-size: 8pt;
                    font-family: 'Lucida Console', Monaco, monospace;
                    color: inherit;
                    background-color: inherit;
                    border: 0px;
                }
            </style>
        </head>
        <body>
            <div id="options">
                <h1>Script Options</h1>
                <form id="optionsForm">
                    <label for="regexList">
                        Posts will be hidden if title matches one of these <a><strong>reg</strong>ular <strong>ex</strong>pressions</a>:
                    </label>
                    <textarea id="regexList" name="regexList" rows="5" cols="33" spellcheck="false"></textarea>
                    <div>
                        <p>Hint: You can also follow a <a><strong>regex</strong></a> with a list of subreddits in curly braces. In that case, \
                            the pattern will only be used to remove posts from those subreddits.</p>
                        <p>Not only that, but you can add lines that create named lists of subreddits, and then put \
                            these names after a regex in place of the list they represent, like so:<p>
                        <div>
                            <code>favorites = { funny, politics }<br>
                                  /[tT]rump/ { favorites, rant }</code>
                        </div>
                    </div>
                    <input id="logCheckbox" name="logCheckbox" type="checkbox">
                    <label for="logCheckbox">
                        Log removed items to the Developer Tools console
                    </label>
                    <div>
                        <button type="submit">Save</button>
                        <button id="closeOptions">Close</button>
                    </div>
                </form>
            <div>
        </body>
        </html>
    `;

    function openOptionsInterface() {
        // create a modal for the options interface. Use an in-page modal because
        // - it doesn't use GM_openInTab because Firefox doesn't allow data: URLs,
        //   so the HTML would have to be in a separate file
        // - it doesn't use a separate HTML file, because I have no idea how to install
        //   that along with a userscript, and the userscript can't generate one
        // - it doesn't use a popup window, because those are typically blocked on a
        //   per-site basis by the browser settings
        const modal = document.createElement("div");
        modal.style.position = "fixed";
        modal.style.top = "0";
        modal.style.left = "0";
        modal.style.width = "100%";
        modal.style.height = "100%";
        modal.style.backgroundColor = "rgba(0, 0, 0, 0.5)";
        modal.style.zIndex = "9999";
        modal.style.display = "flex";
        modal.style.justifyContent = "center";
        modal.style.alignItems = "center";

        const optionsBox = document.createElement("div");
        optionsBox.id = "options";
        optionsBox.style.backgroundColor = "hsl(from thistle h s calc(l - .90*l))";
        optionsBox.style.padding = "20px";
        optionsBox.style.borderRadius = "5px";
        optionsBox.style.boxShadow = "0 0 10px rgba(0, 0, 0, 0.8)";
        optionsBox.innerHTML = optionsHtml;

        // fill text entry with saved value, if any
        const regexArea = optionsBox.querySelector("textarea");
        regexArea.value = GM_getValue("regexList", /*default = */[]).join("\n");
        // place text entry cursor
        if (typeof regexArea.setSelectionRange === "function") {
            regexArea.focus();
            regexArea.setSelectionRange(0, 0);
        } else if (typeof regexArea.createTextRange === "function") {
            const range = regexArea.createTextRange();
            range.moveStart('character', 0);
            range.select();
        }

        // set checkbox to saved value (defaults to checked)
        const logCheckbox = optionsBox.querySelector("input#logCheckbox");
        logCheckbox.checked = GM_getValue("logRemovals", /*default = */true);

        // set up explanatory links about regular expressions
        const regexLinks = optionsBox.querySelectorAll("a");
        regexLinks.forEach(a => {
            a.href = "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions";
            a.target = "_blank";
            a.style.color = "inherit";
        });

        modal.appendChild(optionsBox);
        document.body.appendChild(modal);

        regexArea.focus();

        // deal with issue where the example text wraps
        const exampleOptions = optionsBox.querySelector("#options code");
        const exampleOptionsDiv = exampleOptions.parentElement;
        exampleOptionsDiv.style.width = exampleOptions.offsetWidth + 25 + "px";
        exampleOptionsDiv.style.padding = "10px";
        exampleOptionsDiv.style.paddingTop = "3px";
        exampleOptionsDiv.style.paddingBottom = "6px";

        // add button event listeners to set handlers
        // (button listeners are removed when the modal is removed/closed)
        function addButtonHandlers() {
            const form = document.getElementById("optionsForm");
            if (form) {
                // handle form submission (save options)
                form.addEventListener("submit", function handleFormSubmit(event) {
                    event.preventDefault();
                    let newRegexList = document.getElementById("regexList").value.split("\n");
                    newRegexList = newRegexList.filter(item => item.trim() !== "");
                    GM_setValue("regexList", newRegexList);
                    GM_setValue("logRemovals", logCheckbox.checked);
                    alert("Options saved!");
                });
                // close modal
                document.getElementById("closeOptions").addEventListener("click", function() {
                    document.body.removeChild(modal);
                    // update display using new settings
                    regexList = GM_getValue("regexList", /*default = */[]);
                    logRemovals = GM_getValue("logRemovals", /*default = */true);
                    parseSubredditLists();
                    removeArticles(articles, regexList);
                });
            }
            // opening options adds this new listener every time, so remove every time
            document.removeEventListener("DOMContentLoaded", addButtonHandlers);
        }

        // decide when to add form's event listeners
        if (document.readyState === "loading") {
            // loading hasn't finished yet
            document.addEventListener("DOMContentLoaded", addButtonHandlers);
        } else {
            // DOMContentLoaded has already fired
            addButtonHandlers();
        }
    }

    // set the options handler
    GM_registerMenuCommand("Options", openOptionsInterface);

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址