AO3: [Wrangling] Find the Tag in the Blurb

Make the tag of the page you're on (and up to 300 synonymous/meta/sub/parent/child tags) more visually distinct

// ==UserScript==
// @name        AO3: [Wrangling] Find the Tag in the Blurb
// @namespace   https://github.com/RhineCloud
// @version     1.2
// @description Make the tag of the page you're on (and up to 300 synonymous/meta/sub/parent/child tags) more visually distinct
// @grant       none
// @author      Rhine
// @include     /^https?:\/\/[^\/]*archiveofourown.org\/(works|bookmarks)\?.*tag_id=.+/
// @include     /^https?:\/\/[^\/]*archiveofourown.org\/tags\/[^\/]+(\/((works|bookmarks).*)?)?$/
// @exclude     /^https?:\/\/[^\/]*archiveofourown.org\/tags\/[^\/]+\/(edit|wrangle|comments|troubleshooting).*/
// @license     GPL-3.0 <https://www.gnu.org/licenses/gpl.html>
// ==/UserScript==


// CSS SETTINGS
// DO NOT change the first string in each line
// adjust the second string in each line as you like, any valid CSS works
// delete the entire line if you are not ever at all interested in having a specific kind of related tag highlighted
const STYLE_MAP = new Map([
    ["", "color: #fff; background: #900;"],
    ["synonym", "color: #fff; background: rgba(153, 0, 0, 75%);"],
    ["meta", "color: #fff; text-shadow: 0 0 5px #900;"],
    ["sub", "color: #fff; background: #c00;"],
    ["parent", "color: #fff; text-shadow: 0 0 5px #099; font-style: oblique;"],
    ["child", "color: #fff; background: #090; font-weight: bold;"]
]);

// CACHE SETTINGS
// set these to either true or false
// automatically cache related tags
const AUTO_CACHE_RELATED = false
// show the "Cache related tags" button
const SHOW_CACHE_BUTTON = true


// MAIN FUNCTION
// find matching tags in blurbs and add styling
const PATHNAME = window.location.pathname;
let tag_id = PATHNAME.startsWith("/tags/") ? PATHNAME.split("/")[2] :
    document.querySelector("#main h2.heading a.tag").getAttribute("href").split("/")[2];
style_tag(tag_id, STYLE_MAP.get(""));

if (1 < STYLE_MAP.size) {
    // find corresponding tag canonical if on tags landing page of a synonym
    if (document.querySelector("div.merger.module")) {
        tag_id = document.querySelector("div.merger.module a.tag").getAttribute("href").split("/")[2];
    }

    // automatically cache related tags if applicable
    if (AUTO_CACHE_RELATED && sessionStorage.getItem("cached_tag") !== tag_id) {
        cache_related_tags().then(() => {});
    }

    // find further related tags if applicable
    const button = document.createElement("li");
    if (sessionStorage.getItem("cached_tag") === tag_id) {
        button.innerHTML = `<span class="current">Cached related tags!</span>`;
        style_related_tags();
    } else {
        button.setAttribute("id", "cache_related_tags");
        button.innerHTML = "<a>Cache related tags</a>";
        button.addEventListener("click", cache_related_tags);
    }
    if (SHOW_CACHE_BUTTON) {
        document.querySelector("#main ul.navigation.actions").prepend(button);
    }
}


// HELPER FUNCTIONS
// apply styling to appearances of a specific tag in all blurbs
function style_tag(tag_id, style) {
    document.querySelectorAll(`li.blurb a.tag[href="/tags/${tag_id}/works"], li.blurb a.tag[href="/tags/${tag_id}/bookmarks"]`)
        .forEach(tag => tag.setAttribute("style", style));
}

// use sessionStorage to effectively cache info on related tags (per window/tab)
// and apply styling on related tags once they're cached
async function cache_related_tags() {
    const tag_home = 3 === PATHNAME.split("/").length ? document.querySelector("#main div.tag.home") :
        await fetch(`/tags/${tag_id}`).then(response => response.text()).then(html => {
            const parser = new DOMParser();
            const doc = parser.parseFromString(html, "text/html");
            return doc.querySelector("#main div.tag.home");
        });

    for (let type of STYLE_MAP.keys()) {
        if (type) {
            cache_relation(type, tag_home);
        }
    }
    sessionStorage.setItem("cached_tag", tag_id);

    if (SHOW_CACHE_BUTTON) {
        document.querySelector("#cache_related_tags").innerHTML = `<span class="current">Cached related tags!</span>`;
        document.querySelector("#cache_related_tags").removeEventListener("click", cache_related_tags);
    }
    style_related_tags();
}

// cache one type of relation in sessionStorage
function cache_relation(type, tag_home) {
    let tags = tag_home.querySelectorAll(`div.${type} a.tag`);
    tags = nodes_to_strings(tags);
    sessionStorage.setItem(`cached_${type}_tags`, tags.join(","));
}

// transform list of nodes (linked tags from the page) into plain strings for sessionStorage
function nodes_to_strings(node_list) {
    const strings = [];
    for (let node of node_list.values()) {
        strings.push(node.getAttribute("href").split("/")[2]);
    }
    return strings;
}

// apply styling to all tags of all relation types in all blurbs
function style_related_tags() {
    for (let type of STYLE_MAP.keys()) {
        if (type) {
            sessionStorage.getItem(`cached_${type}_tags`).split(",")
                .forEach(tag => style_tag(tag, STYLE_MAP.get(type)));
        }
    }
}