AO3 Relationship Highlighter

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 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);
        }
    });
})();