AO3 Relationship Highlighter

Highlights the ships you’re looking for if they appear in the first two relationship tags.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @author.      Boni
// @name         AO3 Relationship Highlighter
// @namespace    https://example.com/userscripts/ao3-relationship-highlighter
// @version      1.0.1
// @description  Highlights the ships you’re looking for if they appear in the first two relationship tags.
// @match        https://archiveofourown.org/*
// @grant        none
// @run-at       document-end
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    const COLOR1 = "#E69F00"; // Orange (CVD safe)
    const COLOR2 = "#56B4E9"; // Sky blue (CVD safe)

    /** Remove (xxx) and trim */
    function cleanCharName(str) {
        return str.replace(/\(.*?\)/g, '').trim();
    }

    /** Convert text like Megatron/Optimus Prime/(Transformers) → ["Megatron", "Optimus Prime"] */
    function extractCharacters(text) {
        return text
            .split('/')
            .map(t => cleanCharName(t))
            .filter(t => t.length > 0);
    }

    /** Check whether relationshipTagChars contains all headingChars (order not required) */
    function isRelationshipMatch(headingChars, tagChars) {
        if (headingChars.length < 2) return false; // must be a slash pair
        // heading char exists if any tagChar contains it
        return headingChars.every(h =>
                                  tagChars.some(tc => tc.toLowerCase().includes(h.toLowerCase()))
                                 );
    }

    /** Apply highlight */
    function applyHighlight(anchor, color) {
        if (!anchor) return;
        anchor.style.backgroundColor = color;
        anchor.style.color = "black";
        anchor.style.padding = "2px 4px";
        anchor.style.borderRadius = "4px";
        anchor.style.fontWeight = "bold";
    }

    window.addEventListener('load', () => {
        try {
            // ---- 1. GET HEADING TAG ----
            const headingA = document.querySelector('h2.heading a.tag');
            if (!headingA) return;

            const headingText = headingA.innerText.trim();
            const headingChars = extractCharacters(headingText);

            console.info("[AO3-hl] Heading characters:", headingChars);

            // ---- 2. PROCESS EACH ul.tags.commas ----
            const lists = document.querySelectorAll("ul.tags.commas");
            lists.forEach((ul, idx) => {

                const liNodes = Array.from(ul.querySelectorAll("li"));

                // ---- 2a. Skip all warnings ----
                const relTags = liNodes.filter(li => !li.classList.contains("warnings") && li.classList.contains("relationships"));
                if (relTags.length === 0) return;

                // ---- 2b. Grab first two relationship tags after warnings ----
                const firstTwo = relTags.slice(0, 2);

                const checkTag = (li) => {
                    if (!li) return null;
                    const a = li.querySelector("a.tag");
                    if (!a) return null;
                    const txt = a.innerText.trim();
                    if (!txt.includes("/")) return null; // skip non-relationship tags
                    return {
                        anchor: a,
                        chars: extractCharacters(txt),
                        txt
                    };
                };

                const c1 = checkTag(firstTwo[0]);
                const c2 = checkTag(firstTwo[1]);

                console.debug(`[AO3-hl][${idx}] candidate1:`, c1?.chars, "candidate2:", c2?.chars);

                // ---- 3. CHARACTER MATCHING ----
                if (c1 && isRelationshipMatch(headingChars, c1.chars)) {
                    console.info(`[AO3-hl] Match 1:`, c1.txt);
                    applyHighlight(c1.anchor, COLOR1);
                }

                if (c2 && isRelationshipMatch(headingChars, c2.chars)) {
                    console.info(`[AO3-hl] Match 2:`, c2.txt);
                    applyHighlight(c2.anchor, COLOR2);
                }
            });

        } catch (e) {
            console.error("[AO3-hl] Error:", e);
        }
    });
})();