- // ==UserScript==
- // @name AO3: [Wrangling] Mark Illegal Characters in Canonicals
- // @namespace https://greasyfork.org/en/users/906106-escctrl
- // @version 1.6
- // @description Warns about any canonical tag that includes characters which should, per guidelines, be avoided. Checks on new tag, edit tag, search results, wrangle bins, and tag landing pages
- // @author escctrl
- // @match *://*.archiveofourown.org/tags/*
- // @license MIT
- // @grant none
- // ==/UserScript==
-
- (function() {
- 'use strict';
-
- // we wanna check on a bunch of different pages, and everywhere the check is slightly different
-
- var page_url = window.location.pathname;
- // just in case the URL ended with a / we get rid of that
- // that usually doesn't happen from AO3 links on the site, but may be how browsers store bookmarks or history
- if (page_url.endsWith("/")) { page_url = page_url.slice(0, page_url.length-1); }
-
- if (page_url == "/tags/new") checkAsYouType(); // New Tag page
- else if (page_url == "/tags/search") checkSearchResults(); // Tag Search page
- else if (page_url.match(/^\/tags\/.+\/edit$/gi)) checkEditTag(); // Edit page
- else if (page_url.match(/^\/tags\/.+\/wrangle$/gi)) checkBinTags(); // Wrangle page
- else if (page_url.match(/^\/tags\/[^\/]+$/gi)) checkTag(); // Tag Landing page
- // that excludes anything including another slash, which would only incorrectly match on tags/new and tags/search
- // but those would have already jumped into the other functions and would never get here
- })();
-
- // *************** GENERAL FUNCTIONS ***************
-
- // a holistic function to check
- // not allowed: non-latin (including accented) characters and special chars (with a few exceptions)
- // two apostrophes '' (used instead of a quote ")
- // space at the beginning or end of the string
- // multiple spaces after each other
- // this returns the matched characters in an array
- function hasIllegalChars(string) {
- return string.match(/[^\p{Script=Latin}0-9 \-().&/'"|:!]|'{2,}| {2,}|^ | $/gui);
- }
-
- // similar to above, but in fandoms we allow letters, numbers and tone/accent marks of ANY script, not just Latin
- // also more special characters are allowed
- function hasFandomIllegalChars(string) {
- return string.match(/[^\p{L}\p{M}\p{N} \-().&/'"|:!#?_]|'{2,}| {2,}|^ | $/gui);
- }
-
- // print a box to explain the problem
- function insertHeadsUp(illegalChars, refNode, befNode = null, inline = false) {
- // describe non-printable chars and other hard to identify issues
- illegalChars.forEach((val, ix) => {
- if (val == "''") illegalChars[ix] = "2 single quotes";
- else if (val.trim() == "")
- illegalChars[ix] = (val == "\t") ? "tab" :
- (val === " " && ix == 0 && refNode.childNodes[0].value.slice(0, 1) === " ") ? "space in front" :
- (val === " " && refNode.childNodes[0].value.slice(-1) === " ") ? "space at end" :
- "multiple spaces";
- });
- // setting up the div to contain the heads-up to the user
- const warningNode = document.createElement("div");
- warningNode.id = "illegalChars";
- warningNode.classList.add("notice");
-
- warningNode.innerHTML = "<p>Questionable characters: " + illegalChars.join(", ") + "</p>";
-
- if (inline) {
- warningNode.style.display = "inline-block";
- warningNode.style.padding = "0";
- warningNode.style.margin = "0.1em 0.1em 0.1em 0.5em";
- warningNode.children[0].style.padding = "0.1em 0.3em";
- warningNode.children[0].style.fontWeight = "normal";
- }
-
- // if that already exists, we're gonna replace it rather than add more divs
- if (refNode.querySelector("#illegalChars")) refNode.replaceChild(warningNode, refNode.querySelector("#illegalChars"));
- else refNode.insertBefore(warningNode, befNode);
- }
-
- // remove the explain box again
- function removeHeadsUp(refNode) {
- if (refNode.querySelector("#illegalChars")) refNode.removeChild(refNode.querySelector("#illegalChars"));
- }
-
- // *************** PAGE HANDLING FUNCTIONS ***************
-
- // New tag page
- function checkAsYouType() {
- // a little JS magic to quickly add the same event listener to all elements
- [ document.getElementById("tag_name"),
- document.getElementById('tag_type_fandom'),
- document.getElementById('tag_type_character'),
- document.getElementById('tag_type_relationship'),
- document.getElementById('tag_type_freeform')
- ].forEach((el) => {
- el.addEventListener("input", () => {
- var checkNode = document.getElementById("tag_name");
-
- // which tag type are you trying to create? fandom or anything else?
- const isFandom = document.getElementById('tag_type_fandom').checked;
- var issues = (isFandom) ? hasFandomIllegalChars(checkNode.value) : hasIllegalChars(checkNode.value);
- if (issues !== null) insertHeadsUp(issues, checkNode.parentNode);
- else removeHeadsUp(checkNode.parentNode);
-
- // extra special handling: tag length>100 error
- const refNode = checkNode.parentNode;
- if (checkNode.value.length > 100) {
- const errorNode = document.createElement("div");
- errorNode.id = "tooLong";
- errorNode.classList.add("error");
- errorNode.innerHTML = "<p>Sorry, you'll need to trim this down. You're at "+ checkNode.value.length +" characters!</p>";
-
- // if that already exists, we're gonna replace it rather than add more divs
- if (refNode.querySelector("#tooLong")) refNode.replaceChild(errorNode, refNode.querySelector("#tooLong"));
- else refNode.insertBefore(errorNode, null);
- }
- else if (refNode.querySelector("#tooLong")) refNode.removeChild(refNode.querySelector("#tooLong"));
- });
- });
- // on page load, trigger event once. browser remembers previous form selections/input upon page refresh and box would otherwise not appear until another change is made
- document.getElementById("tag_name").dispatchEvent(new Event("input"));
- }
-
- // Landing page
- function checkTag() {
- // only if the viewed tags is canonical
- var tagDescr = document.querySelector(".tag>p").innerText;
- if (tagDescr.indexOf("It's a common tag") < 0) return true;
-
- // first the viewed tag itself
- var checkNode = document.querySelector(".tag .header h2.heading");
- var tagType = tagDescr.match(/This tag belongs to the (.+) Category/i);
- tagType = tagType[1];
- var issues = (tagType == "Fandom") ? hasFandomIllegalChars(checkNode.innerText) : hasIllegalChars(checkNode.innerText);
- if (issues !== null) insertHeadsUp(issues, checkNode.parentNode.parentNode, checkNode.parentNode.parentNode.children[1]);
-
- // then the meta and subtags (if any)
- checkNode = document.querySelectorAll("div.meta.listbox a.tag, div.sub.listbox a.tag");
- checkNode.forEach((n) => {
- var issues = (tagType == "Fandom") ? hasFandomIllegalChars(n.innerText) : hasIllegalChars(n.innerText);
- if (issues !== null) insertHeadsUp(issues, n.parentNode, n.parentNode.children[1], true);
- });
- // it would be really cool if we could check Parent Tags as well, but we can't tell which of those are fandoms vs. anything else
- }
-
- // Wrangle Bin Page
- // sadly we can't tell here at all if we're ever looking at fandoms
- function checkBinTags() {
- // this needs a different approach to the logic:
- // don't check show=mergers at all, too repetitive
- var searchParams = new URLSearchParams(window.location.search);
- if (searchParams.get('show') == "mergers") return true;
-
- // create a key -> value pair Map of the table columns, so we know which column to check
- var tableIndexes = new Map();
- document.querySelectorAll("#wrangulator table thead th").forEach((th, ix) => {
- tableIndexes.set(th.innerText, ix);
- });
-
- // now we can loop through the list of tags
- var issues, checkNode;
- var checkRows = document.querySelectorAll("#wrangulator table tbody tr");
- checkRows.forEach((r) => {
- // if there's a column "Canonical" and the cell says "Yes" then we check the tag itself
- if (tableIndexes.has("Canonical") && r.cells[tableIndexes.get("Canonical")].innerText == "Yes") {
- checkNode = r.cells[0].querySelector("label");
- issues = searchParams.get('show') == "fandoms" ? hasFandomIllegalChars(checkNode.innerText) : hasIllegalChars(checkNode.innerText);
- if (issues !== null) insertHeadsUp(issues, checkNode.parentNode);
- }
-
- // if there's a column "Synonym", we check the content of that cell (there'll only be one tag)
- if (tableIndexes.has("Synonym") && r.cells[tableIndexes.get("Synonym")].innerText.trim() !== "") {
- checkNode = r.cells[tableIndexes.get("Synonym")].querySelector("a");
- issues = searchParams.get('show') == "fandoms" ? hasFandomIllegalChars(checkNode.innerText) : hasIllegalChars(checkNode.innerText);
- if (issues !== null) insertHeadsUp(issues, checkNode.parentNode);
- }
-
- // if there's a column "Characters", we check the content of that cell (there might be multiple tags)
- if (tableIndexes.has("Characters") && r.cells[tableIndexes.get("Characters")].innerText.trim() !== "") {
- checkNode = r.cells[tableIndexes.get("Characters")].querySelectorAll("a");
- checkNode.forEach((n) => {
- issues = hasIllegalChars(n.innerText);
- if (issues !== null) insertHeadsUp(issues, n.parentNode);
- });
- }
-
- // if there's a column "Metatag", we check the content of that cell (there might be multiple tags)
- if (tableIndexes.has("Metatag") && r.cells[tableIndexes.get("Metatag")].innerText.trim() !== "") {
- checkNode = r.cells[tableIndexes.get("Metatag")].querySelectorAll("a");
- checkNode.forEach((n) => {
- issues = searchParams.get('show') == "fandoms" ? hasFandomIllegalChars(checkNode.innerText) : hasIllegalChars(n.innerText);
- if (issues !== null) insertHeadsUp(issues, n.parentNode);
- });
- }
- });
- }
-
- // Tag Search
- function checkSearchResults() {
- // with search results table userscript enabled
- var checkNodes = document.querySelectorAll("table#resulttable .resulttag.canonical a");
- checkNodes.forEach((n) => {
- var issues = (n.parentNode.parentNode.querySelector('td.resulttype').title == "Fandom") ? hasFandomIllegalChars(n.innerText) : hasIllegalChars(n.innerText);
- if (issues !== null) insertHeadsUp(issues, n.parentNode, null, true);
- });
-
- // with plain search results page
- checkNodes = document.querySelectorAll("ol.tag li span.canonical a.tag");
- checkNodes.forEach((n) => {
- var issues = (n.parentNode.firstChild.textContent.trim() == "Fandom:") ? hasFandomIllegalChars(n.innerText) : hasIllegalChars(n.innerText);
- if (issues !== null) insertHeadsUp(issues, n.parentNode.parentNode, null, true);
- });
- }
-
- // Edit Tag Page
- function checkEditTag() {
- const tagCanonical = document.getElementById('tag_canonical');
- const tagType = document.querySelector('#edit_tag fieldset:first-of-type dd strong').innerText;
- var issues;
-
- // initial check only if the tag is already canonical
- if (tagCanonical.checked) {
- var checkNode = document.getElementById("tag_name");
- issues = (tagType == "Fandom") ? hasFandomIllegalChars(checkNode.value) : hasIllegalChars(checkNode.value);
- if (issues !== null) insertHeadsUp(issues, checkNode.parentNode);
- }
-
- // if the tag's canonical status is changed
- tagCanonical.addEventListener("input", (event) => {
- var checkNode = document.getElementById("tag_name");
- if (event.target.checked) {
- var issues = (tagType == "Fandom") ? hasFandomIllegalChars(checkNode.value) : hasIllegalChars(checkNode.value);
- if (issues !== null) insertHeadsUp(issues, checkNode.parentNode);
- else removeHeadsUp(checkNode.parentNode);
- }
- else removeHeadsUp(checkNode.parentNode);
- });
-
- // if this is a synonym, check the canonical tag it's synned to
- const synonym = document.querySelector('#edit_tag fieldset:first-of-type dd ul.autocomplete .added.tag');
- if (synonym !== null) {
- issues = (tagType == "Fandom") ? hasFandomIllegalChars(synonym.firstChild.textContent.trim()) : hasIllegalChars(synonym.firstChild.textContent.trim());
- if (issues !== null) insertHeadsUp(issues, synonym.parentNode.parentNode, synonym.parentNode.parentNode.children[1]);
- }
-
- // if this is canonical, check its sub- and metatags
- const metasubs = document.querySelectorAll('#parent_MetaTag_associations_to_remove_checkboxes ul li a, #child_SubTag_associations_to_remove_checkboxes ul li a');
- if (metasubs !== null) {
- metasubs.forEach((n) => {
- issues = (tagType == "Fandom") ? hasFandomIllegalChars(n.innerText) : hasIllegalChars(n.innerText);
- if (issues !== null) insertHeadsUp(issues, n.parentNode);
- });
- }
-
- // if this is any other type of tag that's in a fandom, check the fandom tag
- const fandoms = document.querySelectorAll('#parent_Fandom_associations_to_remove_checkboxes ul li a');
- if (fandoms !== null) {
- fandoms.forEach((n) => {
- issues = hasFandomIllegalChars(n.innerText);
- if (issues !== null) insertHeadsUp(issues, n.parentNode);
- });
- }
-
- // if this is a relationship, check the tagged characters
- const chars = document.querySelectorAll('#parent_Character_associations_to_remove_checkboxes ul li a');
- if (chars !== null) {
- chars.forEach((n) => {
- issues = hasIllegalChars(n.innerText);
- if (issues !== null) insertHeadsUp(issues, n.parentNode);
- });
- }
- }