您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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
当前为
// ==UserScript== // @name AO3: [Wrangling] Mark Illegal Characters in Canonicals // @namespace https://greasyfork.org/en/users/906106-escctrl // @version 1.4 // @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) ? "space in front" : (val === " " && ix == illegalChars.length-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 = 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 = 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 = 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; // initial check only if the tag is already canonical if (tagCanonical.checked) { var checkNode = document.getElementById("tag_name"); var 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) => { var 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) => { var 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) => { var issues = hasIllegalChars(n.innerText); if (issues !== null) insertHeadsUp(issues, n.parentNode); }); } }