- // ==UserScript==
- // @name MeFi Navigator Redux
- // @namespace https://github.com/klipspringr/mefi-userscripts
- // @version 2025-04-10
- // @description MetaFilter: navigate through users' comments, and highlight comments by OP and yourself
- // @author Klipspringer
- // @supportURL https://github.com/klipspringr/mefi-userscripts
- // @license MIT
- // @match *://*.metafilter.com/*
- // @grant none
- // ==/UserScript==
-
- const SVG_UP = `<svg xmlns="http://www.w3.org/2000/svg" hidden style="display:none"><path id="mfnr-up" fill="currentColor" d="M 0 93.339 L 50 6.661 L 100 93.339 L 50 64.399 L 0 93.339 Z" /></svg>`;
- const SVG_DOWN = `<svg xmlns="http://www.w3.org/2000/svg" hidden style="display:none"><path id="mfnr-down" fill="currentColor" d="M 100 6.69 L 50 93.31 L 0 6.69 L 50 35.607 L 100 6.69 Z" /></svg>`;
-
- const ATTR_BYLINE = "data-mfnr-byline";
- const ATTR_NAVIGATOR = "data-mfnr-nav";
-
- const getCookie = (key) => {
- const s = RegExp(key + "=([^;]+)").exec(document.cookie);
- if (!s || !s[1]) return null;
- return decodeURIComponent(s[1]);
- };
-
- const markSelf = (targetNode) => {
- const span = document.createElement("span");
- span.style["margin-left"] = "4px";
- span.style["padding"] = "0 4px";
- span.style["border-radius"] = "2px";
- span.style["background-color"] = "#C8E0A1";
- span.style["color"] = "#000";
- span.style["font-size"] = "0.8em";
- span.textContent = "me";
- targetNode.after(span);
- };
-
- const markOP = (targetNode) => {
- const wrapper = targetNode.parentNode.parentNode;
- wrapper.style["border-left"] = "5px solid #0004";
- wrapper.style["padding-left"] = "10px";
- };
-
- const createLink = (href, svgHref) => {
- const a = document.createElement("a");
- a.setAttribute("href", "#" + href);
- const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
- svg.setAttribute("width", "1em");
- svg.setAttribute("viewBox", "0 0 100 100");
- svg.setAttribute("style", "vertical-align: middle; top: -1px;");
- const use = document.createElementNS("http://www.w3.org/2000/svg", "use");
- use.setAttribute("href", "#" + svgHref);
- svg.appendChild(use);
- a.appendChild(svg);
- return a;
- };
-
- const processByline = (
- bylineNode,
- user,
- anchor,
- anchors,
- firstRun,
- self = null,
- op = null
- ) => {
- // don't mark self or OP more than once
- if (firstRun || !bylineNode.hasAttribute(ATTR_BYLINE)) {
- if (self !== null && user === self) markSelf(bylineNode);
- if (op !== null && user === op) markOP(bylineNode);
- bylineNode.setAttribute(ATTR_BYLINE, "");
- }
-
- if (anchors.length <= 1) return;
-
- const i = anchors.indexOf(anchor);
- const previous = anchors[i - 1];
- const next = anchors[i + 1];
-
- const navigator = document.createElement("span");
- navigator.setAttribute(ATTR_NAVIGATOR, "");
-
- const nodes = ["["];
- if (previous) nodes.push(createLink(previous, "mfnr-up"));
- nodes.push(anchors.length);
- if (next) nodes.push(createLink(next, "mfnr-down"));
- nodes.push("]");
-
- navigator.append(...nodes);
- bylineNode.parentElement.appendChild(navigator);
- };
-
- const run = (firstRun = false) => {
- const start = performance.now();
-
- const subsite = window.location.hostname.split(".")[0];
- const opHighlight = subsite !== "ask" && subsite !== "projects"; // don't highlight OP on subsites with this built in
- const self = getCookie("USER_NAME");
-
- // if not first run, remove any existing navigators (from both post and comments)
- if (!firstRun) {
- document
- .querySelectorAll(`span[${ATTR_NAVIGATOR}]`)
- .forEach((n) => n.remove());
- }
-
- // post node
- // tested on all subsites, modern and classic, 2025-04-10
- const postNode = document.querySelector(
- "div.copy > span.smallcopy > a:first-child"
- );
- const op = postNode.textContent;
-
- // initialise with post
- const bylines = [[op, "top"]];
- const mapUsersAnchors = new Map([[op, ["top"]]]);
-
- // comment nodes, excluding live preview
- // tested on all subsites, modern and classic, 2025-04-10
- const commentNodes = document.querySelectorAll(
- "div.comments:not(#commentform *) > span.smallcopy > a:first-child"
- );
-
- for (const node of commentNodes) {
- const user = node.textContent;
-
- const anchorElement =
- node.parentElement.parentElement.previousElementSibling;
- const anchor = anchorElement.getAttribute("name");
-
- bylines.push([user, anchor]);
-
- const anchors = mapUsersAnchors.get(user) ?? [];
- mapUsersAnchors.set(user, anchors.concat(anchor));
- }
-
- for (const [i, bylineNode] of [postNode, ...commentNodes].entries()) {
- processByline(
- bylineNode,
- bylines[i][0],
- bylines[i][1],
- mapUsersAnchors.get(bylines[i][0]),
- firstRun,
- self,
- opHighlight && i > 0 ? op : null
- );
- }
-
- console.log(
- "mefi-navigator-redux",
- firstRun ? "first-run" : "new-comments",
- 1 + commentNodes.length,
- Math.round(performance.now() - start) + "ms"
- );
- };
-
- (() => {
- if (
- !/^\/(\d|comments\.mefi)/.test(window.location.pathname) ||
- /rss$/.test(window.location.pathname)
- )
- return;
-
- document.body.insertAdjacentHTML("beforeend", [SVG_UP, SVG_DOWN].join(""));
-
- const newCommentsElement = document.getElementById("newcomments");
- if (newCommentsElement) {
- const observer = new MutationObserver(() => run(false));
- observer.observe(newCommentsElement, { childList: true });
- }
-
- run(true);
- })();