AO3: [Wrangling] Search my canonicals for illegal characters

automatically runs a fandom-specific tag search over all your assigned fandoms, to find any canonicals with 'illegal' characters such as curly quotes or Chinese pipes

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         AO3: [Wrangling] Search my canonicals for illegal characters
// @namespace    https://greasyfork.org/en/users/906106-escctrl
// @version      3.0
// @description  automatically runs a fandom-specific tag search over all your assigned fandoms, to find any canonicals with 'illegal' characters such as curly quotes or Chinese pipes
// @author       escctrl
// @match        https://archiveofourown.org/tags/search
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const COPY_SEARCH_STRING = false;

    const DEBUG = false;

    var found = 0; // counting found results

    var node_ul = document.getElementById('wranglerbuttons') || document.querySelector('#main ul.navigation.actions');
    const node_li = document.createElement('li');

    node_li.id = 'checkillegal';
    node_li.className = 'reindex';
    node_li.innerHTML = "<a href='#'>Check Illegal Characters</a>";
    if (COPY_SEARCH_STRING) node_li.addEventListener("click", copySearchString);
    else node_li.addEventListener("click", startBackgroundCheck);
    node_ul.appendChild(node_li);

    function copySearchString() {
        document.getElementById('tag_search_name').value = "*’* OR *‘* OR *“* OR *”* OR *|* OR *–* OR *—* OR *―* OR *(* OR *)* OR *\\'\\'*";
        var tagtype = document.getElementById('tag_search_status');
        if (tagtype !== null) tagtype.value = 'T'; // if the Smaller Tag Search script is enabled
        else document.getElementById('tag_search_canonical_t').checked = true; // for the plain New Tag Search form
    }

    function startBackgroundCheck() {
        // who am I logged in as?
        const user = document.querySelector('#greeting ul.user.navigation li.dropdown>a.dropdown-toggle[href*="/users/"]').href.match(/\/([A-Za-z0-9_]+)\/?$/i)[1];
        if (DEBUG) console.log("Illegal Characters: Logged in as user "+user);

        window.ao3jail = false;

        // get a list of all my wrangled fandoms
        pageload('https://archiveofourown.org/tag_wranglers/'+user, 'fandoms');
    }

    function retrieveFandomList(me) {
        // find all the assigned fandoms in the table (URL and name)
        const fandomlist = me.querySelectorAll('table tbody tr th a');

        // add a tracker for how many open XHR there will be
        window.openXHR = fandomlist.length;
        if (DEBUG) console.log("Illegal Characters: There are "+window.openXHR+" fandoms assigned, to be checked");
        document.getElementById('checkillegal').firstChild.innerText = "Checking " + window.openXHR + " fandoms";

        fandomlist.forEach( (f, i) => {
            var url = 'https://archiveofourown.org/tags/search?tag_search%5Bname%5D=*%E2%80%99*+OR+*%E2%80%98*+OR+*%E2%80%9C*+OR+*%E2%80%9D*+OR+*%EF%BD%9C*+OR+*%E2%80%93*+OR+*%E2%80%94*+OR+*%E2%80%95*+OR+*%EF%BC%88*+OR+*%EF%BC%89*+OR+*%5C%27%5C%27*&tag_search%5Btype%5D=&tag_search%5Bcanonical%5D=T&tag_search%5Bsort_column%5D=name&tag_search%5Bsort_direction%5D=asc&commit=Search+Tags&tag_search%5Bfandoms%5D=';
            url += encodeURIComponent(f.innerText);

            if (DEBUG) console.log("Illegal Characters: Fandom "+f.innerText+" will be checked in "+3000*i+" second");
            if (DEBUG) url = 'https://archiveofourown.org/tags/search?tag_search%5Bname%5D=*POV*&tag_search%5Btype%5D=&tag_search%5Bcanonical%5D=T&tag_search%5Bsort_column%5D=name&tag_search%5Bsort_direction%5D=asc&commit=Search+Tags&tag_search%5Bfandoms%5D='+ encodeURIComponent(f.innerText);

            // collect the search results for specifically those fandoms
            setTimeout(function() {
                pageload(url, 'search');
            }, 3000*i);
        });
    }

    function retrieveSearchResults(me) {
        window.openXHR--;
        // avoids updating the button by a slower (working) response after a page load was already jailed
        if (!window.ao3jail) document.getElementById('checkillegal').firstChild.innerText = "Checking " + window.openXHR + " fandoms";

        const results = me.querySelectorAll('ol.tag.index li span');
        const fandom = me.querySelector('#tag_search_fandoms').value;

        if (DEBUG) console.log("Illegal Characters: Fandom "+fandom+" has "+results.length+" tags with illegal characters");
        if (DEBUG) console.log("Illegal Characters: There are "+window.openXHR+" fandoms remaining to be checked");

        // adds the found nodelist to the object
        var printtext = "";
        if (results.length > 0) {
            printtext = '<h4 class="heading">'+fandom+'</h4><ol class="tag index group">';
            found = found + results.length;
            for (let n of results.values()) { printtext += '<li>'+n.outerHTML+'</li>'; }
            printtext += '</ol>';

            const node_parent = document.querySelector('#main');
            node_parent.innerHTML += printtext; // appending at the bottom of the list
        }

        if (window.openXHR == 0) {
            if (DEBUG) console.log("Illegal Characters: Last fandom was checked, tallying up the result tags");
            // this was the last open XHR so we can print the total number found!
            printTotals();
        }
    }

    function printTotals() {
        const heading = document.createElement('h3');
        heading.className = "heading"
        heading.innerText = found+' Found';
        const node_parent = document.querySelector('#new_tag_search');
        node_parent.insertAdjacentElement('afterend', heading);
        if (DEBUG) console.log("Illegal Characters: Script completed.");
        document.getElementById('checkillegal').firstChild.innerText = "Check for illegal characters finished";
    }

    function saySorry(me) {
        // this does not count down the checked fandoms so button can never show as "finished" when there were errors

        // user output only if this is the fist page that failed
        if (window.ao3jail == false) {
            // check if we received a retry-after value in the response
            if (DEBUG) console.log("Illegal Characters: 429 retry-after " + me.getResponseHeader('Retry-After'));
            var timeout = parseInt(me.getResponseHeader('Retry-After') || 0);
            timeout = (timeout > 0) ? ' You can try again in '+ Math.ceil(timeout / 60) +' minutes.' : '';
            document.querySelector('#new_tag_search').outerHTML += "<h3 class='heading'>Sorry, the script stopped because it ran into the "+me.statusText+" error."+ timeout +"</h3>";
            document.getElementById('checkillegal').firstChild.innerText = "Check stopped: error";
        }
        window.ao3jail = true;
        // console output for every page that didn't load properly
        console.log("Illegal Characters: The background page load ran into an issue: "+me.status+" "+me.statusText);
    }

    function pageload(url, what) {
        if (DEBUG) console.log("Illegal Characters: Loading page "+url);
        if (window.ao3jail) {
            if (DEBUG) console.log("Illegal Characters: This page load was skipped due to previously encountered errors.");
            return false;
        }
        const xhr = new XMLHttpRequest();
        xhr.onload = () => {
            if (xhr.status==200 && xhr.response != "") {
                if (what == 'fandoms') retrieveFandomList(xhr.response);
                else retrieveSearchResults(xhr.response);
            }
            else saySorry(xhr);
        };
        xhr.onerror = () => { saySorry(xhr); };
        xhr.open("GET", url);
        xhr.responseType = "document";
        xhr.send();
    }

})();