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

// ==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();
    }

})();