MeFi Navigator Redux

MetaFilter: navigate through users' comments, and highlight comments by OP and yourself

目前为 2025-05-02 提交的版本。查看 最新版本

// ==UserScript==
// @name         MeFi Navigator Redux
// @namespace    https://github.com/klipspringr/mefi-userscripts
// @version      2025-05-02
// @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>`;

// CSS notes:
// - mfnr-op needs to play nicely with .mod in threads where OP is a mod
// - classic theme has different margins from modern, so we can't change margin-left without knowing what theme we're on
// - relative positioning seems to work better
const CLASSES = `<style>
.mfnr-op {
    border-left: 5px solid #0004 !important;
    padding-left: 10px !important;
    position: relative !important;
    left: -15px !important;
}
@media (max-width: 550px) {
    .mfnr-op {
        left: -5px !important;
    }
}
.mfnr-self-badge {
    background-color: #C8E0A1;
    border-radius: 2px;
    color: #000;
    font-size: 0.8em;
    margin-left: 4px;
    padding: 0 4px;
    cursor: default;
}
.mfnr-nav {
    white-space: nowrap;
}
.mfnr-nav svg {
    vertical-align: middle;
    top: -1px;
}
</style>`;

const ATTR_BYLINE = "data-mfnr-byline";

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.classList.add("mfnr-self-badge");
    span.textContent = "me";
    targetNode.after(span);
};

const markOP = (targetNode) =>
    targetNode.parentElement.parentElement.classList.add("mfnr-op");

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("class", "mfnr-nav");
    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("class", "mfnr-nav");

    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 = (subsite, self, firstRun) => {
    const start = performance.now();

    const opHighlight = subsite !== "ask" && subsite !== "projects"; // don't highlight OP on subsites with this built in

    // if not first run, remove any existing navigators (from both post and comments)
    if (!firstRun)
        document.querySelectorAll("span.mfnr-nav").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.trim();

    // 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.trim();

        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);
    document.body.insertAdjacentHTML("beforeend", CLASSES);

    const subsite = window.location.hostname.split(".", 1)[0];
    const self = getCookie("USER_NAME");

    const newCommentsElement = document.getElementById("newcomments");
    if (newCommentsElement) {
        const observer = new MutationObserver(() => run(subsite, self, false));
        observer.observe(newCommentsElement, { childList: true });
    }

    run(subsite, self, true);
})();

QingJ © 2025

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