Adds post authors to items in Reddit feed (on new Reddit)
目前為
// ==UserScript==
// @name Creddit
// @namespace github.com/JasonAMelancon
// @version 2025-09-18
// @description Adds post authors to items in Reddit feed (on new Reddit)
// @author Jason Melancon
// @license GNU AGPLv3
// @match http*://www.reddit.com/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
//-- script begins here --//
const DEBUG = true;
// The msg is in a lambda expression body so that string interpolation only happens if we
// actually print the message (lazy evaluation).
function debugLog(lambdifiedMsg) {
if (DEBUG) console.log(lambdifiedMsg());
}
const scriptName = GM_info.script.name;
const FEED = "shreddit-feed"; // Reddit's custom element name
const POST = "shreddit-post"; // Reddit's custom element name
const VIEW_CONTEXT = "view-context"; // Reddit's custom attribute name
const AGGREGATE_FEED = "AggregateFeed"; // Reddit's post element attribute value; indicates need to run script
const CREDITED = "is-credited"; // my custom attribute for the post element
//== This is the main part of the script. ==//
function runScript() {
const feeds = document.getElementsByTagName(FEED);
if (feeds.length > 1) { // I have no idea whether this is or will ever be necessary
console.log(`[${scriptName}] Multiple Reddit feed nodes present`);
}
const feed = feeds[0];
const articleCollection = feed.querySelectorAll("article");
debugLog(() => `[${scriptName}] ${articleCollection.length} initial articles`);
// Get the first few articles in the feed when the page loads, and add the author.
articleCollection.forEach(article => {
if (!isCredited(article)) {
creditAuthor(article);
markCredited(article);
}
});
// Watch the page for new articles that appear when scrolling.
const dynamicScroll = new MutationObserver(mutations => {
debugLog(() => `[${scriptName}] ${mutations.length} new mutation objects`);
for (let mutation of mutations) {
const newArticleArray = Array.from(mutation.addedNodes).filter(node => node.nodeName === "ARTICLE");
debugLog(() => `[${scriptName}] ${newArticleArray.length} new articles`);
if (newArticleArray.length == 0) continue;
// Add the author to the new articles as they appear.
newArticleArray.forEach(article => {
if (!isCredited(article)) {
creditAuthor(article);
markCredited(article);
}
});
}
});
dynamicScroll.observe(feed, { childList: true, subtree: false, attributes: false, characterData: false });
// Put the author of a single article on the top line, next to the subreddit and post age.
function creditAuthor(article) {
const post = article.querySelector(POST);
const creditBar = post.querySelector("[id*='credit-bar']");
const separator = creditBar.querySelector(".created-separator");
creditBar.appendChild(separator.cloneNode(/*deep = */true));
const byLineClass = separator.nextElementSibling.getAttribute("class");
const byLine = document.createElement("span");
creditBar.appendChild(byLine).setAttribute("class", byLineClass);
const author = post.getAttribute("author");
if (author === "[deleted]") {
byLine.innerHTML = `by ${author}`;
} else {
byLine.innerHTML = `by <a href="/u/${author}">${author}</a>`;
}
}
// When scrolling down far enough, Reddit unloads posts from the top of the page,
// presumably to save memory. In general, Reddit unloads posts you scroll away from
// and loads or reloads posts you scroll toward. Without checking to make sure the
// post hasn't already been credited, this can cause this script to credit the post
// multiple times when the MutationObserver notices a credited post reappear in the
// feed.
//
// Therefore, check first.
function isCredited(article) {
const post = article.querySelector(POST);
debugLog(() => `[${scriptName}] credited check: ${post.hasAttribute(CREDITED)}`);
return post.hasAttribute(CREDITED);
}
// Marks a post as already credited.
function markCredited(article) {
const post = article.querySelector(POST);
post.setAttribute(CREDITED, "");
}
}
//== This is the part of the script that deals with the PITA way Reddit does scripted navigation. ==//
// Detect feed page on initial load or subsequent XHR/AJAX load by checking the view-context attribute
// of the first post.
//
// This script can run on any page that's designated by Reddit as an "aggregate feed," meaning a feed
// with posts taken from more than one subreddit. Examples would be r/popular, r/all, /new/, etc.
function runScriptIfNecessary() {
const isAggregate = document.querySelector(POST)?.getAttribute(VIEW_CONTEXT) == AGGREGATE_FEED;
debugLog(() => `[${scriptName}] This page ${isAggregate ? "contains" : "does not contain"} an aggregate feed`);
if (isAggregate) {
runScript();
}
}
runScriptIfNecessary();
// Run the script when using the site nav (fake AJAX navigation) to get to the feed. This apparently
// can be bouncy and fire multiple times, and lead to repeated console debug messages, but this can
// be ignored since, before modifying any post's DOM, we always check if we've done it already.
new MutationObserver(() => {
const title = document.querySelector("title").textContent.trim();
debugLog(() => `[${scriptName}] Page <title> change detected (indicating navigation to a new page);\n` +
`[${scriptName}] <title> is now "${title}"`);
runScriptIfNecessary();
}).observe(document.querySelector("title"), { childList: true });
//-- script ends here --//
})();