User:Opencooper/highlightStrings.js

// Highlight some errors

// License: CC0

// Attribution for configure icon (MIT license): https://commons.wikimedia.org/wiki/File:OOjs_UI_icon_advanced_apex.svg

// Attribution for report icon (MIT license): https://commons.wikimedia.org/wiki/File:OOjs_UI_icon_feedback-ltr.svg

// TODO: Create a configuration page where rules can be enabled/disabled.

// TODO: Add permalink for current revision

// TODO: test this and other scripts on other skins, including redesign

// TODO: Check out TreeWalker API; maybe useful

//todo:rule to check for empty talk page or talk page without WikiProjects

/*

Principles:

* Focus on formatting, not content (e.g. spelling) – testing out if necessary

* Minimize false positives, and make highlights optional if they are

too noisy

* Preserve original formatting of article if possible

* Try to do something in the DOM first rather than matching the HTML

if it can be done elegantly

  • /

/* jshint esversion: 11 */

/* jshint jquery: true */

/* jshint laxbreak: true */

/* global mw */

/* global document */

/* global window */

/* global navigator */

/* global location */

/* global console */

/* global alert */

/* global CSS */

//

// TODO: compare to https://en.wikipedia.org/wiki/Wikipedia:WikiProject_Check_Wikipedia/List_of_errors

// and https://en.wikipedia.org/wiki/Wikipedia:AutoWikiBrowser/General_fixes

"use strict";

function printError(message, source, lineno, colno, error) {

if (source.includes("highlightStrings") || source.includes("jquery")) {

$("#contentSub").after("

highlightStrings.js: Error: "

+ mw.html.escape(message) + " [line: " + lineno

+ ", column: " + colno + "]

");

addReportButton();

}

return false;

}

// Print warnings that are page breaking

function printExternalWarning(message) {

$("#contentSub").after("

highlightStrings.js: Warning:"

+ mw.html.escape(message) + "");

addReportButton();

}

// Print warnings that are not important enough to show to users

function printInternalWarning(message) {

$("#contentSub").after("

[Internal] "

+ mw.html.escape(message) + "");

}

const matchDescriptions = {};

const searchDescriptions = {};

const filterList = [];

const refSectionsSelector = "#References, #Notes, #Citations, #Bibliography, #Endnotes, #Notes_and_references, #References_and_notes, #Sources, #Works_cited, #General_sources, #General_references, #Footnotes";

let leadMarker;

const stateNames = ["Alabama", "Alaska", "Arizona", "Arkansas", "California", "Colorado", "Connecticut", "Delaware", "Florida", "Georgia", "Hawaii", "Idaho", "Illinois", "Indiana", "Iowa", "Kansas", "Kentucky", "Louisiana", "Maine", "Maryland", "Massachusetts", "Michigan", "Minnesota", "Mississippi", "Missouri", "Montana", "Nebraska", "Nevada", "New Hampshire", "New Jersey", "New Mexico", "New York", "North Carolina", "North Dakota", "Ohio", "Oklahoma", "Oregon", "Pennsylvania", "Rhode Island", "South Carolina", "South Dakota", "Tennessee", "Texas", "Utah", "Vermont", "Virginia", "Washington", "West Virginia", "Wisconsin", "Wyoming", "D.C."];

const countryNames = ["Afghanistan", "Albania", "Algeria", "Andorra", "Angola", "Antigua and Barbuda", "Argentina", "Armenia", "Australia", "Austria", "Azerbaijan", "Bahamas", "Bahrain", "Bangladesh", "Barbados", "Belarus", "Belgium", "Belize", "Benin", "Bhutan", "Bolivia", "Bosnia and Herzegovina", "Botswana", "Brazil", "Brunei", "Bulgaria", "Burkina Faso", "Burma", "Burundi", "Cabo Verde", "Cambodia", "Cameroon", "Canada", "Central African Republic", "CAR", "Chad", "Chile", "China", "Colombia", "Comoros", "Democratic Republic of the Congo", "Republic of the Congo", "Costa Rica", "Cote d'Ivoire", "Croatia", "Cuba", "Cyprus", "Czechia", "Czech Republic", "Denmark", "Djibouti", "Dominica", "Dominican Republic", "Ecuador", "Egypt", "El Salvador", "Equatorial Guinea", "Eritrea", "Estonia", "Eswatini", "Ethiopia", "Fiji", "Finland", "France", "Gabon", "Gambia", "Georgia", "Germany", "Ghana", "Greece", "Grenada", "Guatemala", "Guinea", "Guinea-Bissau", "Guyana", "Haiti", "Honduras", "Hong Kong", "Hungary", "Iceland", "India", "Indonesia", "Iran", "Iraq", "Ireland", "Israel", "Italy", "Jamaica", "Japan", "Jordan", "Kazakhstan", "Kenya", "Kiribati", "Kosovo", "Kuwait", "Kyrgyzstan", "Laos", "Latvia", "Lebanon", "Lesotho", "Liberia", "Libya", "Liechtenstein", "Lithuania", "Luxembourg", "Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", "Marshall Islands", "Mauritania", "Mauritius", "Mexico", "Micronesia", "Moldova", "Monaco", "Mongolia", "Montenegro", "Morocco", "Mozambique", "Myanmar", "Namibia", "Nauru", "Nepal", "Netherlands", "New Zealand", "Nicaragua", "Niger", "Nigeria", "North Korea", "North Macedonia", "Norway", "Oman", "Pakistan", "Palau", "Palestine", "Panama", "Papua New Guinea", "Paraguay", "Peru", "Philippines", "Poland", "Portugal", "Qatar", "Romania", "Russia", "Rwanda", "Saint Kitts and Nevis", "Saint Lucia", "Saint Vincent and the Grenadines", "Samoa", "San Marino", "Sao Tome and Principe", "Saudi Arabia", "Senegal", "Serbia", "Seychelles", "Sierra Leone", "Singapore", "Slovakia", "Slovenia", "Solomon Islands", "Somalia", "South Africa", "South Korea", "South Sudan", "Spain", "Sri Lanka", "Sudan", "Suriname", "Sweden", "Switzerland", "Syria", "Taiwan", "Tajikistan", "Tanzania", "Thailand", "Timor-Leste", "Togo", "Tonga", "Trinidad and Tobago", "Tunisia", "Turkey", "Turkmenistan", "Tuvalu", "Uganda", "Ukraine", "United Arab Emirates", "UAE", "United Kingdom", "UK", "United States of America", "United States", "USA", "Uruguay", "Uzbekistan", "Vanuatu", "Vatican City", "Holy See", "Venezuela", "Vietnam", "Yemen", "Zambia", "Zimbabwe"];

function highlightStrings() {

// TODO: convert this to `window.addEventListener('error', e => {` format

window.onerror = printError;

mw.loader.load("//en.wikipedia.org/w/index.php?title=User:Opencooper/highlightStrings.css&action=raw&ctype=text/css", "text/css");

preClean();

manipulateDOM();

prepHTML();

replaceHTML();

postClean();

displayMatches();

getWikitext();

getItalics();

getDeadInterwikis();

getFreeImages();

tweakDisplay();

}

function addReportButton() {

$(".oHL_error, .oHL_externalWarning").each(function addButton() {

if ($(this).next().is(".oHL_reportButton")) {

return;

}

const error = $(this).text();

const article = mw.config.get("wgTitle");

const currentRevision = mw.config.get("wgRevisionId");

// const latestRevision = mw.config.get("wgCurRevisionId");

const skin = mw.config.get("skin");

const userAgent = navigator.userAgent;

const signature = "~~~~";

const reportLink = "/wiki/User_talk:Opencooper/highlightStrings?action=edit§ion=new&preloadtitle=Bug%20report&preload=User:Opencooper/highlightStringsReportPreload.js"

+ "&preloadparams[]=" + encodeURIComponent(article).replaceAll("'", "%27")

+ "&preloadparams[]=" + currentRevision

+ "&preloadparams[]=" + encodeURIComponent(error).replaceAll("'", "%27")

+ "&preloadparams[]=" + encodeURIComponent(skin).replaceAll("'", "%27")

+ "&preloadparams[]=" + encodeURIComponent(userAgent).replaceAll("'", "%27")

+ "&preloadparams[]=" + signature;

const feedbackIconURL = "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0a/OOjs_UI_icon_feedback-ltr.svg/20px-OOjs_UI_icon_feedback-ltr.svg.png";

$(this).after("");

});

}

// Sanitize to avoid false positives

function preClean() {

// Prefetch

$("head").append("");

// Remove highlight button so we don't run twice

document.getElementById("hStrings")?.remove();

// Link element

document.querySelectorAll("#mw-content-text link")?.forEach(e => e.remove());

// Links

document.querySelectorAll(".Z3988")?.forEach(e => e.remove());

// Workaround for coordinates

document.getElementById("coordinates")?.remove();

document.querySelectorAll("p > .geo-inline-hidden")?.forEach(e => e.parentElement.remove());

document.querySelectorAll(".geo-nondefault, .geo")?.forEach(e => e.remove());

// Rm template for discussion template

document.getElementById("tfd")?.remove();

// Rm redirect notice

document.querySelectorAll(".mw-redirectedfrom")?.forEach(e => e.remove());

// Draft submission box

document.querySelectorAll(".ombox")?.forEach(e => e.remove());

// {{As of}} "[update]"

document.querySelectorAll(".asof-tag")?.forEach(e => e.remove());

// Audio boxes

document.querySelectorAll(".ui-icon-play")?.forEach(e => e.closest(".haudio")?.remove());

document.querySelectorAll("audio")?.forEach(e => e.removeAttribute("data-durationhint"));

// Img srcset

document.querySelectorAll("#mw-content-text img")?.forEach(e => e.removeAttribute("srcset"));

// Video payload

document.querySelectorAll("[videopayload]")?.forEach(e => e.removeAttribute("videopayload"));

// Table sorting

document.querySelectorAll("[data-sort-value]")?.forEach(e => e.removeAttribute("data-sort-value"));

// Empty paragraphs added by the parser

document.querySelectorAll(".mw-empty-elt")?.forEach(e => e.remove());

// Hidden footer

document.querySelectorAll(".printfooter")?.forEach(e => e.remove());

// Maps

document.querySelectorAll(".mw-graph")?.forEach(e => e.removeAttribute("data-graph-id"));

document.querySelectorAll(".mw-kartographer-link")?.forEach(e => e.removeAttribute("data-overlays"));

// MIDI files

document.querySelectorAll(".mw-ext-score")?.forEach(e => e.removeAttribute("data-midi"));

// Infobox wrapping

document.querySelectorAll(".infobox .nowrap")?.forEach(e => e.classList.remove("nowrap"));

// ARIA attributes

document.querySelectorAll("[aria-label]")?.forEach(e => e.removeAttribute("aria-label"));

document.querySelectorAll("[aria-labelledby]")?.forEach(e => e.removeAttribute("aria-labelledby"));

// Talk page section subscription

document.querySelectorAll("[data-mw-thread-id]")?.forEach(e => e.removeAttribute("data-mw-thread-id"));

document.querySelectorAll("[data-mw-comment-end]")?.forEach(e => e.removeAttribute("data-mw-comment-end"));

// Remove userscript-added stuff

document.getElementById("siteSub")?.remove();

// document.getElementById("lastEdit")?.remove(); // Redundant to above

document.getElementById("wikidataDescription")?.remove();

document.getElementById("kanjiInfo")?.remove();

document.getElementById("xtools")?.remove();

document.getElementById("otherImage")?.remove();

// Use one consistent class for all references

document.querySelectorAll(".mw-references-wrap, .reflist, .refbegin")?.forEach(e => e.classList.add("oHL_reflist"));

document.querySelectorAll(".oHL_reflist .oHL_reflist")?.forEach(e => e.classList.remove("oHL_reflist"));

// Add class to section anchors

if (mw.config.get("skin") != "minerva") {

document.querySelectorAll("a[id^='sectiontitlecopy']")?.forEach(e => e.classList.add("oHL_anchorLink"));

} else {

document.querySelectorAll("a[id^='sectiontitlecopy']")?.forEach(e => e.remove());

}

// Sometimes the ToC is wrapped in toclimit-

document.querySelectorAll("[class*='toclimit'] #toc")?.forEach(e => e.parentElement.before(e));

// Remove show/hide toggle for collapsed elements

document.querySelectorAll(".mw-collapsible-toggle")?.forEach(e => e.remove());

// Navboxes contain their own ids which can clash with those on the page,

// causing issues when the HTML is reparsed, such as CSS being switched

document.querySelectorAll(".navbox div[id]")?.forEach(e => e.removeAttribute("id"));

document.querySelectorAll("#mw-content-text a[href^='/wiki/']")?.forEach(e => e.classList.add("oHL_wikilink"));

document.querySelectorAll(".oHL_anchorLink")?.forEach(e => e.classList.remove("oHL_wikilink"));

document.querySelectorAll(".image")?.forEach(e => e.classList.remove("oHL_wikilink"));

document.querySelectorAll(".mw-file-magnify")?.forEach(e => e.classList.remove("oHL_wikilink"));

document.querySelectorAll(".Inline-Template a")?.forEach(e => e.classList.remove("oHL_wikilink"));

// Move TemplateStyles elements to end of document so they don't interfere

// with our rules

$("#bodyContent style, .navbox-styles").appendTo("#bodyContent");

}

function manipulateDOM() {

const isNonDisambigPage = $(".dmbox").length === 0;

const isNonSandboxPage = mw.config.get("wgPageName") != "User:Opencooper/sandbox";

if ($("#toc").length) {

leadMarker = $("#toc");

} else {

leadMarker = $(".mw-heading2").first();

}

// Ref section without list

matchDescriptions["oHL-nonlist-ref"] = ["Non-list ref section", "References sections should contain unordered lists."];

$(refSectionsSelector).each(function findNonlistRefsections() {

const ref = $(this).parent().next();

if ($(ref).prop("tagName") == "P") {

$(ref).prepend("[*] ");

}

});

// Portal in See also

matchDescriptions["oHL-seeAlso-portal"] = ["Portal bar misplacement", "Portal bars should not be placed in the See also section. (MOS:ORDER)"];

if ($(".mw-heading2 h2").last().attr("id") != "See_also") {

$("#See_also").parent().nextUntil(".mw-heading2").filter(".portal-bar").after("[Portal↓]");

}

// Redlinks in see also

matchDescriptions["oHL-seeAlso-redlink"] = ["Red link in See also", "The See also section should not contain red links. (MOS:NOTSEEALSO)"];

$("#See_also").parent().nextUntil(".mw-heading2").filter("ul").find("a.new").addClass("oHL oHL-seeAlso-redlink");

// Title case headers

matchDescriptions["oHL-header-titlecase"] = ["Header case", "Section headers should use title case. (MOS:HEADINGS)"];

$("#See_Also, #External_Links").addClass("oHL oHL-header-titlecase");

// Bolded pseudoheader

matchDescriptions["oHL-pseudoheader"] = ["Pseudoheader", "False headers should not be created using bolding or definition list markup, instead using equal signs. (MOS:PSEUDOHEAD)"];

$("p b:only-child").each(function findPseudoheaders() {

if (this.previousSibling === null && this.nextSibling?.textContent === "\n") {

$(this).addClass("oHL oHL-pseudoheader");

}

});

$("dl dt:only-child").addClass("oHL oHL-pseudoheader");

filterList.push(".navbox .oHL-pseudoheader", ".sidebar .oHL-pseudoheader");

// Unattached inline template

matchDescriptions["oHL-lone-inline"] = ["Unattached inline template", "Inline tags should be preceded by text. (WP:CITEFOOT)"];

$("p .reference:only-child, p .Inline-Template:only-child").each(function findLoneInlines() {

if (this.previousSibling === null && this.nextSibling?.textContent === "\n") {

$(this).addClass("oHL oHL-lone-inline");

}

});

filterList.push("blockquote .oHL-lone-inline");

// Sections without list item

matchDescriptions["oHL-non-list"] = ["Section needing list", "Sections such as See also should contain a bulleted list. (MOS:SEEALSO, MOS:ELLAYOUT)"];

$("#See_also, #External_links").each(function findNonLists() {

const list = $(this).parent().nextUntil(".mw-heading2").filter("ul");

if (list.length === 0) {

$(this).parent().nextUntil(".mw-heading2").filter("p").prepend("[*] ");

}

});

// List items ending with a period

matchDescriptions["oHL-list-period"] = ["List item ending with period", "Sentence fragments in lists should generally not end with a full stop. (MOS:FULLSTOP)"];

const listPeriodMarkup = ".";

$("#See_also, #External_links").parent().nextUntil(".mw-heading2").find("li").each(function findListItemPeriod() {

const elementHtml = $(this).html();

const lastCharacter = elementHtml.slice(-1);

if (lastCharacter == ".") {

const newMarkup = elementHtml.slice(0, -1) + listPeriodMarkup;

$(this).html(newMarkup);

}

});

filterList.push(".navbox .oHL-list-period");

// Missing Commons template

matchDescriptions["oHL-commons-template"] = ["Missing Commons template", "The page has a linked Commons category, but lacks a {{Commons category}} template."];

if ($(".wb-otherproject-commons").length !== 0

&& $("img[src*='Commons-logo.svg']").length === 0) {

$("#mw-content-text .mw-heading2").last().after("

[Needs Commons template]

");

}

// Missing Wikisource template

matchDescriptions["oHL-wikisource-template"] = ["Missing Wikisource template", "The page has a linked Wikisource page, but lacks a {{Wikisource}} template."];

if ($(".wb-otherproject-wikisource").length !== 0

&& $("img[src*='Wikisource-logo.svg']").length === 0) {

$("#mw-content-text .mw-heading2").last().after("

[Needs Wikisource template]

");

}

// Commons template that should be made inline

matchDescriptions["oHL-commons-inline"] = ["Lone block template", "The External links section only has a single template, so it should use an inline equivalent, e.g. `* {{Commons-inline}}`."];

$("#External_links").parent().siblings(".sistersitebox").each(function checkCommonsTemplate() {

const sibling = this.nextElementSibling;

if (sibling === null || sibling.className.includes("navbox")) {

$(this).after("[* Make template inline]");

}

});

// Captions with bolding

matchDescriptions["oHL-bold-caption"] = ["Bolding in caption", "Captions should not be normally specially formatted, including bolded. (MOS:CAPTION)"];

$(".thumbcaption b, .thumbcaption .selflink,"

+ " figcaption b, figcaption .selflink").addClass("oHL oHL-bold-caption");

// Empty captions

matchDescriptions["oHL-missing-caption"] = ["Missing caption", "Images should usually have captions. (MOS:CAPTION)"];

$(".thumbcaption, figcaption, .gallerytext").each(function findEmptyCaptions() {

if ($(this).text() == "") {

$(this).append("[Needs caption]");

$(this).show();

}

});

filterList.push(".listen .side-box-image .oHL-missing-caption", ".navbox .oHL-missing-caption",

".sidebar .oHL-missing-caption", ".infobox .haudio .oHL-missing-caption");

// Wikilinks in bold text

matchDescriptions["oHL-bolded-link"] = ["Bolded wikilink", "Bolded text should not contain wikilinks. (MOS:BOLDLINK)"];

if (isNonDisambigPage) {

$("b .oHL_wikilink").addClass("oHL oHL-bolded-link");

filterList.push(".infobox .oHL-bolded-link", ".sidebar .oHL-bolded-link",

".navbox .oHL-bolded-link", ".succession-box .oHL-bolded-link",

".subjectbar .oHL-bolded-link", ".ambox .oHL-bolded-link",

".side-box .oHL-bolded-link");

}

// Improper header progression

matchDescriptions["oHL-nonlinear-header"] = ["Improper header progression", "Section headers should be nested sequentially. (MOS:BADHEAD)"];

$(".mw-heading2 + .mw-heading4, .mw-heading2 + .mw-heading5,"

+ " .mw-heading2 + .mw-heading6, .mw-heading3 + .mw-heading5,"

+ " .mw-heading3 + .mw-heading6, .mw-heading4 + .mw-heading6").before("

[Non-sequential header level]

");

// Improper header levels

matchDescriptions["oHL-header-level"] = ["Improper header level", "Section headers start at the second level. (MOS:BADHEAD)"];

if ($(".mw-heading2").length == 0 && $(".mw-heading").length) {

$(".mw-heading").first().after("

[Wrong header level]

");

}

// Thumbnails in infoboxes (uncommon?)

matchDescriptions["oHL-infobox-thumbnail"] = ["Infobox thumbnail", "Instead of embedding another thumbnail, infoboxes support the image_size/image_upright parameters to modify the thumbnail size."];

// $(".infobox .thumb").not(".tmulti").not(".mw-kartographer-container").addClass("oHL oHL-infobox-thumbnail");

$(".infobox figure").addClass("oHL oHL-infobox-thumbnail");

filterList.push(".haudio .oHL-infobox-thumbnail");

// External links in body

matchDescriptions["oHL-body-external"] = ["External links in body", "External links do not belong in the body of an article. (WP:ELPOINTS)"];

$("#mw-content-text .external").addClass("oHL_external");

$(".plainlinks .oHL_external, .oHL_external[class*='mw-magiclink'],"

+ " .infobox .oHL_external, .sidebar .oHL_external,"

+ " .oHL_reflist .oHL_external, .navbox .oHL_external,"

+ " .mw-kartographer-map .oHL_external").removeClass("oHL_external");

$(refSectionsSelector + ", #Further_reading, #Additional_reading,"

+ " #External_links, #Publications").parent().nextUntil(".mw-heading2").find(".oHL_external").removeClass("oHL_external");

$(".oHL_external").addClass("oHL oHL-body-external");

// Unformatted external links

matchDescriptions["oHL-unformatted-external"] = ["Unformatted external link", "The links in the External links section should have descriptions instead of being plain links. (MOS:ELLAYOUT)"];

$("#External_links").parent().nextUntil(".mw-heading2").filter("ul").find(".external.free").addClass("oHL oHL-unformatted-external");

// Auto-numbered links

matchDescriptions["oHL-numbered-reflink"] = ["Numbered reference link without title", "Links in citations should have a title and other information for verification. (WP:CS:EMBED)"];

$(".oHL_reflist .autonumber").addClass("oHL oHL-numbered-reflink");

matchDescriptions["oHL-bare-URL"] = ["Bare reference link without title", "Links in citations should have a title and other information for verification. (WP:CS:EMBED)"];

$(".oHL_reflist .free").addClass("oHL oHL-bare-URL");

matchDescriptions["oHL-numbered-extlink"] = ["External link without title", "Links should contain a title. (WP:ELCITE)"];

$("#External_links").parent().nextUntil(".mw-heading2").find(".autonumber").addClass("oHL oHL-numbered-extlink");

// Wikilinks in headers

matchDescriptions["oHL-header-wikilink"] = ["Header wikilink", "Headers should not contain wikilinks. (MOS:NOSECTIONLINKS)"];

$(".mw-heading .oHL_wikilink").addClass("oHL oHL-header-wikilink");

filterList.push(".oHL_anchorLink.oHL-header-wikilink");

// Big text

matchDescriptions["oHL-big-text"] = ["Big text", "The HTML <big> element is deprecated and changes to font size should be avoided. (MOS:FONTSIZE)"];

$("#mw-content-text big").addClass("oHL oHL-big-text");

// Underlined text

matchDescriptions["oHL-underlined"] = ["Underlining", "Italics or headers should be used instead of underlining. (MOS:UNDERLINE)"];

$("#mw-content-text u").addClass("oHL oHL-underlined");

// Struck out text

matchDescriptions["oHL-striked-text"] = ["Struck text", "Strikethrough should not be used. (MOS:STRIKETHROUGH)"];

$("#mw-content-text s, #mw-content-text strike").addClass("oHL oHL-striked-text");

// Monospaced text

matchDescriptions["oHL-tt-tag"] = ["tt tag", "The <tt> is deprecated. (see MOS:CODE for alternatives)"];

$("#mw-content-text tt").addClass("oHL oHL-tt-tag");

// Text marked as en

matchDescriptions["oHL-lang-en"] = ['Text marked "en"', "The <html> tag of every Wikipedia article already identifies the language of the content as English. (Exception: English text embedded within text marked as another language)"];

$("#mw-content-text span[lang=en]").not(".mw-ext-cite-error").addClass("oHL oHL-lang-en");

// Sister templates next to reflist

matchDescriptions["oHL-misplaced-sisterbox"] = ["Sister template next to reflist", "Floating templates cause layout issues with reference lists and should be relocated. (see Template:Sister_project#Location)"];

$(".sistersitebox + .oHL_reflist, .oHL_reflist + .sistersitebox").before("

[Relocate box↕]

");

// Floated template after and not before list

matchDescriptions["oHL-misplaced-template"] = ["Floating template placement", "Floating templates should go before the content they displace."];

$("ul + .sistersitebox, ul + .portal").after("

[Move template up↑]

");

// Quote boxes at end of sections

matchDescriptions["oHL-misplaced-quotebox"] = ["Floating quote placement", "Quote boxes should be placed after section headers and not before."];

$(".quotebox + .mw-heading2").prev().append("

[Relocate quote box↕]

");

// Horizontal rules

matchDescriptions["oHL-hr"] = ["Horizontal rule", "Horizontal rules should not be used for separation. Instead, use section headings."];

$("hr").before("[horizontal rule]");

filterList.push(".sidebar .oHL-hr", ".infobox .oHL-hr", ".navbox .oHL-hr",

".listen .oHL-hr", ".side-box .oHL-hr", ".quotebox .oHL-hr");

// Sites using http

matchDescriptions["oHL-insecure-site"] = ["Insecure site", "Most modern websites support the HTTPS protocol and external links should be updated to use it."];

const httpsMarkup = " [http]";

$(".infobox .url a[href^='http:']").after(httpsMarkup);

$("#External_links").parent().nextUntil(".mw-heading2").filter("ul").find(".external[href^='http:']").after(httpsMarkup);

// Flag icons in infoboxes

matchDescriptions["oHL-infobox-flagicon"] = ["Infobox flag icon", "Flag icons in infoboxes are deprecated. (MOS:INFOBOXFLAG)"];

$(".infobox .flagicon, .infobox-data img[src*='Flag_of']").addClass("oHL oHL-infobox-flagicon");

// Breaks in infobox titles

matchDescriptions["oHL-infobox-title-br"] = ["Infobox title break", "Content should not be manually line-broken, instead letting the browser word-wrap to the appropriate width. Infoboxes usually have dedicated parameters for alternate names."];

$(".infobox tr").first().find("th br").before(" <br>");

$(".oHL-infobox-title-br + br + .honorific-suffix").prev().prev().remove();

// Redundant bolding

matchDescriptions["oHL-redundant-bold"] = ["Redundant bolding", "Definition lists and table headers are already bolded."];

$("dt b, th b").addClass("oHL oHL-redundant-bold");

// External links which should be internal

matchDescriptions["oHL-external-wikilink"] = ["External wikilink", "Links to Wikipedia pages should use internal linking syntax (square brackets)."];

$(".oHL_external[href*='wikipedia.org']").addClass("oHL oHL-external-wikilink");

filterList.push(".hatnote .oHL-external-wikilink", ".nv-edit .oHL-external-wikilink",

".stub .oHL-external-wikilink", ".dmbox-body .oHL-external-wikilink",

".ambox .oHL-external-wikilink");

// Cross-namespace wikilinks

matchDescriptions["oHL-x-namespace-wl"] = ["Cross-namespace wikilink", "Article content should not link to other namespaces. (MOS:LINKSTYLE)"];

$(".oHL_wikilink[href^='/wiki/Wikipedia:']").addClass("oHL oHL-x-namespace-wl");

filterList.push(".hatnote .oHL-x-namespace-wl", ".stub .oHL-x-namespace-wl",

"#setindexbox .oHL-x-namespace-wl", ".sidebar .oHL-x-namespace-wl",

".navbox-abovebelow .oHL-x-namespace-wl", ".sistersitebox .oHL-x-namespace-wl",

".Inline-Template .oHL-x-namespace-wl", ".portal-bar .oHL-x-namespace-wl",

".spoken-wikipedia .oHL-x-namespace-wl", ".sister-bar .oHL-x-namespace-wl",

".ambox .oHL-x-namespace-wl", ".catlinks .oHL-x-namespace-wl",

".tfd .oHL-x-namespace-wl", ".side-box .oHL-x-namespace-wl",

"[href^='/wiki/Wikipedia:WikiProject_Color'].oHL-x-namespace-wl",

"[href^='/wiki/Wikipedia:WikiProject_Chemicals'].oHL-x-namespace-wl",

"[href^='/wiki/Wikipedia:Chemical_infobox'].oHL-x-namespace-wl");

// Dab links

matchDescriptions["oHL-dab-link"] = ["Disambiguation link", "Articles should not link to disambiguation pages outside of hatnotes. (MOS:LINK#What_generally_should_not_be_linked)"];

$(".mw-disambig").each(function findDabLinks() {

if ($(this).attr("title")?.includes("(disambiguation)")

|| $(this).text().includes("(disambiguation)")) {

return true;

}

$(this).addClass("oHL oHL-dab-link");

});

// Japanese romanization

matchDescriptions["oHL-romaji"] = ["Japanese romanization", "Unless in the title of a work or a common name, modern romanization should be used for Japanese. (WP:ROMAJI)"];

const romajiRe = /(o[ou]|uu|aa|ī|wo|cch|m[bp]|ô|ê|î|é)/g;

$("[lang='ja-Latn']").each(function findRomaji() {

const romaji = this.textContent;

if (romajiRe.test(romaji)) {

const romajiHighlight = romaji.replace(romajiRe, "$1");

this.innerHTML = this.innerHTML.replace(">" + romaji + "<",

">" + romajiHighlight + "<");

}

});

// Thumbnails with link=

matchDescriptions["oHL-thumbnail-link"] = ["Thumbnail link", "For proper attribution, the links in thumbnails should not be overridden."];

$("figure > a:not(.mw-file-description, .mw-file-magnify)").parent().addClass("oHL oHL-thumbnail-link");

// $(".thumbimage").each(function findLinkedThumbs() { // Don't want divs from CSS crop

// if ($(this).children(".mw-graph").length) {

// return true;

// }

// // .tsingle ignores multi-images

// if (!$(this).parent().hasClass("image") && !$(this).parent().hasClass("tsingle")

// && !$(this).parent().hasClass("video") && !$(this).parent().hasClass("audio")) {

// $(this).parents(".thumb").addClass("oHL oHL-thumbnail-link");

// }

// });

// Piped interlanguage links

matchDescriptions["oHL-interlang"] = ["Piped interlanguage link", "Links to non-English articles should not be obscured. (MOS:EGG) Use {{ill}} instead."];

const interlanguageRe = /[a-z]{2}\.wikipedia.org/;

$("#mw-content-text .extiw").each(function findPipedInterlangLinks() {

if ($(this).text().length === 2) {

return true;

}

if (interlanguageRe.test(this.href)) {

$(this).addClass("oHL oHL-interlang");

}

});

filterList.push(".oHL_reflist .oHL-interlang", ".ambox .oHL-interlang");

matchDescriptions["oHL-piped-image"] = ["Piped image link", "Links to images should not be obscured (MOS:EGG). Either embed the image itself or move the link to a parenthetical, making it clear that it’s not to an article."];

$(".oHL_wikilink[href*='File:'], .extiw[href*='File:']").not(".mw-file-description")

.addClass("oHL oHL-piped-image");

filterList.push(".ambox .oHL-piped-image", ".oHL_reflist .oHL-piped-image",

".mw-tmh-player .oHL-piped-image", ".listen-file-header .oHL-piped-image",

".haudio .oHL-piped-image", ".magnify .oHL-piped-image",

".ext-phonos .oHL-piped-image");

// Internal links that should be external

matchDescriptions["oHL-masked-link"] = ["Masked external link", "Links to external websites should not be obscured. (MOS:EGG)"];

$(".extiw[href^='//doi.org'], .extiw[href^='//archive.org']").addClass("oHL oHL-masked-link");

const leadSection = $("#mw-content-text .mw-heading2").first().prevUntil("#mw-content-text");

// See also hatnote at top

matchDescriptions["oHL-hatnote-misuse"] = ["See also hatnote", "The {{see also}} template is not meant to be used as a hatnote, but rather for subsections. (Template:See also)"];

let hatnoteLead;

if ($("#mw-content-text .mw-heading2").length) {

hatnoteLead = leadSection;

} else {

hatnoteLead = $("#mw-content-text .hatnote");

}

hatnoteLead.filter(".hatnote").each(function findHatnoteMisuse() {

if (!isNonSandboxPage) { return false; }

if ($(this).text().startsWith("See also:")) {

$(this).prepend("[rm] ");

}

});

// Infobox not at top

matchDescriptions["oHL-infobox-placement"] = ["Infobox placement", "Infoboxes should be placed before article content. (MOS:ORDER)"];

$("p ~ .infobox:first-of-type").before("

[Move infobox to top↑]

");

// Sidebar in the lead

// FIXME: conflicts with oHL-fullname

matchDescriptions["oHL-sidebar-placement"] = ["Sidebar placement", "Sidebars in the lead are discouraged, and if placed there, preferably after the infobox or lead image. (MOS:LEAD#Sidebars)"];

leadSection.filter(".sidebar").each(function findSidebarMisplacement() {

$(this).after("

[Move sidebar after lead↓]

");

});

// Hatnote not at top of a section

matchDescriptions["oHL-low-hatnote"] = ["Low hatnote", "Hatnotes should be placed at the top of subsections. (WP:HNP)"];

$("p + .hatnote").after("

[Move hatnote up↑]

");

// Hatnote below maintenance template

matchDescriptions["oHL-hatnote-placement"] = ["Hatnote placement", "Hatnotes should be placed above maintenance templates. (WP:HNP)"];

$(".ambox + .hatnote").after("

[Move hatnote up↑]

");

// Italics for long quotes

matchDescriptions["oHL-italquote"] = ["Italicized quote", "Quotations should not be italicized. (MOS:NOITALQUOTE)"];

$("#mw-content-text p i").each(function findItalQuotes() {

if ($(this).text().length >= 80) {

if (typeof $(this).attr("lang") != "undefined") { return true; }

let target = this;

if ($(this).parent("a").length) {

target = this.parentElement;

}

$(target).after(" [Italicized quote]");

}

});

// Empty sections

matchDescriptions["oHL-empty-section"] = ["Empty section", "Empty sections should be deleted or filled by a placeholder. (e.g. {{Empty section}})"];

const emptySectionMarkup = "[Empty section]";

$(".mw-heading").each(function findEmptySections() {

// We don't count headers that only contain subheaders

const headerLevel = $(this).children("h2, h3, h4, h5, h6")[0].tagName[1];

const nextHeader = $(this).next(".mw-heading");

if (nextHeader.length) {

const nextHeaderLevel = nextHeader.children("h2, h3, h4, h5, h6")[0].tagName[1];

if (nextHeaderLevel > headerLevel) {

return true;

}

}

if ($(this).nextUntil(nextHeader).not("style, span").length === 0) {

$(this).after(emptySectionMarkup);

}

});

const lastSection = $(".mw-heading2").last();

if (lastSection.next(".navbox").length) {

lastSection.after(emptySectionMarkup);

}

$(".reflist").each(function findEmptyReflists() {

if ($(this).children().length == 0) {

$(this).after(emptySectionMarkup);

}

});

filterList.push(".toc .oHL-empty-section");

if (mw.config.get("skin") == "minerva") {

filterList.push(".mw-heading .oHL-empty-section");

}

// Anchor links inside article itself

matchDescriptions["oHL-anchor-link"] = ["Self anchor link", "Piped links that lead to subsections within the same article should not be hidden, but indicated with a section marker such as by using {{Section link}}. (see MOS:EGG and principle of least surprise)"];

$("#mw-content-text a[href^='#']").addClass("oHL_sl");

$("#toc .oHL_sl, sup .oHL_sl, .mw-cite-backlink .oHL_sl,"

+ " .reference-text .oHL_sl, .mw-kartographer-map.oHL_sl,"

+ " .mw-kartographer-link.oHL_sl, .oHL_sl[href^='#CITEREF'],"

+ " #catlinks .oHL_sl, .navbox .oHL_sl, .sidebar .oHL_sl").removeClass("oHL_sl");

$(".oHL_sl").each(function findAnchorLinks() {

if (!$(this).text().includes("§")) {

$(this).before("[§] ");

}

});

filterList.push(".NavHead .oHL-anchor-link", ".wikicite .oHL-anchor-link");

// See also section links for other articles

matchDescriptions["oHL-seeAlso-section-link"] = ["See also section link", "Links to subsections of other articles can be indicated using {{Section link}}. (see MOS:EGG and principle of least surprise)"];

$("#See_also").parent().nextUntil(".mw-heading2").find(".oHL_wikilink[href*='#']").each(function findSeeAlsoSectionLinks() {

if (!$(this).text().includes("§")) {

$(this).after(" [§]");

}

});

filterList.push(".navbox .oHL-seeAlso-section-link", ".mw-heading .oHL-seeAlso-section-link");

// Find broken section links

matchDescriptions["oHL-broken-section-link"] = ["Broken section link", "A link points to a subsection that was renamed or removed."];

$("#mw-content-text [href^='#']").each(function findBrokenSectionLinks() {

const target = $(this).attr("href");

if (target == "#" || target.startsWith("#cite_")) {

return true;

}

const targetId = target.substring(1);

const targetSelector = $("#" + $.escapeSelector(targetId));

if (targetSelector.length === 0) {

$(this).addClass("oHL oHL-broken-section-link");

}

});

filterList.push("#toc .oHL-broken-section-link",

".mw-kartographer-link.oHL-broken-section-link");

// Images in see also or external links sections

matchDescriptions["oHL-misplaced-image"] = ["Misplaced images", "Images should be placed in the body of an article, supporting the text. (MOS:IMAGERELEVANCE)"];

$("#See_also, #External_links").each(function findMisplacedImages() {

const images = $(this).parent().nextUntil(".mw-heading2").not(".navbox").find("figure, .tmulti, .gallery");

if (images.length) {

$(this).parent().after("

[Move images]

");

}

});

// External links section formatted as a reflist

matchDescriptions["oHL-ext-reflist"] = ["External links w/ reflist format", "The external links section should not use citation templates. (WP:ELCITE)"];

const externalLinksWrapped = $("#External_links").parent().nextUntil(".mw-heading2").filter(".refbegin");

if (externalLinksWrapped) {

externalLinksWrapped.before("

[Wrapped in Refbegin]

");

externalLinksWrapped.removeClass("oHL_reflist");

}

// Adjacent lists

matchDescriptions["oHL-spaced-list"] = ["Adjacent lists", "Blank lines between list items creates separate lists. (MOS:BULLETLIST)"];

$("#mw-content-text ul + ul, dl + dl").addClass("oHL_adj_li");

$(".gallery.oHL_adj_li, .portalbox + .oHL_adj_li").removeClass("oHL_adj_li");

$(".oHL_adj_li").each(function findSpacedLists() {

const previousSibling = $(this).prev();

if (!previousSibling.hasClass("oHL_adj_li")) {

previousSibling.before("

[Spaced list]

");

}

});

filterList.push(".navbox .oHL-spaced-list", ".sidebar .oHL-spaced-list");

// Lowercase in infobox values

matchDescriptions["oHL-lower-infobox"] = ["Infobox lowercase", "Text in infoboxes should not be arbitrarily lowercased."];

$(".infobox td").each(function findLowercasedInfoboxes() {

const text = $(this).text().trim();

if (text.length === 0) { return true; }

if (text.includes(".")) { return true; }

if (text.startsWith("macOS") || /^[ei][A-Z]/.test(text)) {

return true;

}

if (/^[a-z]/.test(text)) {

$(this).prepend("[↑]");

}

});

// Lowercase See also items

matchDescriptions["oHL-lower-seeAlso"] = ["See also lowercase", "The links in See also sections are normally capitalized."];

$("#See_also").parent().next("ul").children("li").each(function findLowercasedSeeAlsos() {

const text = $(this).text().trim();

const firstLetter = text[0];

if (/[a-z]/.test(firstLetter)) {

$(this).prepend("[↑]");

}

});

// Infobox website not using {{URL}}

matchDescriptions["oHL-plain-site"] = ["Plain infobox website", "External links in infoboxes should be wrapped in {{URL}}. (e.g. see the website parameter at Template:Infobox person)"];

$(".infobox .external").each(function findFullURLs() {

const text = $(this).text();

if (/^http/.test(text)) {

$(this).prepend("[URL] ");

}

});

// Redundant quote marks in blockquotes

matchDescriptions["oHL-redundant-quotes"] = ["Redundant quote marks", "Block quotes should not use enclosing quote marks. (MOS:BLOCKQUOTE)"];

$("blockquote p, .templatequote p, .quotebox-quote").each(function findRedundantQuotes() {

const html = $(this).html();

if (html.includes(`"'`)) { // {{" '}} template

return true;

}

const text = $(this).text();

if (text.charAt(0) == '"') {

$(this).html(html.slice(1));

$(this).prepend("\"");

}

});

// Wikilinked parenthesis or punctuation

matchDescriptions["oHL-wikilink-punc"] = ["Wikilinked punctuation", "Punctuation should not be wikilinked, as it is not part of the link."];

$(".oHL_wikilink").each(function findWikilinkedPunctuation() {

if (!isNonDisambigPage) { return false; }

const text = $(this).text();

const finalChar = text.at(-1);

if (text.substring(text.length-2) == "..") { return true; } // ellipses

if ('.,;:")]/'.includes(finalChar)) {

if (finalChar == ".") {

if (text.endsWith("Inc.") || text.endsWith("Sr.")

|| text.endsWith("Jr.") || text.endsWith("Bros.")

|| text.endsWith("Co.")) { return true; }

if (/\.[A-Z]\./.test(text)) { return true; }

if (/ [A-Z]\.$/.test(text)) { return true; }

if (/\..*\./.test(text)) { return true; }

}

const html = $(this).html(); // use HTML to preserve formatting, e.g. Inception (film)

const replaceRe = new RegExp("(.*)\\" + finalChar);

$(this).html(html.replace(replaceRe, "$1"));

$(this).append("" + finalChar + "");

}

});

$("#See_also").parent().nextUntil(".mw-heading2").filter("ul, .columns, .div-col")

.find(".oHL-wikilink-punc").each(function filterSeeAlsoWikilinkedPunc() {

removeHLClass(this, "oHL-wikilink-punc");

});

filterList.push(".reference .oHL-wikilink-punc", ".external .oHL-wikilink-punc",

".hatnote .oHL-wikilink-punc", ".mw-kartographer-link .oHL-wikilink-punc",

".IPA .oHL-wikilink-punc", ".infobox-label .oHL-wikilink-punc",

".listen .oHL-wikilink-punc");

// Underscore in wikilink

matchDescriptions["oHL-wl-underscore"] = ["Wikilink underscore", "Article text should not contain underscores."];

$(".oHL_wikilink").each(function findUnderscoredWikilinks() {

if ($(this).text().includes("_")) {

$(this).addClass("oHL oHL-wl-underscore");

}

});

// Check proper sections

matchDescriptions["oHL-missing-ref-section"] = ["Missing ref section", "Articles should have a References section. (MOS:LAYOUT)"];

if (isNonDisambigPage && isNonSandboxPage) {

if ($(refSectionsSelector).length === 0) {

const highlightMarkup = "

[References]

";

const endMatter = $(".succession-box, .navbox, .stub");

if (endMatter.length) {

$(endMatter).first().before(highlightMarkup);

} else {

$("#mw-content-text").append(highlightMarkup);

}

}

checkSectionOrder();

}

// Tables without headers

matchDescriptions["oHL-table-header"] = ["Table without headers", "Tables should have headers for the columns."];

$("table").each(function findHeaderlessTables() {

if ($(this).hasClass("succession-box")

|| $(this).hasClass("sistersitebox")

|| $(this).hasClass("ambox")

|| $(this).hasClass("ombox")

|| $(this).hasClass("clade")

|| $(this).parent().hasClass("stub")

|| $(this).attr("role") == "presentation") {

return true;

}

if ($(this).find("tr").length > 1 && $(this).find("th").length === 0) {

$(this).prepend("[Missing table headers]");

}

});

filterList.push("table .oHL-table-header", ".chessboard .oHL-table-header");

// Poem not inside blockquote or verse translation

matchDescriptions["oHL-unwrapped-poem"] = ["Unwrapped poem", "Quoted poem content needs to be wrapped in {{quote}} or {{verse translation}}. (see MOS:BLOCKQUOTE)"];

$(".poem").each(function findUnwrappedPoems() {

if ($(this).parent().is(":not(blockquote):not(td)")) {

$(this).before("

[Unwrapped poem]

");

}

});

// Citations at paragraph start

matchDescriptions["oHL-cite-placement"] = ["Citation placement", "Citations should come after the text they support."];

$("#mw-content-text p").each(function findMisplacedCitations() {

if (this.firstChild?.classList?.contains("reference")) {

$(this.firstChild).addClass("oHL oHL-cite-placement");

}

});

// Sentences missing a period

matchDescriptions["oHL-missing-sentence-period"] = ["Missing sentence period", "Sentences should end with a full stop."];

$(".reference").each(function findUnterminatedSentences() {

if ($(this).hasClass("oHL-cite-placement")) { return true; }

const prevChar = this.previousSibling?.textContent.slice(-1);

const nextChars = this.nextSibling?.textContent.slice(0, 2);

if (prevChar === null || nextChars === null) { return true; }

if (/[a-z]/.test(prevChar) && / [A-Z]/.test(nextChars)) {

$(this).before("[.]");

}

});

// Paragraphs missing a period

matchDescriptions["oHL-missing-paragraph-period"] = ["Missing paragraph period", "Paragraphs should end with a full stop."];

$("#mw-content-text p").each(function findUnterminatedParagraphs() {

if ($(this).children(".oHL-pseudoheader, .mwe-math-element, .tfd, .anchor").length) { return true; }

if ($(this).children().first().is(".oHL_added, br")) { return true; }

const element = this.cloneNode(true);

element.querySelectorAll("style, sup")?.forEach(e => e.remove());

const paragraph = $(element).text();

// Delete quotes and trailing whitespace

const paragraphCleaned = paragraph.replace(/["”'’]/g, "").replace(/\s+$/, "");

const lastTwoCharacters = paragraphCleaned.slice(-2);

if ([".)", "?)", "!)"].includes(lastTwoCharacters)) { return true; }

const lastCharacter = lastTwoCharacters[1];

if (".?!".includes(lastCharacter)) { return true; }

const followedByBlockElement = $(this).next().is("ol, ul, blockquote,"

+" .quotebox, .poem, .mwe-math-element, pre, .mw-highlight, .center,"

+ " .mw-halign-center, .mw-ext-score, .verse_translation, table");

let allowedChars = "";

if (!isNonDisambigPage || followedByBlockElement) {

allowedChars += ",:";

}

if (!allowedChars.includes(lastCharacter)) {

$(this).append("[.]");

}

});

filterList.push("blockquote .oHL-missing-paragraph-period", ".quotebox .oHL-missing-paragraph-period",

".gallerytext .oHL-missing-paragraph-period", "th .oHL-missing-paragraph-period",

".poem .oHL-missing-paragraph-period", ".infobox .oHL-missing-paragraph-period",

".clade .oHL-missing-paragraph-period");

// Breaks between paragraphs

matchDescriptions["oHL-br"] = ["Paragraph breaks", "Paragraph breaks should use newlines (not the <br> element) and there should only be a single break between paragraphs. For lists, use * (or {{ubl}} in infoboxes). For poems, use <poem> tags wrapped in <blockquote>."];

// $("p br").each(function findParagraphBreaks() {

// if (this.previousSibling === null) {

// $(this).before("<br>");

// }

// });

$("#mw-content-text p br").before("<br>");

// Filter out stub templates

$(".oHL-br").each(function filterStubBreaks() {

if ($(this).parent().next(".stub").length) {

$(this).remove();

}

});

filterList.push(".poem .oHL-br", ".chemf .oHL-br", ".music-symbol .oHL-br");

// Improper infobox lists

matchDescriptions["oHL-infobox-br"] = ["Improper infobox list", "Embedded lists in infoboxes should use semantic markup with {{ubl}}. (MOS:UBLIST)"];

$(".infobox-data br, .infobox br + a, .infobox br + .url").before("<br> ");

filterList.push("b + .oHL-infobox-br", ".infobox-header .oHL-infobox-br",

".nickname + .oHL-infobox-br");

$(".oHL-infobox-br + br + .birthplace").prev().prev().remove();

$(".oHL-infobox-br + br + .deathplace").prev().prev().remove();

$(".oHL-infobox-br + br + .geo-inline").prev().prev().remove();

$(".oHL-infobox-br + br + b").prev().prev().remove(); // Pseudoheaders

$(".oHL-infobox-br + br + .oHL-infobox-br").prev().prev().remove(); // double matches

$("a[title='Least Concern']").prev(".oHL-infobox-br").remove(); // Endangered status

// Improper table lists

matchDescriptions["oHL-table-br"] = ["Improper table list", "Embedded lists in tables should use semantic markup with {{ubl}}. (MOS:UBLIST)"];

$("table:not(.infobox) td br").before("<br> ");

filterList.push(".infobox .oHL-table-br", ".navbox .oHL-table-br",

".sidebar .oHL-table-br", ".listen .oHL-table-br",

".ambox .oHL-table-br", ".vgr-reviews .oHL-table-br",

".vgr-aggregators .oHL-table-br", ".succession-box .oHL-table-br");

// Double quotes which should be nested

matchDescriptions["oHL-nested-quote"] = ["Nested quote marks", "Nested quote marks should alternate between double and single. (MOS:QWQ)"];

$("cite, q").each(function findDupeDoubleQuotesKerned() {

const text = this.textContent;

if (text.includes('"')) {

const oldHtml = this.innerHTML;

let html = oldHtml;

html = html.replaceAll('"',

'"');

html = html.replaceAll('"',

'"');

if (html != oldHtml) {

this.innerHTML = html;

}

}

});

$("cite .external, q").each(function findDupeDoubleQuotes() {

const text = this.textContent;

if (text.includes('"')) {

const oldHtml = this.innerHTML;

let html = oldHtml;

html = html.replace(/(?<=<[^>]*)"(?=[^<]*>)/g, "€€"); // guard

html = html.replace(/^""/, '""');

html = html.replace(/""$/, '""');

html = html.replace(/ "/g, ' "');

html = html.replace(/" /g, '" ');

html = html.replaceAll("€€", '"'); // unguard

if (html != oldHtml) {

this.innerHTML = html;

}

}

});

$(".oHL-nested-quote ~ .oHL-nested-quote").addClass("oHL_quoteAdditional")

.each(function removeDupeQuoteClass() {

removeHLClass(this, "oHL-nested-quote");

});

// Images without |thumb|

matchDescriptions["oHL-frameless-img"] = ["Frameless image", "Most image thumbnails should use |thumb|, along with a caption."];

$("figure[typeof='mw:File'], [typeof='mw:File/Frameless']").each(function findFramelessImages() {

const displayWidth = $(this).find("img").first().attr("width");

if (displayWidth > 30 && $(this).closest(".thumb, table").length === 0) {

$(this).addClass("oHL oHL-frameless-img");

}

});

filterList.push(".side-box .oHL-frameless-img", ".dmbox .oHL-frameless-img");

// Improper indentation

matchDescriptions["oHL-bad-indent"] = ["Improper indentation", "Quotations, tables, formulas, and generic lists are not definition lists and should use proper semantic markup. (see: User:Opencooper/Proper indentation)"];

$("dd, dd ul, dd ol").addClass("oHL_indent");

$("dt + .oHL_indent, dd + .oHL_indent, .oHL_indent dl .oHL_indent").removeClass("oHL_indent");

$(".oHL_indent sub").parent().removeClass("oHL_indent"); // Chem formulas

$(".oHL_indent").parent("dl").addClass("oHL_bad-indent");

$(".oHL-spaced-list + .oHL_bad-indent").prev().remove();

$(".mwe-math-element").closest(".oHL_bad-indent").addClass("oHL_bad-math-indent").removeClass("oHL_bad-indent");

$("p > .mwe-math-element > .mwe-math-mathml-inline").parent().parent().addClass("oHL_bad-math-indent");

$(".oHL_bad-indent").each(function findBadIndents() {

const previousSibling = $(this).prev();

if (!previousSibling.hasClass("oHL_bad-indent")) {

$(this).before("

[Improper indent]

");

}

});

// Unindented definition terms

matchDescriptions["oHL-dl-indent"] = ["Unindented term definition", "Definition lists consist of term–definition pairs, the latter of which should be indented. (see: MOS:DEFLIST)"];

$("dl:not(.oHL_bad-math-indent) + p + dl, dl:not(.oHL_bad-math-indent) + p + .mw-heading2").prev().prepend("[→] ");

// Unindented math

matchDescriptions["oHL-math-indent"] = ["Unindented math", "Math placed on its own line should be indented using <math display=block> (MOS:MATH#Using_LaTeX_markup)"];

$(".oHL_bad-math-indent .mwe-math-element").each(function findUnindentedMath() {

$(this).prepend("[→] ");

});

// Tall math equations

// TODO: check if this works with other math rendering options

matchDescriptions["oHL-tall-math"] = ["Tall math", "Math equations that extend past the line should be made compact using <math display=inline> (MOS:MATH#Using_LaTeX_markup)"];

$(".mwe-math-element img").each(function findTallMath() {

if ($(this).hasClass("mwe-math-fallback-image-display")) {

return true;

}

const heightStyle = this.style.height;

const heightAmount = Number(heightStyle.replace(/[a-z]/g, ""));

if (heightStyle.includes("ex") && heightAmount >= 4.0) {

$(this).closest(".mwe-math-element").addClass("oHL oHL-tall-math");

}

});

filterList.push(".oHL_bad-math-indent .oHL-tall-math");

//

tag

matchDescriptions["oHL-center"] = ["Center tag", "The HTML <center> tag is deprecated. Content, such as captions, should not be arbitrarily centered."];

$("center").each(function findCenterTags() {

$(this).before("

[center tag]

");

});

// Centered captions

matchDescriptions["oHL-caption-center"] = ["Centered caption", "Content, such as captions, should not be arbitrarily centered."];

$("figcaption center, figcaption .center, .thumb .center").addClass("oHL oHL-caption-center");

filterList.push(".infobox center .oHL-caption-center", ".chessboard .oHL-caption-center");

// Unnecessary ToC

// TODO: still relevant after floating ToC changes?

matchDescriptions["oHL-toc"] = ["Unnecessary ToC", "Stubs without unique subsections do not need a table of contents."];

const firstSection = $(".toctext").first().text();

if (firstSection && "See also, References, Sources, External links".includes(firstSection)) {

$("#toc").after("

[Hide ToC]

");

}

// Missing lead

matchDescriptions["oHL-missing-lead"] = ["Missing lead", "The article is missing a lead. (MOS:LEAD)"];

if (leadMarker.prevAll("p").length === 0) {

leadMarker.before("

[Missing lead]

");

}

// Table with missing cells

matchDescriptions["oHL-missing-cells"] = ["Missing table cells", "Tables should not have missing cells. Add empty cells with placeholders () instead."];

$(".wikitable").each(function findTables() {

const tableElement = this;

if ($(this).find("[colspan], [rowspan]").length) {

return true;

}

const rows = $(this).find("tr");

const firstRow = $(rows).first();

const columnsCount = $(firstRow).find("th").length;

rows.each(function findMissingCells() {

const cellsCount = $(this).children().length;

if (cellsCount < columnsCount) {

$(tableElement).after("

[Table missing cells]

");

return false;

}

});

});

// Table with both vertical and horizontal headers

// matchDescriptions["oHL-redundant-headers"] = ["Table header misuse", "Tables should not have both horizontal and vertical headers."];

// $(".wikitable").each(function findTableHeaderMisuse() {

// const hasVerticalHeader = $(this).find("tr:first-child th").length != 0;

// const hasHorizontalHeader = $(this).find("tr:not(:first-child) th").length != 0;

// if (hasVerticalHeader && hasHorizontalHeader) {

// $(this).after("

[Table misuses headers]

");

// }

// });

// Authority control before navboxes

matchDescriptions["oHL-auth-placement"] = ["Authority control placement", "Authority control templates should go after navboxes. (MOS:ORDER)"];

$(".authority-control + .navbox").before("

[Move authority control down↓]

");

// Long lists

// $("#mw-content-text ul").addClass("oHL_list");

// $("#toc .oHL_list, .navbox .oHL_list, #catlinks .oHL_list,"

// + " .refbegin .oHL_list, .div-col .oHL_list, .sidebar .oHL_list").removeClass("oHL_list");

// $(".oHL_list").each(function findLongLists() {

// const items = $(this).children("li").length;

// if (items >= 8) {

// $(this).before("

[Split into columns]

");

// }

// });

// Lists broken up by an image

matchDescriptions["oHL-list-img"] = ["Image in list", "Images inside lists break them up into separate lists, and should go before the list. (MOS:LIST#Images_and_lists)"];

$("ul + figure + ul, ul + .tmulti + ul").before("

[Move image interrupting list]

");

// Duplicate references

matchDescriptions["oHL-duplicated-ref"] = ["Duplicated ref", "References should not be duplicated, instead using named references. (see: WP:NAMEDREFS)"];

const refLinks = [];

$(".oHL_reflist .reference-text").each(function findReferenceLinks() {

const firstLink = $(this).find(".external").first();

if (!firstLink.length) { return true; }

const href = $(firstLink).attr("href");

const hrefCleaned = href.replace(/https?:\/\//, "");

refLinks.push(hrefCleaned);

});

const refLinksUnique = new Set(refLinks);

if (refLinksUnique.size != refLinks.length) {

for (const link of refLinksUnique) {

const count = refLinks.filter(l => l === link).length;

if (count > 1) {

let selector = "a[href*='" + link + "']";

if (!link.includes("archive.org")) {

selector += ":not([href*='archive.org'])";

}

const dupes = $(".oHL_reflist " + selector);

dupes.first().addClass("oHL oHL-duplicated-ref");

dupes.slice(1).addClass("oHL_dupe_ref");

}

}

}

// Duplicate images

matchDescriptions["oHL-duplicate-img"] = ["Duplicate image", "In most cases, images should not be repeated."];

let imageLinks = [];

$(".mw-file-description").each(function getImages() {

imageLinks.push($(this).attr("href"));

});

const imagesUnique = new Set(imageLinks);

if (imagesUnique.size != imageLinks.length) {

for (const link of imagesUnique) {

const count = imageLinks.filter(l => l === link).length;

if (count > 1) {

$(".mw-file-description[href='" + link + "']").not(":first").children("img").addClass("oHL oHL-duplicate-img");

}

}

}

filterList.push(".stub .oHL-duplicate-img", ".navbox .oHL-duplicate-img",

".sidebar .oHL-duplicate-img", ".ambox .oHL-duplicate-img",

".chessboard .oHL-duplicate-img", ".side-box .oHL-duplicate-img");

// Thumbnails at end end of sections

matchDescriptions["oHL-thumb-placement"] = ["Thumbnail placement", "Floating content, such as thumbnails, should go before the content they displace."];

$("p + figure + .mw-heading2, ul + figure + .mw-heading2").before("

[Relocate image]

");

filterList.push(".mw-halign-center + .oHL-thumb-placement");

// Thumbnails larger than actual size

matchDescriptions["oHL-overlarge-img"] = ["Overlarge image", "Thumbnails should not be set to sizes larger than the actual image file, resulting in upscaling."];

$("[typeof^='mw:File'] img").not(".noviewer").each(function findOverlargeThumbnails() {

const src = $(this).attr("src");

if (src.endsWith(".svg.png")) { return true; }

const displayWidth = $(this).attr("width");

const originalWidth = $(this).attr("data-file-width");

if (parseInt(displayWidth) > parseInt(originalWidth)) {

$(this).addClass("oHL oHL-overlarge-img");

}

});

// Dash in front of quote attributions

// Maybe not. Reference: https://en.wikipedia.org/wiki/Wikipedia:Manual_of_Style#Other_uses_(em_dash_only)

// $(".quotebox cite").each(function findMissingQuoteDashes() {

// const text = $(this).text();

// if (!text.startsWith("–") && !text.startsWith("—")) {

// $(this).prepend("[—] ");

// }

// });

// Fake footnotes

matchDescriptions["oHL-pseudo-footnotes"] = ["Pseudo footnote", "The {{ref}} template is deprecated for citing sources. (Template:Ref)"];

$(".reference.plainlinks").addClass("oHL oHL-pseudo-footnotes");

// Short descriptions

matchDescriptions["oHL-description-uppercase"] = ["Short description case", "Short descriptions should begin with an uppercase letter. (WP:SDFORMAT)"];

matchDescriptions["oHL-description-punc"] = ["Short description period", "Short descriptions should not use a full stop. (WP:SDFORMAT)"];

matchDescriptions["oHL-description-length"] = ["Short description length", "Short descriptions should usually use less than 40 characters. (WP:SDLENGTH)"];

matchDescriptions["oHL-description-dupe"] = ["Short description duplication", "Short descriptions should not be the same as the page title. (WP:SDDUPLICATE)"];

const shortDescriptions = $(".shortdescription");

shortDescriptions.first().each(function analyzeShortDescription() {

let text = $(this).text();

const length = text.length;

if (length == 0) { return false; }

const pageTitle = mw.config.get("wgTitle").replace(/ \(.*\)$/, "");

if (text.toLowerCase() == pageTitle.toLowerCase()) {

$(this).append(" [duplicates title]");

}

const firstChar = text[0];

if (/[a-z]/.test(firstChar)) {

text = text.slice(1);

$(this).text(text);

$(this).prepend(""

+ firstChar + "");

}

const finalChar = text.at(-1);

if (!text.endsWith("U.S.") && finalChar == ".") {

text = text.slice(0, -1);

$(this).text(text);

$(this).append("" + finalChar + "");

}

if (length > 100) {

$(this).append(" [too long] ("

+ length + " chars)");

}

});

// Missing short description

// Filtered in: checkEmptyShortdescription()

matchDescriptions["oHL-missing-desc"] = ["Missing short description", "All mainspace articles should have a short description. (WP:SHORTDESC)"];

if (shortDescriptions.length == 0 && mw.config.get("skin") != "minerva") {

$("#mw-content-text").prepend("

[Missing short description]

");

}

// Short description placement

matchDescriptions["oHL-description-placement"] = ["Short description placement", "Short descriptions should be placed at the top of an article. (MOS:ORDER)"];

if (isNonDisambigPage) {

$("p ~ .shortdescription").first().append(" [Move up↑]");

}

// Superscripts in headers

matchDescriptions["oHL-header-superscript"] = ["Header tag", "Headers should not have references or other inline templates. (MOS:HEADINGS)"];

$(".mw-heading .Inline-Template, .mw-heading .reference").addClass("oHL oHL-header-superscript");

// Redundant italicization

matchDescriptions["oHL-redundant-italics"] = ["Redundant italicization", "The {{lang}} template already italicizes text, so italics markup is not necessary."];

$("i > span > i[lang]").addClass("oHL oHL-redundant-italics");

// Emphasis

matchDescriptions["oHL-emph"] = ["Emphasis", "When italics are used to emphasize text, the {{em}} template is more semantic. Foreign text should use {{lang}}. (MOS:EMPHASIS)"];

$("#mw-content-text i").each(function findEmphasis() {

if (this.parentElement.tagName == "SUP" || this.firstChild?.tagName == "A"

|| this.hasAttribute("lang")

|| (this.firstChild?.tagName == "SPAN" && this.firstChild.hasAttribute("lang"))

|| $(this).closest("a").length

|| this.parentElement.classList.contains("serif-fonts")) {

return true;

}

const text = $(this).text();

if (text.length == 0 || text.includes(" ")) {

return true;

}

// Try excluding math variables

if (text.length == 1 && text != "a") {

return true;

}

const firstLetter = text[0];

if (/[A-Z]/.test(firstLetter)) {

return true;

}

// Require a preceding space

const previousSibling = this.previousSibling;

if (previousSibling != null && previousSibling.nodeName == "#text" && !previousSibling.textContent.endsWith(" ")) {

return true;

}

$(this).addClass("oHL-opt oHL-emph");

});

filterList.push(".oHL_reflist .oHL-emph", ".texhtml .oHL-emph", ".side-box .oHL-emph");

// Multi-column lists with too many columns

matchDescriptions["oHL-col-count"] = ["Column count", "A list should not have so many columns that it hampers scannability. (the list would have more than three columns on a 1920px display at the default Vector font size)"];

$(".div-col").not(".sidebar").each(function inspectColumnWidths() {

const colWidthCSS = this.style["column-width"];

if (colWidthCSS == null || !/em$/.test(colWidthCSS)) { return true; }

const colWidthEm = parseFloat(colWidthCSS.replace("em", ""));

if (colWidthEm <= 29.3) {

$(this).before("

[Too many columns]

");

}

});

// Self-ref hatnotes

matchDescriptions["oHL-self-ref"] = ["Self-ref hatnote", "Hatnotes that link to Wikipedia pages should use the |selfref=yes parameter. (WP:ITSELF)"];

$(".hatnote:not(.selfreference) a[href^='/wiki/Wikipedia']").parent().addClass("oHL oHL-self-ref");

// Stub template spacing

matchDescriptions["oHL-stub-space"] = ["Stub template spacing", "Stub templates should be preceded by two blank lines. (WP:STUBSPACING)"];

$(".mw-parser-output > :not(p, .stub) + .stub").before("

[¶]

");

// Non-romanized text outside of parenthesis

matchDescriptions["oHL-non-Latin-prose"] = ["Non-Latin prose", "Article prose should primarily use romanized text, with the non-Latin text in parenthesis. (see MOS:TEXT#Foreign_terms)"];

$("[lang]").each(function findNonLatinProse() {

if (this.lang.includes("Latn")) {

return true;

}

const sibling = this.parentElement?.nextSibling;

if (sibling && sibling.nodeType == 3 && sibling.textContent == " (") {

const nextElement = sibling?.nextSibling;

if (nextElement && nextElement.hasAttribute("lang")) {

$(this).addClass("oHL oHL-non-Latin-prose");

}

}

});

// Italicized non-Latin text

matchDescriptions["oHL-non-Latin-italics"] = ["Non-Latin italics", "Italics should not be used with non-Latin scripts that don’t use them. (MOS:BADITALICS)"];

const nonItalicLangs = ["ja", "zh", "ko", "cmn", "ar", "ur", "hi", "sa"];

$("#mw-content-text i [lang]").each(function findItalicizedNonLatin() {

if (nonItalicLangs.includes(this.lang)) {

$(this).addClass("oHL oHL-non-Latin-italics");

}

});

// Bolding title after lead

matchDescriptions["oHL-overbolding"] = ["Overbolding", "Only the first occurrence of the article title should be bolded. (MOS:BOLDSYN)"];

if (isNonDisambigPage) {

const leadBolded = leadMarker.prevAll().not(".infobox, .ambox, .sidebar, .side-box, .navbox").find("b");

leadBolded.addClass("oHL_title");

const leadNames = leadBolded.toArray().map(e => e.textContent.toLowerCase());

$("#mw-content-text b").not(".oHL_title").each(function findOverbolding() {

const boldText = this.textContent.toLowerCase();

if (leadNames.includes(boldText)) {

$(this).addClass("oHL oHL-overbolding");

}

});

filterList.push(".infobox .oHL-overbolding", ".ambox .oHL-overbolding",

".navbox .oHL-overbolding", ".sistersitebox .oHL-overbolding",

".sidebar .oHL-overbolding", ".side-box .oHL-overbolding",

".dmbox .oHL-overbolding", ".clade .oHL-overbolding",

".sister-bar .oHL-overbolding");

}

// Duplicate bolded lead items

const boldLeadTitles = [];

$(".oHL_title").each(function findDuplicateLeadBolding() {

const title = $(this).text();

if (boldLeadTitles.includes(title)) {

$(this).addClass("oHL oHL-overbolding");

} else {

boldLeadTitles.push(title);

}

});

// Bolded quote marks in lead

matchDescriptions["oHL-title-quote-bold"] = ["Bolded title quote mark", "Quote marks in the subject’s name should not be bolded. (MOS:QUOTENAME)"];

$(".oHL_title").each(function findBoldedNameQuotes() {

const text = this.textContent;

if (!text.includes('"')) { return true; }

const oldHtml = this.innerHTML;

let html = oldHtml;

html = html.replaceAll(' "', ' "');

html = html.replaceAll('" ', '" ');

if (html != oldHtml) {

this.innerHTML = html;

}

});

// Citation overkill

matchDescriptions["oHL-overciting"] = ["Overciting", "Use of more than three adjacent citations should be trimmed or bundled. (WP:OVERCITE)"];

$(".reference").each(function findOverciting() {

// Make sure we're at the first of adjacent citations

if (this.previousSibling?.nodeName == "SUP"

&& this.previousSibling.classList.contains("reference")) {

return true;

}

let citeCount = 1;

let nextElement = this.nextSibling;

while (nextElement?.classList?.contains("reference")) {

citeCount += 1;

const neighbor = nextElement.nextSibling;

if (neighbor?.nodeName != "SUP") {

break;

}

nextElement = neighbor;

}

if (citeCount > 3) {

$(nextElement).after(" [Overciting]");

}

});

// Sandwiched images

let sandwichSelector = "";

const sandwichVariants = [".tright", ".infobox", ".sidebar", ".quotebox"];

for (const variant of sandwichVariants) {

sandwichSelector += " .tleft + " + variant + ", ";

sandwichSelector += variant + " + .tleft,";

}

sandwichSelector = sandwichSelector.replace(/,$/, "");

matchDescriptions["oHL-image-sandwich"] = ["Sandwiched images", "Avoid squishing text between a left-floating image. (MOS:SANDWICH)"];

$(sandwichSelector).after("

[Sandwiched images]

");

// Orphaned references

matchDescriptions["oHL-orphaned-refs"] = ["Orphaned references", "References should normally be housed in the References section. These references were likely cited after the {{reflist}} appears."];

if ($(".references").length > 1) {

const lastElement = $(".mw-parser-output").children().last();

if (lastElement.hasClass("oHL_reflist")) {

lastElement.before("

[Orphaned references]

");

}

$(refSectionsSelector).parent().nextUntil(".mw-heading2").filter(".oHL_orphanedRefs").remove();

}

// Floating elements clashing with a reflist

matchDescriptions["oHL-obstructed-reflist"] = ["Obstructed reflist", "Floating templates should not encroach the space of a multi-column reflist or they will cause layout problems. To fix this, a {{clear}} should be placed at the end of the section before the References section."];

const columnarReflists = $(".mw-references-columns");

if (columnarReflists.length > 0) {

const bodyElement = document.getElementById("mw-content-text");

const bodyWidth = window.getComputedStyle(bodyElement).width;

columnarReflists.each(function findObstructedReflists() {

const reflistWidth = window.getComputedStyle(this).width;

if (reflistWidth != bodyWidth) {

$(this).closest(".mw-references-wrap").before("

[Obstructed reflist]

");

}

});

}

// Font size

matchDescriptions["oHL-font-size-change"] = ["Font size", "Reduced or enlarged font sizes should be used sparingly. (MOS:SMALL)"];

$(".div-col-small").before("

[Font size]

");

// Special rules for disambiguation pages

if (!isNonDisambigPage) {

matchDescriptions["oHL-disambig-multi-links"] = ["Multiple wikilinks", "Disambiguation listings should only have one blue link. (MOS:DABONE)"];

$(".oHL_wikilink ~ .oHL_wikilink").addClass("oHL oHL-disambig-multi-links");

}

// Emitted citation errors

matchDescriptions["oHL-citation-error"] = ["Citation error", "The article contains a citation error."];

$(".cs1-maint").show();

$(".cs1-visible-error:first-of-type, .cs1-maint, .mw-ext-cite-error").addClass("oHL oHL-citation-error");

// Find overlinking

checkOverlinking();

// Italics title for works

checkTitleItalicization();

// Colored backgrounds with poor contrast

checkContrast();

// Check ALT text and show full size of images

showImageInfo();

}

function prepHTML() {

expandCollapsed();

// Mark ISBNs

$(".oHL_wikilink[href^='/wiki/Special:BookSources']").addClass("oHL_ISBN");

// Temporarily remove elements from the DOM

$("#toc, .mw-editsection, .mwe-math-element, .mw-cite-backlink, #catlinks,"

+ " .IPA, .mw-highlight, code, .oHL_ISBN, .external.free,"

+ " .external[href*='doi.org'], .external[href*='worldcat.org'],"

+ " .navbox .uid, .barbox, .mw-kartographer-map, .texhtml,"

+ " video, canvas, .oHL_anchorLink, .mw-tmh-player, .ext-phonos,"

+ " .lazy-image-placeholder, .timeline-wrapper, .infobox .bday,"

+ " .calculator-container").each(detachTemp);

/*

* Replace attributes so they don't get caught in our highlighting

* Note: id needs to come first because we insert our own ids for the others

*/

document.querySelectorAll("#mw-content-text [id]")?.forEach(e => mangle(e, "id"));

document.querySelectorAll("#mw-content-text [style]")?.forEach(e => mangle(e, "style"));

document.querySelectorAll("#mw-content-text [href]")?.forEach(e => mangle(e, "href"));

document.querySelectorAll("#mw-content-text [title]")?.forEach(e => mangle(e, "title"));

document.querySelectorAll("#mw-content-text img[src]")?.forEach(e => mangle(e, "src"));

document.querySelectorAll("#mw-content-text img[alt]")?.forEach(e => mangle(e, "alt"));

document.querySelectorAll("#mw-content-text img[resource]")?.forEach(e => mangle(e, "resource"));

// document.querySelectorAll("h2, h3, h4, h5, h6")?.forEach(e => {

// mangle(e, "onmouseover");

// mangle(e, "onmouseout");

// });

}

function expandCollapsed() {

$(".mw-collapsible").children().children("tr").css("display", "");

$(".mw-collapsible-content").css("display", "");

$(".NavFrame .NavToggle").each(function expandNavs() {

if ($(this).text() == "[show]") {

$(this).click();

}

});

$(".collapsible-heading").not(".open-block").click();

}

function replaceHTML() {

const contentElement = document.getElementById('mw-content-text');

let html = contentElement.innerHTML;

// Delete comments

html = html.replace(//gs, '');

// Keep track of Euro symbols, which we use to guard text we don't want matched

const euroCountBefore = (html.match(/€/g) || []).length;

// p. in refs with multiple pages

matchDescriptions["oHL-pp"] = ["Multipage cite", "Citations containing multiple pages should use “pp.”"];

html = html.replace(/ p.((?: | | )[0-9]+[-–])/g, ' p.$1');

// Dashes

matchDescriptions["oHL-rangedash"] = ["Range dash", "Ranges should use an en dash. (MOS:ENDASH)"];

html = html.replace(/(\w+)-(\w+)-(\w+)/g, '$1€-€$2€-€$3'); // Guard YYYY-MM-DD, 9-1-1, etc.

html = html.replace(/(\d)-(\d)/g, '$1-$2');

html = html.replace(/(\d)-present/g, '$1-present');

html = html.replaceAll('€-€', '-'); // Unguard

filterList.push(".external .oHL-rangedash");

matchDescriptions["oHL-typewriter-dash"] = ["Typewriter dash", "Dashes should use the proper Unicode character instead of typewriter dashes. ( or ; MOS:DASH)"];

html = html.replace(/(---|--|–-|-–|-—|—-)/g, '$1');

filterList.push(".oHL_reflist .oHL-typewriter-dash");

matchDescriptions["oHL-spaced-endash"] = ["Spaced dash", "Spaced dashes should use the proper Unicode character for en dashes. (; MOS:ENDASH)"];

html = html.replaceAll(' - ', ' - ');

html = html.replaceAll(' -', ' -');

filterList.push(".oHL_reflist .oHL-spaced-endash", ".side-box .oHL-spaced-endash");

matchDescriptions["oHL-spaced-emdash"] = ["Spaced em dash", "Em dashes should be unspaced. (MOS:EMDASH)"];

html = html.replace(/( —|(?$1');

filterList.push(".oHL_reflist .oHL-spaced-emdash");

matchDescriptions["oHL-bad-rangedash"] = ["Bad range dash", "Dashes in ranges should use an en dash. (MOS:ENDASH)"];

html = html.replace(/(\d)—(\d)/g, '$1$2');

filterList.push(".oHL_reflist .oHL-bad-rangedash");

matchDescriptions["oHL-spaced-range"] = ["Spaced range", "The en dash in numerical ranges should be unspaced. (MOS:ENDASH)"];

html = html.replace(/(\d{4}) – (\d{1,2} [A-Z])/g, '$1€–€$2'); // Guard YYYY – DD Month YYYY

html = html.replace(/(\d{1,2} [A-Z][a-z]+ \d{4}) – (\d{4})/g, '$1€–€$2'); // Guard DD Month YYYY – YYYY

html = html.replace(/(\d) – (\d)/g, '$1 $2');

html = html.replaceAll('€–€', ' – '); // Unguard

filterList.push(".oHL_reflist .oHL-spaced-range");

matchDescriptions["oHL-unspaced-endash"] = ["Unspaced en dash", "En dashes should usually have spaces surrounding them. (MOS:ENDASH; exception: MOS:ENBETWEEN)"];

html = html.replace(/([A-Z][^A-Z \n]+)–([A-Z])/g, '$1€–€$2'); // Guard Capital–Capital

html = html.replace(/([Ww]in)–([Ll]oss)/g, '$1€–€$2'); // Guard Win–loss

html = html.replace(/([Bb]lood)–([Bb]rain)/g, '$1€–€$2'); // Guard Blood–brain

html = html.replace(/([0-9]{2}s)–([0-9]{2})/g, '$1€–€$2'); // Guard 60s–70s

html = html.replace(/([0-9]th)–([0-9]+th)/g, '$1€–€$2'); // e.g. 12th–13th century

html = html.replace(/([A-Za-z])–/g, '$1');

html = html.replaceAll('€–€', '–'); // Unguard

filterList.push(".oHL_reflist .oHL-unspaced-endash", ".stub .oHL-unspaced-endash");

matchDescriptions["oHL-bad-minus"] = ["Bad minus sign", "Instead of a hyphen, minus signs should use the proper Unicode character. (; MOS:NEGATIVE)"];

html = html.replace(/([ >(])([-–—])(\d)/g, '$1$2$3');

html = html.replace(/ ([A-FO]{1,3})([-–—])([.,;:\)\n<"' ])/g, ' $1$2$3');

filterList.push(".oHL_reflist .oHL-bad-minus");

// Quotes

matchDescriptions["oHL-bad-quote"] = ["Bad quote mark", "Proper quote marks should be used."];

html = html.replace(/([`´]|'')/g, '$1');

html = html.replaceAll('′s', 's');

filterList.push(".oHL_img_info .oHL-bad-quote");

matchDescriptions["oHL-mismatched-quotes"] = ["Mismatched quotes", "Either double or single quotation marks should be consistently used."];

html = html.replace(/"(\w+)'(\W)/g, '"$1\'$2');

html = html.replace(/(\W)'(\w+)"/g, '$1\'$2"');

matchDescriptions["oHL-foreign-quote"] = ["Non-English quote mark", "Wikipedia only uses straight quote marks. (MOS:STRAIGHT)"];

html = html.replace(/([„«»‹›])/g, '$1');

filterList.push(".oHL_reflist .oHL-foreign-quote", ".tfd .oHL-foreign-quote");

// Nested quote mark

html = html.replace(/([.,;:] ?)""/g, '$1""');

matchDescriptions["oHL-adj-quote"] = ["Adjacent quote marks", "Adjacent quote marks should have their kerning adjusted. (see {{\" '}} and {{' \"}})"];

html = html.replace(/((?<= )"'|'"(?!>))/g, '$1');

filterList.push(".oHL_reflist .oHL-adj-quote");

matchDescriptions["oHL-punc-in-quote"] = ["Punctuation in quotes", "Punctuation in quotations should use the logical quotation style. (MOS:LQ)"];

html = html.replace(/([“‘"']\w+)([.,;:])(["'’”])/g, '$1$2$3');

filterList.push("blockquote .oHL-punc-in-quote", ".oHL_reflist .oHL-punc-in-quote");

// Italics for and s

matchDescriptions["oHL-aposS-italics"] = ["Apostrophe italics", "Apostrophes after italics should have their kerning adjusted. (see {{'s}} and {{'}})"];

html = html.replaceAll("Collier's", "Collier€s"); // Guard

html = html.replaceAll(/('s?)<\/i>/g, '$1');

//'s

html = html.replaceAll(/<\/i>('s)/g, '$1');

html = html.replaceAll("Collier€s", "Collier's"); // Unguard

// Punctuation in italics and bold

matchDescriptions["oHL-italpunc"] = ["Italicized punctuation", "Punctuation should not be italicized if it is not part of the title."];

matchDescriptions["oHL-boldpunc"] = ["Bolded punctuation", "Punctuation should not be bolded if it is not part of the title."];

html = html.replace(/(i\.e\.|e\.g\.|et al\.|etc\.)/g, '$1€'); // Guard

html = html.replace(/([A-Z])\./g, '$1€'); // Guard

html = html.replaceAll('...', '..€'); // Guard

html = html.replaceAll(' ', ' €'); // Guard

html = html.replace(/([.,;:"\])/])<\/i>/g, '$1');

html = html.replace(/([,;:\])/])<\/b>/g, '$1');

html = html.replace(/(i\.e\.|e\.g\.|et al\.|etc\.)€/g, '$1'); // Unguard

html = html.replace(/([A-Z])€/g, '$1.'); // Unguard

html = html.replaceAll('..€', '...'); // Unguard

html = html.replaceAll(' €', ' '); // Unguard

filterList.push(".oHL_reflist .oHL-italpunc", ".stub .oHL-italpunc",

".listen .oHL-italpunc", ".ambox .oHL-italpunc",

"blockquote i[lang] .oHL-italpunc");

filterList.push(".infobox td .oHL-boldpunc");

matchDescriptions["oHL-formatted-quotemark"] = ["Formatted quote mark", "Quotation marks should not be italicized or bolded. (except when part of a work’s title)"];

html = html.replace(/(<[ib]>)([“"'])/g, '$1$2');

matchDescriptions["oHL-formatted-bracket"] = ["Formatted bracket", "Brackets should not be italicized or bolded. (except when part of a work’s title)"];

html = html.replace(/(<[ib]>)([[(])/g, '$1$2');

filterList.push(".ambox .oHL-formatted-bracket");

// Text which should use

// html = html.replace(/([a-z]{2,}<\/i>)/g, '$1 [em]');

// Quote marks in bolded title

matchDescriptions["oHL-bold-quotemark"] = ["Bolded quote mark", "Quote marks around a bolded title should not be bolded themselves. (except when they are part of the title)"];

html = html.replace(/([^<\n]*)"/g, '$1"');

// Bolded parenthesis

matchDescriptions["oHL-boldparen"] = ["Bolded parenthesis", "Parentheses should not be italicized or bolded. (except when part of a work’s title)"];

html = html.replaceAll(')', ')');

// Bolded single letters

matchDescriptions["oHL-bolded-letter"] = ["Bolded letter", "Avoid boldface for emphasis or variables. (MOS:NOBOLD; MOS:TEXT#Mathematics_variables)"];

html = html.replace(/(\w)<\/b>/g, '$1');

filterList.push(".oHL_reflist .oHL-bolded-letter", ".navbox .oHL-bolded-letter",

".music-symbol .oHL-bolded-letter", ".serif-fonts .oHL-bolded-letter");

// Text without "lang"

// See: https://www.unicode.org/Public/UCD/latest/ucd/Scripts.txt

matchDescriptions["oHL-lang-han"] = ["Script missing lang tag (CJK)", "Non-English text should be tagged with its language. (MOS:FOREIGN)"];

html = html.replace(/([\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}]+)([\),])(?!<)/gu, '$1$2');

matchDescriptions["oHL-lang-kor"] = ["Script missing lang tag (Korean)", "Non-English text should be tagged with its language. (MOS:FOREIGN)"];

html = html.replace(/(\p{Script=Hangul}+)([\),])(?!<)/gu, '$1$2');

matchDescriptions["oHL-lang-cyrl"] = ["Script missing lang tag (Cyrillic)", "Non-English text should be tagged with its language. (MOS:FOREIGN)"];

html = html.replace(/(\p{Script=Cyrillic}+)([\),])(?!<)/gu, '$1$2');

matchDescriptions["oHL-lang-grk"] = ["Script missing lang tag (Greek)", "Non-English text should be tagged with its language. (MOS:FOREIGN)"];

html = html.replace(/(\p{Script=Greek}+)([\),])(?!<)/gu, '$1$2');

matchDescriptions["oHL-lang-deva"] = ["Script missing lang tag (Devanagari)", "Non-English text should be tagged with its language. (MOS:FOREIGN)"];

html = html.replace(/(\p{Script=Devanagari}+)([\),])(?!<)/gu, '$1$2');

matchDescriptions["oHL-lang-ara"] = ["Script missing lang tag (Arabic)", "Non-English text should be tagged with its language. (MOS:FOREIGN)"];

html = html.replace(/(\p{Script=Arabic}+)([\),])(?!<)/gu, '$1$2');

matchDescriptions["oHL-lang-heb"] = ["Script missing lang tag (Hebrew)", "Non-English text should be tagged with its language. (MOS:FOREIGN)"];

html = html.replace(/(\p{Script=Hebrew}+)([\),])(?!<)/gu, '$1$2');

matchDescriptions["oHL-lang-tam"] = ["Script missing lang tag (Tamil)", "Non-English text should be tagged with its language. (MOS:FOREIGN)"];

html = html.replace(/(\p{Script=Tamil}+)([\),])(?!<)/gu, '$1$2');

matchDescriptions["oHL-lang-arm"] = ["Script missing lang tag (Armenian)", "Non-English text should be tagged with its language. (MOS:FOREIGN)"];

html = html.replace(/(\p{Script=Armenian}+)([\),])(?!<)/gu, '$1$2');

matchDescriptions["oHL-lang-thai"] = ["Script missing lang tag (Thai)", "Non-English text should be tagged with its language. (MOS:FOREIGN)"];

html = html.replace(/(\p{Script=Thai}+)([\),])(?!<)/gu, '$1$2');

const langList = ["oHL-lang-han", "oHL-lang-kor", "oHL-lang-cyrl", "oHL-lang-grk",

"oHL-lang-deva", "oHL-lang-ara", "oHL-lang-heb","oHL-lang-tam",

"oHL-lang-arm","oHL-lang-thai"];

const bdiFilter = langList.map(l => ".cs1-prop-foreign-lang-source ." + l);

filterList.push(...bdiFilter);

// ISBNs

matchDescriptions["oHL-isbn"] = ["Unlinked ISBN", "ISBNs can be linked using the {{ISBN}} template. (WP:ISBN)"];

html = html.replace(/(ISBN [0-9X-]{3,})/, '$1');

// Ellipses

// Two ellipses

matchDescriptions["oHL-two-dots"] = ["Incomplete ellipsis", "Ellipses should have three dots."];

html = html.replace(/([^.])\.\.([^.])/g, '$1..$2');

filterList.push(".oHL_reflist .oHL-two-dots", "a[href*='adsabs.harvard.edu'] .oHL-two-dots");

// html = html.replaceAll('…', '');

// filterList.push(".oHL_reflist .oHL-ellipsis-char");

// Four ellipses

matchDescriptions["oHL-four-dots"] = ["Extra ellipsis period", "Ellipses should have three dots."];

html = html.replace(/([\[\( ])\.\.\.\./g, '$1....');

// Interspaced ellipses

matchDescriptions["oHL-spaced-ellipsis"] = ["Interspaced ellipsis", "Ellipses should not have spaces in between. (MOS:ELLIPSES)"];

html = html.replace(/(\. \. \.|\. \. \.)/g, '. . .');

// Bracketed ellipses

matchDescriptions["oHL-bracketed-ellipsis"] = ["Bracketed ellipsis", "Ellipses indicating omission in a quote should not be enclosed by square brackets. (MOS:BRACKET; Exception: if the quote itself already uses ellipses.)"];

html = html.replaceAll(/((]\.\.\.[)\)/g, '$1');

// Unspaced ellipses

matchDescriptions["oHL-unspaced-ellipsis"] = ["Unspaced ellipsis", "Ellipses should have spaces surrounding them. (MOS:ELLIPSES)"];

html = html.replaceAll(" ", "€€"); // guard

html = html.replaceAll(/([^ "'€])\.\.\.([^ .?!:;,)\]])/g, '$1...$2');

html = html.replaceAll(/(&[^ "'€])\.\.\. /g, '$1... ');

html = html.replaceAll(/ \.\.\.([^ .?!:;,)\]])/g, ' ...$1');

html = html.replaceAll("€€", " "); // unguard

filterList.push(".oHL_reflist .oHL-unspaced-ellipsis", ".oHL_wikilink .oHL-unspaced-ellipsis",

"a.new .oHL-unspaced-ellipsis", ".oHL-four-dots .oHL-unspaced-ellipsis",

"a[href*='adsabs.harvard.edu'] .oHL-unspaced-ellipsis");

// Non-breaking spaces

matchDescriptions["oHL-nbsp-multi"] = ["Multiple non-breaking spaces", "Only a single NBSP should be between words. They should also not be used to force formatting, instead, semantic elements or CSS should be used."];

html = html.replace(/((?: ){2,})/g, '$1');

filterList.push(".poem .oHL-nbsp-multi", "table .oHL-nbsp-multi",

".quotebox .oHL-nbsp-multi");

matchDescriptions["oHL-bad-nbsp"] = ["Spaced non-breaking space", "A non-breaking space should not have any spaces around it."];

html = html.replace(/(  |  )/g, '$1');

filterList.push(".mw-kartographer-map ~ div .oHL-bad-nbsp");

// Note: units, am/pm handled elsewhere

matchDescriptions["oHL-nbsp"] = ["Non-breaking space", "Numbers, ellipses, etc. should be preceded by &nbsp;. (MOS:NBSP)"];

html = html.replace(/([0-9]) (dozen|hundred|thousand|million|billion|trillion)/g, "$1 $2");

html = html.replaceAll(" ...", " ...");

filterList.push(".oHL_reflist .oHL-nbsp", ".infobox .oHL-nbsp",

".navbox .oHL-nbsp", "th .oHL-nbsp", ".nowrap .oHL-nbsp",

".texhtml .oHL-nbsp", ".oHL_img_info .oHL-nbsp");

// Whitespace

matchDescriptions["oHL-unspaced-period"] = ["Unspaced period", "A full stop should be followed by a space."];

html = html.replaceAll('Ph.D.', 'Ph€D.'); // Guard "Ph.D."

html = html.replace(/([a-z]\.[A-Z])([^A-Z])/g, '$1$2');

html = html.replaceAll('Ph€D.', 'Ph.D.'); // Unguard

matchDescriptions["oHL-spaced-punc"] = ["Spaced punctuation", "Punctuation should not be preceded by a space. (MOS:PUNCTSPACE)"];

html = html.replace(/( | )\.\.\./g, '$1€.€€.€€.€'); // Guard ellipses

html = html.replaceAll('. . .', '€.€ €.€ €.€'); // Guard spaced ellipses

html = html.replace(/\.([0-9]{2,})/g, '€€$1'); // Guard gun calibers

html = html.replace(/(( | )[,;:.?!%])/g, '$1');

html = html.replaceAll('€.€', '.'); // Unguard

html = html.replaceAll('€€', '.'); // Unguard

matchDescriptions["oHL-full-space"] = ["Fullwidth space", "Half-width spaces should be used instead of full-width ones. (Exception: inside Japanese text)"];

html = html.replaceAll(' ', ' ');

matchDescriptions["oHL-spaced-chars"] = ["Spaced characters", "Characters should not have spacing between them."];

html = html.replace(/ (["'“‘’”\[\]()]) /g, ' $1 ');

matchDescriptions["oHL-spaced-ref"] = ["Spaced reference", "References should not be preceded by a space. (MOS:REFSPACE)"];

html = html.replaceAll('

matchDescriptions["oHL-unspaced-ref"] = ["Unspaced reference", "References should be followed by a space."];

html = html.replace(/\/sup>(\w)/g, '/sup>$1');

html = html.replace(/\/sup>(\w)/g, '/sup>$1');

// Inches/feet marks

matchDescriptions["oHL-ft-inch"] = ["Height notation", "The symbols for feet and inches should be spelled out. (MOS:NUM#Specific_units)"];

html = html.replace(/(\d' [1-9]\d?")/g, '$1');

// Multiplication

matchDescriptions["oHL-mult-sign"] = ["Multiplication sign", "Multiplication should use the proper Unicode character, ×, instead of the Latin letter x. (MOS:MATH#Multiplication_sign)"];

html = html.replace(/ x(64|86)/g, ' €€$1'); // Guard

html = html.replaceAll(' 0x', ' 0€€'); // Guard

html = html.replace(/(annotation_+[0-9]+)x/g, '$1€€'); // Guard

html = html.replace(/(\d ?)x/g, '$1x');

html = html.replace(/ x /g, ' x ');

html = html.replaceAll('€€', 'x'); // Unguard

// Parenthesis

matchDescriptions["oHL-unspaced-paren"] = ["Unspaced parenthesis", "Parenthesis should have spaces around them. (MOS:PAREN; exception: function names in programming)"];

// Trying to match parenthesis hitting words like this(

html = html.replaceAll('()', '€€)'); // Guard function names

html = html.replace(/([a-z"]\()/g, '$1');

html = html.replaceAll('€€)', '()'); // Unguard

filterList.push(".Inline-Template .oHL-unspaced-paren", ".infobox-label .oHL-unspaced-paren");

// Trying to match parenthesis hitting words like )this

html = html.replace(/(\)[A-Za-z])/g, '$1');

matchDescriptions["oHL-adj-parens"] = ["Adjacent parentheses", "Avoid adjacent parenthesis. (MOS:PAREN)"];

html = html.replace(/(\) ?\()/g, '$1');

filterList.push(".oHL_reflist .oHL-adj-parens");

matchDescriptions["oHL-nested-parens"] = ["Nested parentheses", "Nested parentheticals should utilize square brackets ([])."];

html = html.replace(/(\([^)\n]+)\(/g, '$1(');

// html = html.replaceAll('))', '))');

filterList.push(".oHL_img_info .oHL-nested-parens");

// Minus sign

matchDescriptions["oHL-minus-score"] = ["Minus sign", "The proper Unicode minus sign, , should be used instead of dashes. (MOS:MINUS)"];

html = html.replace(/( [A-F])([-–—])([.,;: ])/g, ' $1$2$3');

// Note: have never encountered this highlight.

matchDescriptions["oHL-plus-minus"] = ["Plus/minus sign", "The proper Unicode plus-minus sign, ±, should be used."];

html = html.replace(/(\+\/[-–—−])/g, '$1');

// Exponents and subscripts

matchDescriptions["oHL-subsup"] = ["Precomposed sub/superscript", "Instead of precomposed Unicode characters for subscripts or superscripts, use <ref> tags, or <sub>; and <sup> tags. (MOS:SUPERSCRIPT)"];

html = html.replace(/([²³¹⁰ⁱ⁴⁵⁶⁷⁸⁹⁺⁻⁼⁽⁾ⁿ₀₁₂₃₄₅₆₇₈₉₊₋₌₍₎ₐₑₒₓₔₕₖₗₘₙₚₛₜᴬᴮᴰᴱᴳᴴᴵᴶᴷᴸᴹᴺᴼᴾᴿᵀᵁⱽᵂᶦᶫᶰᶸᵃᵇᶜᵈᵉᶠᵍʰⁱʲᵏˡᵐⁿᵒᵖʳˢᵗᵘᵛʷˣʸᶻₐₑₕᵢⱼₖₗₘₙₒₚᵣₛₜᵤᵥₓᵝᵞᵟᶿᶥᵠᵡᵦᵧᵨᵩᵪ⏨])/g, '$1');

// Fractions

// html = html.replace(/([½¼¾⅐⅑⅒⅓⅔⅕⅖⅗⅘⅙⅚⅛⅜⅝⅞⅟↉])/g, '$1 ');

matchDescriptions["oHL-frac-slash"] = ["Fraction slash", "Fractions should use the {{frac}} template. (MOS:FRAC)"];

html = html.replace(/([0-9])\/([0-9])/g, '$1/$2');

html = html.replaceAll("sup>//

filterList.push(".oHL_reflist .oHL-frac-slash", ".video-game-reviews .oHL-frac-slash",

".external .oHL-frac-slash", ".navbox .oHL-frac-slash");

// Ordinals

matchDescriptions["oHL-ordinal"] = ["Ordinal", "Do not superscript ordinals. (MOS:ORDINAL)"];

html = html.replace(/(th|st|[nr]d)/g, '$1');

html = html.replace(/([ªº])/g, '$1');

// Substed nihongo question mark

matchDescriptions["oHL-nihongo-question"] = ["Substed nihongo template", "The {{nihongo}} template should be be substituted."];

html = html.replaceAll('?', '?');

// Precomposed units

matchDescriptions["oHL-unit-char"] = ["Precomposed unit", "Do not use precomposed unit symbols. (MOS:UNITSYMBOLS)"];

html = html.replace(/([㎚㎛㎜㎝㎞㏌㎟㎠㎡㎢㎣㎤㎥㎦㎕㎖㎗㎘㏄㎰㎱㎲㎳㎍㎎㎏㎅㎆㎇㎐㎑㎒㎓㎔㎴㎵㎶㎷㎸㎹㎺㎻㎼㎽㎾㎿㏀㏁㎀㎁㎂㎃㎄㎧㎨㎭㎮㎯㎩㎪㎫㎬㎈㎉㍷㍸㍹㎙㍱㍲㍳㍴㍵㍶㍺㎊㎋㎌㏃㏅㏆㏇㏈㏉㏊㏋㏍㏎㏏㏐㏑㏒㏓㏔㏕㏖㏗㏚㏛㏜㏝㏞㏟㏿㏂㏘㏙])/g, '$1');

// Unspaced unit

matchDescriptions["oHL-unit-space"] = ["Unspaced unit", "Unit symbols should usually be preceded by a non-breaking (&nbsp;) space. (MOS:UNITSYMBOLS)"];

html = html.replace(/([0-9])(in|ft|mi|mph|cm|μm|mm|km|mg|kg|g|m|psi|oz|qt|gal|lb|yr|kcal|cal|hz|sq|cu|W|kW|Ah|mAh|ohm)/g, '$1[ ]$2');

filterList.push(".oHL_reflist .oHL-unit-space", ".mw-kartographer-map ~ div .oHL-unit-space");

// Hyphenated unit

matchDescriptions["oHL-unit-hyphen"] = ["Hyphenated unit", "Unit symbols should not be preceded by a hyphen. (MOS:UNITSYMBOLS)"];

html = html.replace(/([0-9])-(in|ft|mi|mph|cm|μm|mm|km|mg|kg|g|m|psi|oz|qt|gal|lb|yr|kcal|cal|hz|sq|cu|W|kW|Ah|mAh|ohm)([ .,;:'"\)])/g, '$1-$2$3');

// Dotted unit name

matchDescriptions["oHL-dotted-unit"] = ["Dotted unit", "Unit symbols should not have a dot. (MOS:UNITSYMBOLS)"];

html = html.replace(/([0-9])( | )(in|ft|mi|mph|cm|μm|mm|km|mg|kg|g|m|psi|oz|qt|gal|lb|yr|kcal|cal|hz|sq|cu|W|kW|Ah|mAh|ohm)\./g, '$1$2$3.');

// Brackets

matchDescriptions["oHL-bracket"] = ["Unparsed brackets", "Unparsed brackets likely indicate a broken template or wikilink."];

html = html.replace(/(\{\

');

filterList.push(".chessboard .oHL-missing-placeholder");

// Circa

matchDescriptions["oHL-circa"] = ["Circa", "The preferred formatting for circa is the {{circa}} template. (MOS:CIRCA)"];

html = html.replaceAll(' ca.', ' ca.');

html = html.replaceAll(' ca ', ' ca ');

html = html.replace(/([ \(])c\.(\w)/g, '$1c.$2');

// Missing dots in abbreviations

matchDescriptions["oHL-abbr-period"] = ["Abbreviation dot", "Abbreviations should end with periods. (MOS:POINTS)"];

html = html.replace(/([ (])(etc|i\.e|e\.g|cf|et al|viz|vs|Inc|Jr|Sr)([ ),:;'"])/g, ' $1$2[.]$3');

filterList.push(".oHL_reflist .oHL-abbr-period", ".oHL_wikilink .oHL-abbr-period",

"a.new .oHL-abbr-period", ".external .oHL-abbr-period");

// Inconsistent slash spacing

matchDescriptions["oHL-slash-space"] = ["Slash spacing", "Slashes, when considered proper to use, should have consistent spacing on both sides. (MOS:SLASH)"];

html = html.replaceAll(' / ', ' /€'); // Guard

html = html.replace(/([^\/ ])\/ /g, '$1/ ');

html = html.replace(/ \/([^\/ ])/g, ' /$1');

html = html.replaceAll(' /€', ' / '); // Unguard

// Fullwidth characters

matchDescriptions["oHL-fullwidth"] = ["Fullwidth characters", "Fullwidth characters should be replaced with their halfwidth equivalents."];

html = html.replace(/([:;?!$%()&+*@])/g, '$1');

filterList.push("[lang='ja'] .oHL-fullwidth",

".oHL_reflist .oHL-fullwidth");

// Time

matchDescriptions["oHL-time-space"] = ["Time space", "Times should have a non-breaking space (&nbsp;) before a.m./p.m. (MOS:AMPM)"];

html = html.replace(/([0-9])([ap]\.?m)/gi, '$1[ ]$2');

filterList.push(".oHL_reflist .oHL-time-space");

matchDescriptions["oHL-time-uppercase"] = ["Time uppercase", "a.m./p.m. notation should be lowercase. (MOS:AMPM)"];

html = html.replace(/([0-9]) ([AP](M|\.M\.))/g, '$1 $2');

filterList.push(".oHL_reflist .oHL-time-uppercase");

matchDescriptions["oHL-time-dot"] = ["Time dot", "a.m./p.m. notation should have two dots. (MOS:AMPM)"];

html = html.replace(/(a\.m|p\.m)([ ),:;'"])/g, '$1[.]$2');

// Commas after place names

matchDescriptions["oHL-place-comma"] = ["Place name comma", "A comma should follow the last part of a geographic location. (MOS:GEOCOMMA)"];

const stateNamesRe = stateNames.join("|").replaceAll(".", "\\.");

const countryNamesRe = countryNames.join("|").replaceAll(".", "\\.");

const stateRe = new RegExp(", (" + stateNamesRe + ")([ <])", "g");

const stateRe2 = new RegExp("(, (?:\n]*?>))(" + stateNamesRe + ")<", "g"); // wikilinked

const countryRe = new RegExp(", (" + countryNamesRe + ")([ <])", "g");

const countryRe2 = new RegExp("(, (?:\n]*?>))(" + countryNamesRe + ")<", "g"); // wikilinked

html = html.replace(/([0-9]),/g, '$1,₭'); // guard

html = html.replace(stateRe, ', $1€€$2');

html = html.replace(stateRe2, '$1$2€€<');

html = html.replace(countryRe, ', $1€€$2');

html = html.replace(countryRe2, '$1$2€€<');

html = html.replaceAll('€€', '€€'); // neater

html = html.replaceAll('€€<', '<'); // clean

html = html.replaceAll('€€\n<', '\n<'); // clean

html = html.replaceAll('€€

html = html.replaceAll('€€ –', ' –'); // clean

html = html.replace(/€€([,.:;)\]])/g, '$1'); // clean

html = html.replaceAll('€€', '[,]');

html = html.replaceAll(',₭', ','); // unguard

filterList.push(".oHL_reflist .oHL-place-comma",

".hatnote .oHL_wikilink + .oHL-place-comma",

".hatnote .oHL_wikilink .oHL-place-comma");

// Title italicization

matchDescriptions["oHL-title-italics"] = ["Title italics", "Instances of the italicized page title should be consistently italicized in the body."];

const pageTitle = mw.config.get("wgTitle").replace(/ \(.*\)$/, "");

if (pageTitle == $("h1 i").first().text()) {

const pageTitleCleaned = pageTitle.replace(/:.*/, "");

const pageTitleEscaped = mw.util.escapeRegExp(pageTitleCleaned);

const titleRe = new RegExp("([\(\"' ])(" + pageTitleEscaped + ")", "gi");

html = html.replace(titleRe, '$1$2');

filterList.push("i .oHL-title-italics", ".oHL_reflist .oHL-title-italics",

"a .oHL-title-italics, .shortdescription .oHL-title-italics",

".oHL_img_info .oHL-title-italics", ".hatnote .oHL-title-italics");

}

matchDescriptions["oHL-court-italics"] = ["Court case italics", "Titles of court cases are usually italicized. (MOS:TEXT#Names_and_titles)"];

// Court cases italicization

html = html.replace(/ (v\.?) /g, ' $1 ');

filterList.push("i .oHL-court-italics", ".oHL_reflist .oHL-court-italics",

".infobox-above .oHL-court-italics", ".hatnote .oHL-court-italics");

// Full name in biographies

matchDescriptions["oHL-fullname"] = ["Full name", "After the first mention, people should not be referred to by their full name. (MOS:SURNAME)"];

const categories = mw.config.get("wgCategories")?.join(" | ");

if (categories.includes("births")

`;

}

const refContentsRe = />([^<]*)<\/ref/i;

for (const ref of plainRefs) {

let url = "—";

let title;

const formattedLinkMatch = ref.match(/\[(http[^ ]*) ([^\]]*)\]/i);

const numberedLinkMatch = ref.match(/\[(http[^ ]+)\]/i);

const plainURLMatch = ref.match(/(http[^ <]*)/i);

if (formattedLinkMatch) {

url = formattedLinkMatch[1];

title = formattedLinkMatch[2];

} else if (numberedLinkMatch) {

url = numberedLinkMatch[1];

title = ref.match(refContentsRe)[1];

} else if (plainURLMatch) {

url = plainURLMatch[1];

if (/[.,;:"]$/.test(url)) {

url = url.slice(0, -1);

}

title = ref.match(refContentsRe)[1];

} else { // ref contains no URLs

title = ref.match(refContentsRe)[1];

}

const refEscaped = ref.replaceAll('"', '"');

if (url != "—") {

title = `${title}`;

} else {

title = `${title}`;

}

const ordinal = getRefOrdinalFromURL(url, ref, "N/A");

let protocol = "—";

if (url != "—") { protocol = url.startsWith("https:") ? "🔐" : "🔓"; }

tableContent += `

`;

}

const table = tableHeader + tableContent + "

\[\[|\]\]|\}\}|\{\\|\})/g, '$1');

filterList.push(".navbox .oHL-bracket");

// Malformed tags

matchDescriptions["oHL-bad-tag"] = ["Malformed tag", "A tag was not properly closed."];

html = html.replace(/(<\/?(ref|blockquote|poem|math|chem))/g, '$1');

// Malformed header

matchDescriptions["oHL-broken-header"] = ["Malformed header", "Header markup should use an even number of equal signs on both sides."];

html = html.replace(/=(<\/h[2-6])/g, '=$1');

// Unspaced comma

matchDescriptions["oHL-unspaced-comma"] = ["Unspaced comma", "Commas should usually have a space after them."];

html = html.replace(/([a-z]),([A-Za-z])/g, '$1,$2');

// Commas in money

matchDescriptions["oHL-money-comma"] = ["Thousands separator (money)", "In general, digits are grouped by commas. (MOS:DIGITS)"];

html = html.replace(/([$€£¥₣₹])(\d{4})/g, '$1$2');

filterList.push(".oHL_reflist .oHL-money-comma");

// Commas in five digit plus numbers

matchDescriptions["oHL-digit-comma"] = ["Thousands separator", "Digits should be grouped by commas. (MOS:DIGITS)"];

html = html.replace(/(\d{2,})(\d{3})/g, '$1€€$2');

html = html.replace(/(\.[0-9]+)€€/g, '$1'); // filter decimals out

html = html.replace(/([A-Za-z]\w+)€€/g, '$1'); // filter model numbers out

html = html.replace(/(="[0-9]+)€€([0-9]+")/g, '$1$2'); // filter HTML attributes out

html = html.replaceAll('€€', '[,]');

filterList.push(".external .oHL-digit-comma",

".oHL_reflist .oHL-digit-comma",

".extiw .oHL-digit-comma",

".navbox .oHL-digit-comma",

".oHL_img_info_dimensions .oHL-digit-comma",

".oHL-isbn .oHL-digit-comma");

// Postfix currency symbols

matchDescriptions["oHL-currency-postfix"] = ["Currency symbol placement", "Currency symbols usually precede the amount. (MOS:CURRENCY)"];

html = html.replace(/([ (][0-9,.]+)( | )?([$€£¥₣₹])/g, '$1$2$3');

// 9s at the end of prices, a form of psychological pricing

matchDescriptions["oHL-excess-precision"] = ["Excess precision (money)", "In most cases, large monetary figures do not necessitate a lot of precision. (MOS:LARGENUM)"];

html = html.replace(/([$€£¥₣₹][0-9,]+)(9{2,})([^0-9])/g, '$1$2$3');

filterList.push("blockquote .oHL-excess-precision");

// Double punctuation

matchDescriptions["oHL-doublepunc"] = ["Double punctuation", "Punctuation should only be present once."];

html = html.replaceAll(' ', '€nbsp€'); // Guard

html = html.replace(/(,,|;;|::)/g, '$1');

html = html.replaceAll('€nbsp€', ' '); // Unguard

// Punctation after citations

matchDescriptions["oHL-cite-punc"] = ["Citation punctuation", "References should be placed after punctuation. (MOS:CITEPUNCT)"];

html = html.replace(/<\/sup>([.,;:])/g, '$1');

filterList.push("sup:not(.reference):not(.Inline-Template) + .oHL-cite-punc"); // Actual exponents

// Extra punctuation after inline quote

matchDescriptions["oHL-extra-punc"] = ["Extra punctuation", "Quotations with terminal punctuation shouldn’t usually have another punctuation mark after them. (MOS:CONSECUTIVE)"];

// html = html.replace(/(\?["'])\./g, '$1.');

html = html.replace(/(\.["'’”])([,.])/g, '$1$2');

filterList.push(".oHL_reflist .oHL-extra-punc");

// Extra perid after parenthetical

matchDescriptions["oHL-extra-period"] = ["Extra period", "Quotations with terminal punctuation shouldn’t usually have another punctuation mark after them. (MOS:CONSECUTIVE)"];

html = html.replace(/(\. \([^)]+\))\./g, '$1.');

filterList.push(".oHL_reflist .oHL-extra-period");

// Curly quotes

matchDescriptions["oHL-curly-quote"] = ["Curly quote mark", "Wikipedia only uses straight quote marks. (MOS:STRAIGHT)"];

html = html.replace(/([‘’“”])/g, '$1');

// Date comma

matchDescriptions["oHL-datecomma"] = ["Date comma", "Dates in MDY format require a comma after the year. (MOS:DATECOMMA)"];

html = html.replace(/((?:(?:Jan|Febr)uary|March|April|May|June|July|August|(?:Septem|Octo|Novem|Decem)ber) [0-9]{1,2})( [0-9])/g, '$1[,]$2');

html = html.replace(/((?:(?:Jan|Febr)uary|March|April|May|June|July|August|(?:Septem|Octo|Novem|Decem)ber) [0-9]{1,2}, [0-9]{4})( \w|[,]$2');

// Also filtered in whitelist()

filterList.push(".oHL_reflist .oHL-datecomma", ".wikitable .oHL-datecomma");

// "In $year" comma

matchDescriptions["oHL-yearcomma"] = ["Year comma", "This type of clause should probably have a comma after it."];

html = html.replace(/((?:In|As of) ?(?:(?:Jan|Febr)uary|March|April|May|June|July|August|(?:Septem|Octo|Novem|Decem)ber)? [0-9]{4})( \w|[,]$2');

// Start of sentence comma

matchDescriptions["oHL-word-comma"] = ["Word comma", "This type of clause should probably have a comma after it."];

html = html.replaceAll('Recently ', 'Recently[,] ');

html = html.replaceAll(/Originally (?!known)/g, 'Originally[,] ');

// html = html.replace(/([a-z]) (but|whereas) /g, '$1[,] $2 ');

filterList.push(".oHL_reflist .oHL-word-comma", ".oHL_wikilink .oHL-word-comma");

// Double conjunction

// html = html.replace(/ and ([^"'.,;:!()[\]—–\n]+) and/g, ' and $1 and');

// html = html.replace(/ or ([^"'.,;:!()[\]—–\n]+) or/g, ' or $1 or');

// Short months

matchDescriptions["oHL-month-abbr"] = ["Abbreviated month", "Abbreviations for months should only be used where space is limited. (WP:MOS#Months)"];

html = html.replace(/((?:Jan|Feb|Mar|Apr|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\.?) ([0-9]{1,2})/g, '$1 $2');

filterList.push(".oHL_reflist .oHL-month-abbr",

"table .oHL-month-abbr"); // table includes .infobox

// Short days

matchDescriptions["oHL-day-abbr"] = ["Abbreviated day", "Abbreviations should only be used where space is limited."];

html = html.replace(/(Mon|Tues|Wed|Thur|Fri|Sat|Sun)([^\w])/g, '$1$2');

filterList.push(".oHL_reflist .oHL-day-abbr", ".external .oHL-day-abbr",

".oHL_wikilink .oHL-day-abbr", "a.new .oHL-day-abbr",

".wikitable .oHL-day-abbr");

// All caps

matchDescriptions["oHL-allcaps"] = ["All caps", "Avoid using all capital letters for things other than acronyms. (MOS:ALLCAPS)"];

html = html.replace(/([A-Z]{3,} [ "',;:.?!A-Z]*[A-Z]{3,})/g, '$1');

filterList.push(".smallcaps .oHL-allcaps", ".navbox .oHL-allcaps", ".Inline-Template .oHL-allcaps");

// Hyphen table placeholders

matchDescriptions["oHL-table-hyphen"] = ["Table placeholder hyphen", "Placeholders should use em dashes instead of hyphens."];

html = html.replaceAll('

-', '-');

// Empty cells

matchDescriptions["oHL-missing-placeholder"] = ["Table cell w/o placeholder", "Empty cells in a table should probably use em dashes as placeholders."];

html = html.replace(/

\s*<\/td>/g, '[—] categories.includes("deaths")categories.includes("Living people")) {

const fullNameTitle = mw.config.get("wgTitle").replace(/ \([^)]+\)$/, "");

const fullNameLead = $("#mw-content-text p b").first().text();

const subjectNames = [];

subjectNames.push(fullNameTitle);

if (fullNameLead != fullNameTitle) {

subjectNames.push(fullNameLead);

}

for (const name of subjectNames) {

if (name.includes(" ")) {

html = html.replaceAll(name, '' + name + '');

}

}

// We also filter by subsection in whitelist()

filterList.push("#mw-content-text p:first-of-type .oHL-fullname",

"i .oHL-fullname", "a .oHL-fullname", "table .oHL-fullname",

"figcaption .oHL-fullname", ".oHL_reflist .oHL-fullname",

".reference-text .oHL-fullname", ".side-box .oHL-fullname",

".hatnote .oHL-fullname", ".sister-bar .oHL-fullname",

".oHL_img_info .oHL-fullname", ".quotebox .oHL-fullname");

}

// Pseudo-references

matchDescriptions["oHL-pseudo-ref"] = ["Pseudo ref", "Numbered references should use <ref> tags."];

html = html.replace(/(\[\d{1,2}\])/g, '$1');

filterList.push(".reference .oHL-pseudo-ref", ".external .oHL-pseudo-ref", ".oHL_reflist .oHL-pseudo-ref");

// Degrees symbol

matchDescriptions["oHL-bad-degree"] = ["Bad degree symbol", "Degrees should use the proper symbol. (°; MOS:NUM#Specific_units)"];

html = html.replaceAll('˚', '˚');

matchDescriptions["oHL-missing-degree"] = ["Missing degrees symbol", "Temperatures should have the degrees symbol. (°; MOS:NUM#Specific_units)"];

html = html.replace(/([0-9]) (C|F)([ .,;:)])/g, '$1 [°]$2$3');

matchDescriptions["oHL-unspaced-degree"] = ["Unspaced degrees symbol", "The degrees symbol should be spaced. (MOS:NUM#Specific_units)"];

html = html.replace(/([0-9])°(C|F)/g, '$1[ ]°$2');

// Misc

matchDescriptions["oHL-corporate"] = ["Corporate symbol", "Avoid using symbols like , etc. (MOS:TMRULES)"];

html = html.replace(/([™©®]|\(TM\)|\(C\)|\(R\))/ig, '$1');

filterList.push(".oHL_reflist .oHL-corporate");

/* html = html.replace(/([0-9]+°) ([0-9]+)′ ([0-9]+)″/g, '$1 $2€€ $3€€€'); // Guard

matchDescriptions["oHL-prime"] = ["Prime symbol", "Outside of angles and coordinates, the prime symbols shouldn't be used. (MOS:UNITS)"];

html = html.replaceAll('€€€', '″'); // Unguard

html = html.replaceAll('€€', '′'); // Unguard

html = html.replace(/([′″])/g, '$1');

  • /

matchDescriptions["oHL-unit-symbol"] = ["Inch & feet symbols", "“in” and “ft” should be used instead of quote marks. (MOS:NUM#Specific_units)"];

html = html.replace(/("\w.*?[0-9])"/g, '$1€€"'); // Guard

html = html.replace(/([0-9])'s/g, "$1€€'s"); // Guard

html = html.replace(/( [0-9]+)(['"])/g, '$1$2');

html = html.replaceAll("€€'", "'"); // Unguard

html = html.replaceAll('€€"', '"'); // Unguard

filterList.push(".oHL_reflist .oHL-unit-symbol");

matchDescriptions["oHL-bad-bullet"] = ["Bad bullet", "Lists on Wikipedia should use the proper list markup. (MOS:LISTBULLET)"];

html = html.replaceAll('•', '');

filterList.push(".infobox-label .oHL-bad-bullet");

matchDescriptions["oHL-spaced-amper"] = ["Ampersand", "Normal text should use “and” instead of the ampersand. (MOS:AMP)"];

html = html.replace(/ & ([A-Z])/g, ' €amp€ $1'); // Guard

html = html.replaceAll(' & ', ' & ');

html = html.replaceAll('€amp€', '&'); // Unguard

filterList.push(".oHL_reflist .oHL-spaced-amper", ".oHL_wikilink .oHL-spaced-amper",

"a.new .oHL-spaced-amper", "i .oHL-spaced-amper", "table .oHL-spaced-amper",

".reference-text .oHL-spaced-amper", "blockquote .oHL-spaced-amper");

// Never actually hit this one

matchDescriptions["oHL-spaced-el"] = ["Spaced el", "An “el” (l) seems to have been typed instead of “eye” (I)."];

html = html.replaceAll(' l ', ' l ');

matchDescriptions["oHL-unspaced-pgnum"] = ["Unspaced page number", "Page number abbreviations in citations should be spaced. (see examples at WP:CITE)"];

html = html.replace(/, p\.([0-9])/g, ', p.[ ]$1');

html = html.replace(/pp\.([0-9])/g, 'pp.[ ]$1');

matchDescriptions["oHL-unspaced-ordinal"] = ["Unspaced ordinal", "Ordinal abbreviations should be followed by a space."];

html = html.replace(/([Nn]o\.)([0-9])/g, '$1[ ]$2');

filterList.push(".oHL_reflist .oHL-unspaced-ordinal");

matchDescriptions["oHL-ascii-symbol"] = ["ASCII symbols", "Symbols should use the proper Unicode characters. (WP:MOS#Symbols)"];

html = html.replace(/( |<

)->/g, '$1->');

html = html.replace(/<-(

)/g, '<-$1');

html = html.replace(/ (>|<|~)=/g, ' $1=');

filterList.push(".oHL_reflist .oHL-ascii-symbol");

// Contractions

matchDescriptions["oHL-contraction"] = ["Contraction", "Contractions should not be used outside of quoted text. (MOS:CONTRACTIONS)"];

// Try guarding up to four contractions

// Guards won't work if HTML tags in between, e.g. `He said that's…``

html = html.replace(/("\w[^"]*?)'([^'"]*)'([^'"]*)'([^'"]*)'/g, "$1'€$2'€$3'€$4'€"); // Guard

html = html.replace(/("\w[^"]*?)'([^'"]*)'([^'"]*)'/g, "$1'€$2'€$3'€"); // Guard

html = html.replace(/("\w[^"]*?)'([^'"]*)'/g, "$1'€$2'€"); // Guard

html = html.replace(/("\w[^"]*?)([a-z])'([a-z])/g, "$1$2'€$3"); // Guard; this one also checks for surrounding letters

html = html.replaceAll("n't", "n't");

html = html.replaceAll("'ve", "'ve");

html = html.replace(/(\w)'d/g, '$1\'d');

html = html.replaceAll("'ll", "'ll");

html = html.replaceAll("they're", "they're");

html = html.replaceAll("might've", "might've");

html = html.replaceAll("that's", "that's");

html = html.replaceAll(/ (t?here's)/g, " $1");

html = html.replace(/ (s?he's)/g, " $1");

html = html.replaceAll(" it's", " it's");

html = html.replaceAll("who's", "who's");

html = html.replaceAll("something's", "something's");

html = html.replace(/'€+/g, "'"); // Unguard

filterList.push(".oHL_reflist .oHL-contraction", ".oHL_wikilink .oHL-contraction",

"a.new .oHL-contraction", "i .oHL-contraction", "blockquote .oHL-contraction",

".poem .oHL-contraction");

// Editorial issues

// html = html.replaceAll('and/or', '$&');

// filterList.push("blockquote .oHL-editorializing", ".reference-text .oHL-editorializing",

// ".oHL_wikilink .oHL-editorializing", "a.new .oHL-editorializing");

// Annotate non-visible or hard to distinguish elements

html = html.replaceAll('×', '×mult');

html = html.replaceAll(' ', ' _nbsp_');

html = html.replaceAll(' ', '_thinsp_');

html = html.replaceAll(' ', '_hairsp_');

html = html.replaceAll('​', '_ZeroWidthSpace_');

html = html.replaceAll('­', '­_shy_');

html = html.replaceAll('–', 'en');

html = html.replaceAll('—', 'em');

html = html.replaceAll('−', 'minus');

html = html.replaceAll('‐', 'hyphen');

html = html.replaceAll('\u2011', 'nb hyphen');

html = html.replaceAll('ʼ', 'ʼglottal');

// Trailing spaces

html = html.replace(/ \n/g, '_\n');

contentElement.innerHTML = html;

// Make sure we didn't improperly unguard something

const euroCountAfter = (html.match(/€/g)

[]).length;

if (euroCountBefore != euroCountAfter) {

printExternalWarning(`Guard character count changed from ${euroCountBefore} to ${euroCountAfter}. Page text might be formatted incorrectly from original.`);

}

const brokenHTML = html.match(/]+

if (brokenHTML != null && brokenHTML.length != 0) {

const brokenHTMLMessage = mw.html.escape(brokenHTML.toString());

printExternalWarning(`Might have broken the page HTML: ${brokenHTMLMessage}`);

}

}

function postClean() {

// Put back original attributes

// We iterate backwards because we target the mangled ids and we don't want

// them to change back until the end

for (let i = mangled.length-1; i >= 0; i--) {

unmangle(mangled[i]);

}

// Reattach the elements we removed at the start

reattachTemp();

whitelist();

// Optionals

$(".oHL_reflist .oHL, .navbox .oHL").each(function markOptionalsSelector() {

$(this).addClass("oHL-opt");

$(this).removeClass("oHL");

});

$(refSectionsSelector).parent().nextUntil(".mw-heading2").find(".oHL").each(function markOptionalsSection() {

$(this).addClass("oHL-opt");

$(this).removeClass("oHL");

});

// Handle elements tagged multiple times

$(".oHL.oHL-opt").removeClass("oHL-opt");

$(".infobox tr .oHL_ruby").children("rt").remove();

$(".oHL_img_info_dimensions .oHL_ruby").children("rt").remove();

$(".Inline-Template .oHL_ruby").children("rt").remove();

$("#bodyContent").addClass("oHL_highlighted");

}

// Get wikitext of current page

function getWikitext() {

// API docs: https://www.mediawiki.org/wiki/API:Revisions

const apiUrl = location.origin + "/w/api.php";

$.ajax({

url: apiUrl,

data: {

action: "query",

prop: "revisions",

format: "json",

revids: mw.config.get("wgRevisionId"),

rvprop: "content",

rvslots: "main"

},

success: searchWikitext

});

}

function searchWikitext(response) {

const pageId = mw.config.get("wgArticleId");

const wikitext = response.query.pages[pageId].revisions[0].slots.main["*"];

const searches = new Map();

const results = new Map();

// Note: need to escape backslashes here

searchDescriptions["oHL_nowiki"] = ["nowiki", ""];

searches.set("oHL_nowiki", "");

searchDescriptions["oHL_include_tag"] = ["include tags", ""];

searches.set("oHL_include_tag", "<(no|only)include/?>");

searchDescriptions["oHL_infobox"] = ["Infoboxes", ""];

searches.set("oHL_infobox", "{{infobox");

searchDescriptions["oHL_infobox_name"] = ["Infobox name param", ""];

searches.set("oHL_infobox_name", "\\| +name +=");

searchDescriptions["oHL_closable"] = ["Closable named refs", ""];

searches.set("oHL_closable", ']+>');

searchDescriptions["oHL_author"] = ["Cite with author param", ""];

searches.set("oHL_author", "\\| ?author1? ?=");

searchDescriptions["oHL_reflist"] = ["Reflists", ""];

searches.set("oHL_reflist", "{{reflist\\|");

searchDescriptions["oHL_anchor_span"] = ["Anchors (span)", ""];

searches.set("oHL_anchor_span", "]+>");

searchDescriptions["oHL_anchor_template"] = ["Anchors (template)", ""];

searches.set("oHL_anchor_template", "{{anchor ?\\|[^}]+}}");

searchDescriptions["oHL_anchor_visible"] = ["Anchors (visible)", ""];

searches.set("oHL_anchor_visible", "{{(visible anchor|visanc|va|vanchor) ?\\|[^}]+}}");

searchDescriptions["oHL_inline_file"] = ["Inline files", ""];

searches.set("oHL_inline_file", "(?<=.)\\[\\[File:[^\\|]+\\|");

searchDescriptions["oHL_interwiki"] = ["Crosslanguage links", ""];

searches.set("oHL_interwiki", "\\[\\[[A-Za-z]{2}:[^\\]\\n]+\\]\\]");

searchDescriptions["oHL_font_size"] = ["Font size", ""];

searches.set("oHL_font_size", "font-size:");

searchDescriptions["oHL_comment"] = ["Comments", ""];

searches.set("oHL_comment", "");

searchDescriptions["oHL_math"] = ["Math", ""];

searches.set("oHL_math", "");

searchDescriptions["oHL_tag"] = ["HTML tags", ""];

searches.set("oHL_tag", "

+ "body|figure|caption|hr|h1|h2|h3|h4|h5|h6|img|kbd|u|"

+ "legend|pre|q|s|ruby|script|samp|small|big|span)>");

searchDescriptions["oHL_en_link"] = ["Redundant lang in link", ""];

searches.set("oHL_en_link", ":en:");

searchDescriptions["oHL_underscore"] = ["Underscored wikilinks", ""];

searches.set("oHL_underscore", "\\[\\[[^|\\]#]+_");

searchDescriptions["oHL_notoc"] = ["NOTOC", ""];

searches.set("oHL_notoc", "__NOTOC__");

searchDescriptions["oHL_sortkey"] = ["Sort keys", ""];

searches.set("oHL_sortkey", "\\[\\[Category:[^\\]\\n]+\\|[^\\]\\n]+\\]\\]");

searchDescriptions["oHL_thumb_size"] = ["Hardcoded thumbnail sizes", ""];

searches.set("oHL_thumb_size", "\\[\\[(File|Image).*?[0-9]px\\|");

searchDescriptions["oHL_auto_ref"] = ["Auto-named refs", ""];

searches.set("oHL_auto_ref", '

searchDescriptions["oHL_wikt_link"] = ["Wiktionary links", ""];

searches.set("oHL_wikt_link", "\\[\\[(wikt|wiktionary):[^\\]\\n]+\\]\\]");

searchDescriptions["oHL_commaless"] = ["Comma-less numbers", ""];

searches.set("oHL_commaless", "(?

+ "June|July|August|September|October|"

+ "November|December|= *)( [0-9]{1,2},)?)"

+ "|File:[^|\\n]*)" // or an image

+ "(?<=[ (–])\\d{1,}\\d{3}"

+ "(?!'?s" // not followed by 's, e.g. 1990s

+ "|[^<]+<\\/ref|\"?/>" // not in a ref

+ "|[^|\\n]*\\.\\w{3,}\\|)"); // or an image

searchDescriptions["oHL_piped_italics"] = ["Piped italics", ""];

searches.set("oHL_piped_italics", "[\\[|][^\\]\\n]+\\]\\]");

// TODO: expensive RegEx, can freeze the tab in rare cases

searchDescriptions["oHL_quote_punc"] = ["Punctuation in quotes", ""];

searches.set("oHL_quote_punc", ".{40}(?

searchDescriptions["oHL_adj_ital"] = ["Adjacent formatting", ""];

searches.set("oHL_adj_ital", "\\s+");

// TODO: expensive RegEx

searchDescriptions["oHL_name_hyphen"] = ["Hyphenated names", ""];

searches.set("oHL_name_hyphen", "(?]*)"

+ "[A-Z][a-z]+-[A-Z][a-z]+"

+ "(?![-/])");

searchDescriptions["oHL_adj_num"] = ["Adjacent numbers", ""];

searches.set("oHL_adj_num", "(?

+ "(?<= )[0-9]+ [0-9]+"

+ "(?![^|\\n]*\\.\\w{3,}\\|)"); // or followed by an image

searchDescriptions["oHL_day"] = ["Days of the week", ""];

searches.set("oHL_day", "(?<= )(Mon|Tue|Wed|Thur|Fri|Sat|Sun)(s|ur|nes)?(day)?(?=[ .,;:<])");

searchDescriptions["oHL_redundant_wl"] = ["Redundant piped wikilinks", ""];

searches.set("oHL_redundant_wl", "\\[\\[([^|\\]\\n]+)\\|\\1\\]\\]");

searchDescriptions["oHL_simplifiable_wl"] = ["Simplifiable wikilinks", ""];

searches.set("oHL_simplifiable_wl", "\\[\\[([^|\\n]+)\\|\\1[a-z]+\\]\\]");

searchDescriptions["oHL_overprecise"] = ["Overly precise numbers", ""];

searches.set("oHL_overprecise", "(?

+ "([0-9][,.][1-9]{3}|[0-9][,.]0[1-9]{2}|[0-9][,.][1-9]0[1-9]|[0-9][,.][1-9]{2}0|[0-9][,.]00[1-9])");

searchDescriptions["oHL_egg_link"] = ["Long to short links", ""];

searches.set("oHL_egg_link", "\\[\\[[^|\\]\\n]+ [^|\\]\\n]+ [^|\\]\\n]+\\|[^ \\]\\n]+\\]\\]");

searchDescriptions["oHL_capital_header"] = ["Headers with capital letters", ""];

searches.set("oHL_capital_header", "(?<===)[\\w ]+ [A-Z][^=\\n]+(?===)");

searchDescriptions["oHL_capital_table"] = ["Table captions with capital letters", ""];

searches.set("oHL_capital_table", "(?<=\\|\\+)[\\w ]+ [A-Z][^|\\n]+");

searchDescriptions["oHL_honorific"] = ["Honorific", ""];

searches.set("oHL_honorific", "(Mr|Mrs| Ms|Dr|Prof|Re?v|Sgt|Maj|Gen)[. ]");

searchDescriptions["oHL_inflation"] = ["Inflation", ""];

searches.set("oHL_inflation", "{{Inflation ?\\|[^}]+}}");

searchDescriptions["oHL_magic_word"] = ["Magic words", ""];

searches.set("oHL_magic_word", "{{(__INDEX__|__NOINDEX__|__DISAMBIG__"

+ "|DISPLAYTITLE|CURRENTMONTH|CURRENTDAY|CURRENTYEAR|DEFAULTSORT)[^}]+}}");

searchDescriptions["oHL_editorial"] = ["Editorial content", ""];

searches.set("oHL_editorial", "[ >]\\A-Za-z][^/\\+\\]");

searchDescriptions["oHL_inline_style"] = ["Inline style", ""];

searches.set("oHL_inline_style", "style=[\"']");

for (const [type, re] of searches) {

let flags = "gd";

if (type != "oHL_name_hyphen" && type != "oHL_capital_header"

&& type != "oHL_capital_table" && type != "oHL_honorific") {

flags += "i";

}

const searchRe = new RegExp(re, flags);

const matches = wikitext.matchAll(searchRe);

const matchesList = [];

for (const m of matches) {

const start = m.indices[0][0];

const end = m.indices[0][1];

const context = 20;

const leftContext = wikitext.substring(start-context, start);

const matchText = wikitext.substring(start, end);

const rightContext = wikitext.substring(end, end+context);

matchesList.push([leftContext, matchText, rightContext]);

}

if (matchesList.length > 0) {

results.set(type, matchesList);

}

}

searchDescriptions["oHL_nested_quotes"] = ["Nested quote marks", ""];

const nestedResults = getNestedQuotes(wikitext);

if (nestedResults.length > 0) {

results.set("oHL_nested_quotes", nestedResults);

}

showWikitextMatches(results);

$("#oHL_commaless summary").after("

");

$("#oHL_commalessFilter").change(function filterCommalessResults() {

if ($("#oHL_commalessFilter").is(":checked")) {

$("#oHL_commaless .oHL_wikitext-match").each(function hideYearResults() {

const yearText = $(this).children(".oHL_wikitext-match-text").text();

const year = parseInt(yearText);

if (year > 1300 && year < 2500) { // arbritrary date range

$(this).hide();

}

});

} else {

$("#oHL_commaless .oHL_wikitext-match").show();

}

});

checkEmptyShortdescription(wikitext);

compareDefaultSort(wikitext);

showLongQuotes(wikitext);

showRedlinks();

showFrequency();

showRedirects();

checkDisambigLink();

checkOutlinkAnchors();

tabulateReferences(wikitext);

}

function checkEmptyShortdescription(wikitext) {

if (/{{short description\s*\|\s*none}}/i.test(wikitext)) {

$(".oHL-missing-desc").remove();

updateMatches();

}

}

function getNestedQuotes(wikitext) {

wikitext = wikitext.replace(/(=[^>]+?)" /g, '$1₭ '); // guard quotes in

wikitext = wikitext.replaceAll('>"<', '>₭<'); // guard single highlighted quotes

wikitext = wikitext.replace(/([ >\()])"/g, '$1𐑱'); // 𐑱: left quote placeholder

wikitext = wikitext.replace(/"([ .,;:\n\)]|

wikitext = wikitext.replaceAll('₭', '"'); // unguard

// Creating this separately to prevent jshint error due to newer /d flag

const matchRegex = new RegExp("𐑱[^𐑲\n]+𐑱[^𐑱\n]+𐑲[^𐑱\n]+𐑲", "gd");

const matches = wikitext.matchAll(matchRegex);

const matchesList = [];

for (const m of matches) {

const start = m.indices[0][0];

const end = m.indices[0][1];

const context = 20;

const leftContext = wikitext.substring(start - context, start);

const matchText = wikitext.substring(start, end);

const rightContext = wikitext.substring(end, end + context);

const match = [leftContext, matchText, rightContext];

const matchUnguarded = match.map(m => m.replace(/[𐑱𐑲]/gu, '"'));

matchesList.push(matchUnguarded);

}

return matchesList;

}

function compareDefaultSort(wikitext) {

let title = mw.config.get("wgTitle");

let defaultSort;

const match = wikitext.match(/{DEFAULTSORT:([^}\n]+)}/i);

if (match != null) {

defaultSort = match[1];

} else {

defaultSort = title;

}

defaultSort = defaultSort.replace(/ \([^)]+\)$/, ""); // Remove " (disambiguation)"

title = title.replace(/ \([^)]+\)$/, ""); // Remove " (disambiguation)"

title = title.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); // https://stackoverflow.com/a/37511463/1995949

title = title.replaceAll("–", "-");

title = title.replaceAll("—", "-");

title = title.replaceAll("×", "x");

let titleCollated = title;

const categories = $("#mw-normal-catlinks").text();

if (title.startsWith("The ")) {

titleCollated = title.substring(4) + ", The";

} else if (title.startsWith("A ")) {

titleCollated = title.substring(2) + ", A";

} else if (categories.includes("births")

categories.includes("deaths")categories.includes("Living people")) { // people

// See: https://en.wikipedia.org/wiki/WP:NAMESORT

title = title.replace("Saint ", "");

title = title.replace("O'", "O"); // e.g. O'Neil

let suffix = "";

if (title.endsWith(" Jr.")) {

suffix = " Jr.";

title = title.substring(0, title.length - suffix.length);

}

// Assume the final part of the name is surname; not applicable to all cultures

const splitName = title.split(" ");

if (splitName.length > 1) {

const lastPart = splitName.at(-1);

const firstPart = splitName.slice(0, -1).join(" ");

titleCollated = lastPart + ", " + firstPart + suffix;

} else {

titleCollated = title;

}

}

if (titleCollated != defaultSort) {

searchDescriptions["oHL_defaultSort"] = ["DefaultSort mismatch", ""];

const mismatchText = `"${defaultSort}" (current) ≠ "${titleCollated}" (expected)`;

$("#oHL_results").append("

DefaultSort mismatch"

+ " (1)

"

+ "

  • " + mismatchText + "
");

}

}

function showLongQuotes(wikitext) {

const quoteRe = / "[A-Z.].*?"/g;

const quotes = wikitext.match(quoteRe);

if (quotes === null) { return; }

const longQuotes = [];

for (const quote of quotes) {

const wordCount = quote.split(" ").length;

if (wordCount > 40) {

longQuotes.push([quote, wordCount]);

}

}

if (longQuotes.length == 0) {

return;

}

let list = "

    ";

    for (const [quote, wordCount] of longQuotes) {

    list += "

  • " + quote.substring(1) + " [~" + wordCount + " words]
  • ";

    }

    list += "

";

searchDescriptions["oHL_longQuotes"] = ["Long quotes", ""];

$("#oHL_results").append("

Long quotes ("

+ longQuotes.length + ")

" + list + "
");

}

function checkOutlinkAnchors() {

const anchorLinksArray = [];

$(".oHL_wikilink").each(function getAnchoredWikilinks() {

const linkObject = $(this).clone();

linkObject.find(".oHL_ruby rt, .oHL_added").remove();

const linkElement = linkObject[0];

const anchor = decodeURIComponent(linkElement.hash.slice(1));

if (anchor != "" && !linkElement.href.includes("/wiki/Help:")

&& !linkElement.href.includes("/wiki/Wikipedia:")

&& !linkElement.href.includes("/wiki/Talk:")) {

const pageTitle = decodeURIComponent(linkElement.pathname?.split("/")[2]);

const linkText = $(linkElement).text().replace("|", " | ");

anchorLinksArray.push({"link": pageTitle, "text": linkText, "anchor": anchor});

}

});

if (anchorLinksArray.length == 0) {

return;

}

// Deduplicate

const anchorLinks = [...new Set(anchorLinksArray)];

for (const anchorLink of anchorLinks) {

// API docs: https://www.mediawiki.org/wiki/API:Parsing_wikitext

const apiUrl = location.origin + "/w/api.php";

$.ajax({

url: apiUrl,

data: {

action: "parse",

page: anchorLink.link,

prop: "text",

format: "json",

redirects: "true",

},

success: function processRevisions(response) {

checkAnchor(anchorLink, response);

}

});

}

}

function checkAnchor(anchorLink, response) {

const pageHtml = response.parse.text["*"];

const pageParsed = $.parseHTML(pageHtml);

const ids = $(pageParsed).find(".mw-heading [id], span[id]").toArray().map(e => e.id);

matchDescriptions["oHL-broken-outgoing-anchor"] = ["Broken outgoing anchor", "This anchor does not exist at the target article."];

if (!ids.includes(anchorLink.anchor)) {

const linkHref = anchorLink.link + "#" + anchorLink.anchor;

let titleEncoded = linkHref.replaceAll(" ", "_");

titleEncoded = encodeURIComponent(titleEncoded).replaceAll("'", "%27");

$("[href^='/wiki/" + titleEncoded + "']").addClass("oHL oHL-broken-outgoing-anchor");

updateMatches();

}

}

function tabulateReferences(wikitext) {

const redundantRefs = [];

const missingWorkRefs = [];

const missingAccessDateRefs = [];

const insecureRefs = [];

wikitext = wikitext.replace(//gs, ''); // delete comments

const templateRefRe = /]*>\s*{{(cite |citation)[^<]+<\/ref>/gi;

const templateRefs = wikitext.match(templateRefRe)

[];

const templateRefcount = templateRefs.length;

const plainRefRe = /]*>[^<]+<\/ref>/gi;

let plainRefs = wikitext.match(plainRefRe)

[];

const shortenedRefRe = /{{(cite|citation|harvtxt|harvnb|sfn|unbulleted list citebundle|multiref)/i;

plainRefs = plainRefs.filter(r => !shortenedRefRe.test(r));

const plainRefcount = plainRefs.length;

const foundCount = templateRefcount + plainRefcount;

if (foundCount == 0) { return; }

let tableHeader = "

"

+ "

"

+ "

"

+ "

";

let tableContent = "";

for (const ref of templateRefs) {

const template = ref.match(/{cite ([^

#Template AuthorDateAccessTitle WorkPublisherLang Proto
]+)/i)?.[1] || ref.match(/{(citation)/i)?.[1];

const firstName = ref.match(/\|\s*(?:first|given)1?\s*=\s*([^|}]+)/i)?.[1] || "—";

const lastName = ref.match(/\|\s*(?:last|surname)1?\s*=\s*([^|}]+)/i)?.[1] || "—";

let author = ref.match(/\|\s*(?:v?authors?|host)\s*=\s*([^|}][^|}]+)/i)?.[1] || "—";

if (author == "—" && firstName != "—") {

const firstName_ = firstName.trim();

const lastName_ = lastName.trim();

author = `${firstName_} / ${lastName_}`;

}

let date = ref.match(/\|\s*(?:date|year)\s*=\s*(\w[^|}]+)/i)?.[1] || "—";

if (date == "—" && template.includes("tweet")) {

const tweetID = ref.match(/\/status\/([0-9]+)/)?.[1];

date = tweetURLtoDate(tweetID) || "—";

}

const accessdate = ref.match(/\|\s*access-?date\s*=\s*(\w[^|}]+)/i)?.[1] || "—";

let title = ref.match(/\|\s*(?:script-)?title\s*=\s*([^|}][^|}]+)/i)?.[1] || "—";

let url = ref.match(/\|\s*url\s*=\s*(http[^|}]+)/i)?.[1] || "—";

if (template.includes("journal")) {

const doi = ref.match(/\|\s*doi\s*=\s*([^|}][^|}]+)/i)?.[1];

if (doi != null) {

url = "https://doi.org/" + doi.replace("/", "%2F");

}

} else if (template.includes("tweet") || template.includes("twitter") || template.includes(" X")) {

const user = ref.match(/\|\s*user\s*=\s*([^|}][^|}]+)/i)?.[1] || "—";

const number = ref.match(/\|\s*number\s*=\s*([^|}][^|}]+)/i)?.[1] || "—";

url = `https://x.com/${user}/status/${number}`;

url = url.replaceAll(" ", "");

} else if (template == "Q") {

title = ref.match(/(Q[0-9]+)/i)?.[1];

url = `https://www.wikidata.org/wiki/${title}`;

}

url = url.trim();

const ordinal = getRefOrdinalFromURL(url, ref, template);

const archiveUrl = ref.match(/\|\s*archive-?url\s*=\s*(http[^|}]+)/i)?.[1] || "—";

const urlStatus = ref.match(/\|\s*url-?status\s*=\s*(\w[^|}]+)/i)?.[1] || "—";

const isLiveLink = /live/i.test(urlStatus);

let isArchived = false;

if (archiveUrl != "—" && !isLiveLink) { url = archiveUrl; isArchived = true; }

const refEscaped = ref.replaceAll('"', '"');

const originalTitle = title;

if (title != "—" && url != "—") { title = `${title}`; }

if (title != "—") { title = `${title}`; }

let protocol = "—";

if (url != "—") { protocol = url.startsWith("https:") ? "🔐" : "🔓"; }

let work = ref.match(/\|\s*(?:work|website|journal|newspaper|magazine|periodical)\s*=\s*([^|}][^|}]+)/i)?.[1] || "—";

if (template.includes("tweet")) { work = "Twitter"; }

const publisher = ref.match(/\|\s*(?:publisher|agency)\s*=\s*([^|}][^|}]+)/i)?.[1] || "—";

const language = ref.match(/\|\s*lang(?:uage)?\s*=\s*(\w[^|}]+)/i)?.[1] || "—";

if (work == "—" && publisher == "—") {

const refSelector = $(ordinal).attr("href");

missingWorkRefs.push(refSelector);

}

if (accessdate == "—" && (template == "web" || template == "news")) {

const refSelector = $(ordinal).attr("href");

missingAccessDateRefs.push([url, refSelector]);

}

if (url != "—" && url.startsWith("http:") && date != "—") {

// Arbritrarily based off: https://www.eff.org/deeplinks/2021/09/https-actually-everywhere

const cutoffDate = Date.parse("2021-09-21");

const dateParsed = Date.parse(date);

if (dateParsed > cutoffDate) {

const refSelector = $(ordinal).attr("href");

insecureRefs.push(refSelector);

}

}

const workOrPublisherRaw = work != "—" ? work : publisher;

if (workOrPublisherRaw != "—" && (originalTitle != "—" || author != "—")) {

let workOrPublisher = workOrPublisherRaw.trim();

if (workOrPublisher.includes("|")) {

workOrPublisher = workOrPublisher.split("|")[0];

}

if (workOrPublisher.includes("[")) {

workOrPublisher = workOrPublisher.replaceAll(/[\[\]]/g, "");

}

if ((originalTitle != "—" && originalTitle.trim() != workOrPublisher.trim()

&& originalTitle.includes(workOrPublisher))

|| (author != "—" && author.includes(workOrPublisher))) {

redundantRefs.push(url);

}

}

tableContent += `

${ordinal}${template}${author}${date} ${accessdate}${title}${work} ${publisher}${language}${protocol}
${ordinal} ${title} ${protocol}
";

let countString = foundCount;

const totalCount = $(".reference-text").last().closest("li").index() + 1;

if (foundCount != totalCount) { countString += "/" + totalCount; }

searchDescriptions["oHL_refTable"] = ["References", ""];

$("#oHL_results").append("

References ("

+ countString + ")

" + table + "
");

catchFinalOrdinal(foundCount, totalCount);

matchDescriptions["oHL-redundant-title"] = ["Redundant reference parameter", "Citations should not repeat the work parameter."];

for (const redundantRef of redundantRefs) {

$(".oHL_reflist a[href*='" + redundantRef + "']").closest("cite").after(" [Redundant parameter]");

updateMatches();

}

matchDescriptions["oHL-missing-work"] = ["Reference without work", "Citations should include work or publisher information."];

for (const refSelector of missingWorkRefs) {

$(refSelector).first().find("cite").after(" [Missing work]");

updateMatches();

}

const pageTitle = mw.config.get("wgTitle");

const pageTitleEncoded = encodeURIComponent(pageTitle);

matchDescriptions["oHL-missing-accessdate"] = ["Reference without access date", "Web citations should include the date accessed."];

for (const [url, refSelector] of missingAccessDateRefs) {

const urlEncoded = encodeURIComponent(url.replace(/https?:\/\//, ""));

const blameURL = "https://wikipedia.ramselehof.de/wikiblame.php?user_lang=en&lang=en&project=wikipedia&tld=org&force_wikitags=on&article="

+ pageTitleEncoded + "&needle=" + urlEncoded;

$(refSelector).first().find("cite").after(" [Missing access date (Blame)]");

updateMatches();

}

matchDescriptions["oHL-insecure-ref"] = ["Insecure reference", "Most modern websites support the HTTPS protocol and references should be updated to use it."];

for (const refSelector of insecureRefs) {

$(refSelector).find("cite").after(" [http]");

updateMatches();

}

mw.loader.using("jquery.tablesorter", function makeTableSortable() {

$("#oHL_refTable table").tablesorter( { sortList: [ {0: "asc"} ] } );

});

}

function getRefOrdinalFromURL(url, ref, template) {

let ordinal = "—";

let refParent;

if (url != "—") {

// for some reason, MediaWiki upgrades some URLs to https even if they were http in the source

const urlStripped = url.replace(/https?:/, "");

const urlEscaped = CSS.escape(urlStripped);

refParent = $(".reference-text [href$='" + urlEscaped + "']").closest(".reference-text").closest("li");

}

if (typeof refParent == "undefined" || refParent.length == 0) {

const isbn = ref.match(/\|\s*isbn\s*=\s*([^|}][^|}]+)/i)?.[1];

if (isbn) {

const isbnTrimmed = isbn.trim();

refParent = $(".reference-text [href*='" + isbnTrimmed + "']").closest(".reference-text").closest("li");

}

}

if (refParent) {

const ordinalNumber = $(refParent).index() + 1;

const refId = $(refParent).attr("id");

ordinal = `${ordinalNumber}`;

if (ordinalNumber == 0) {

const warningMessage = "highlightStrings.js: Warning: Couldn't match ordinal for URL: " + url + ".";

console.warn(warningMessage);

printInternalWarning(warningMessage);

}

}

return ordinal;

}

function catchFinalOrdinal(foundCount, totalCount) {

if (foundCount != totalCount) { return; }

const ordinalColumn = $("#oHL_refTable tbody tr td:first-child");

const ordinalString = $(ordinalColumn).text();

const noOrdinalCount = (ordinalString.match(/—/g) || []).length;

if (noOrdinalCount != 1) { return; }

const ordinals = [];

let blankOrdinal;

ordinalColumn.each(function getOrdinals() {

const ordinal = $(this).text();

if (ordinal != "—") {

ordinals.push(parseInt(ordinal));

} else {

blankOrdinal = this;

}

});

ordinals.sort((a, b) => a - b);

for (let counter = 1; counter <= totalCount; counter++) {

if (!ordinals.includes(counter)) {

const ordinalId = $(".references > li[id$='-" + counter + "']").attr("id");

const markup = `${counter}`;

$(blankOrdinal).html(markup);

break;

}

}

}

// Reference: https://en.wikipedia.org/wiki/Snowflake_ID

function tweetURLtoDate(tweetID) {

if (!tweetID) { return null; }

const epoch = 1288834974657;

// Example: https://twitter.com/wikipedia/status/1541815603606036480

// e.g. 1541815603606036480

const snowflake = parseInt(tweetID);

// e.g. 0b 1 0101 0110 0101 1010 0001 0001 1111 0110 0010 00|01 0111 1010|0000 0000 0000

const offsetBinary = snowflake.toString(2).substring(0, 39);

// e.g. 367597485448

const offset = parseInt(offsetBinary, 2);

// e.g. 1288834974657 + 367597485448 = 1656432460_105

const timestampMS = epoch + offset;

// e.g. June 28, 2022

const date = new Date(timestampMS).toLocaleDateString("en-us", { day:"numeric", year:"numeric", month:"long"});

return date;

}

function showRedlinks() {

const redLinks = $("#mw-content-text a.new");

if (redLinks.length == 0) { return; }

const linkText = {};

$(redLinks).each(function getRedlinks() {

const linkObject = $(this).clone();

linkObject.find(".oHL_ruby rt, .oHL_added").remove();

const link = linkObject.attr("href");

const text = linkObject.text();

linkText[link] = text;

});

const navLinks = $(".navbox a.new, .sidebar a.new, .ambox a.new");

$(navLinks).each(function getNavlinks() {

const link = $(this).attr("href");

delete linkText[link];

});

const linkTextSize = Object.keys(linkText).length;

if (linkTextSize == 0) { return; }

let list = "

    ";

    for (const [link, text] of Object.entries(linkText)) {

    list += "

  • " + text + "
  • ";

    }

    list += "

";

searchDescriptions["oHL_redlinks"] = ["Redlinks", ""];

$("#oHL_results").append("

");

}

const wordListURL = "https://" + window.location.hostname + "/w/index.php?title=User:Opencooper/highlightStringsWordlist.js&action=raw&ctype=text/javascript";

function showFrequency() {

$.ajax({

url: wordListURL,

success: getFrequencies

});

}

function containsVowel(s) {

const vowels = ["a", "e", "i", "o", "u", "y"];

return Array.from(s).filter(c => vowels.includes(c)).length > 0;

}

function getSingular(word) {

const startLength = word.length;

word = word.replace(/('s'|'s)$/, "");

if (word.length != startLength) { return word; }

word = word.replace(/'$/, "");

if (word.endsWith("sses") || word.endsWith("xes")) {

word = word.slice(0, -2);

} else if (word.endsWith("us") || word.endsWith("ss") || word.endsWith("es")) {

// do nothing

} else if (word.endsWith("s")) {

const precedingWordPart = word.slice(0, -2);

if (containsVowel(precedingWordPart)) {

word = word.slice(0, -1);

}

}

return word;

}

// Simpler form of the Porter2 algorithm: http://snowball.tartarus.org/algorithms/english/stemmer.html

// Attempts to lemmatize better

function getStem(word) {

function getRegions(s) {

// Not implementing gener/commun/arsen exception

const regionRe = /[aeiouy][^aeiouy](.*)/;

const r1 = s.match(regionRe)?.[1] || "";

const r2 = r1.match(regionRe)?.[1] || "";

return [r1, r2];

}

function endsWithDouble(s) {

return s.length >= 2 && s.slice(-1) == s.slice(-2, -1);

}

function isShort(s) {

const [r1, r2] = getRegions(s);

return r1 == "" && /[^aeiouy][aeiouy][^aeiouywxY]$/.test(s);

}

word = getSingular(word);

if (word.length <= 2) {

return word;

}

if (word.endsWith("ies")) {

word = word.replace(/ies$/, "y");

} else if (word.endsWith("es")) {

word = word.replace(/es$/, "");

const finalLetter = word.slice(-1);

if (/[bcdefgklmnopqrstuvz]/.test(finalLetter) || word.endsWith("ach")) {

word += "e";

}

}

// e.g. painting, rating

if (word.endsWith("ting") || word.endsWith("ted")) {

word = word.replace(/ing$/, "").replace(/ed$/, "");

if (/[aeiouyt]$/.test(word)) {

word += "e";

}

}

// e.g. rising, sized, inviting

if (word.endsWith("ising") || word.endsWith("izing") || word.endsWith("iting")) {

word = word.replace(/(i[szt])ing$/, "$1e");

} else if (word.endsWith("ised") || word.endsWith("ized") || word.endsWith("ised") || word.endsWith("ited")) {

word = word.replace(/(i[szt])ed$/, "$1e");

}

// e.g. ensnaring, snored

if (word.endsWith("naring") || word.endsWith("noring")) {

word = word.replace(/(n[ao]r)ing$/, "$1e");

} else if (word.endsWith("nared") || word.endsWith("nored")) {

word = word.replace(/(n[ao]r)ed$/, "$1e");

}

if (word.endsWith("ing")) {

word = word.replace(/ing$/, "");

if (endsWithDouble(word)) {

word = word.slice(0, -1);

} else if (/[cpvn]$/.test(word)) {

word += "e";

}

}

if (word.endsWith("ied")) {

word = word.replace(/ied$/, "y");

} else if (word.endsWith("ed")) {

const deletedWord = word.replace(/ed$/, "");

if (containsVowel(deletedWord)) {

word = deletedWord;

if (word.endsWith("at") || word.endsWith("bl") || word.endsWith("iz") || word.endsWith("en") || word.endsWith("ok") || word.endsWith("v") || word.endsWith("in")) {

word += "e";

} else if (endsWithDouble(word)) {

word = word.slice(0, -1);

} else if (isShort(word)) {

word += "e";

}

}

}

if (word.endsWith("er") || word.endsWith("est")) {

word = word.replace(/er$/, "").replace(/est$/, "");

if (word.endsWith("i")) {

word = word.slice(0, -1) + "y";

} else if (word.endsWith("m")) {

word += "e";

}

}

if (word.endsWith("tche")) {

// e.g. blotches

word = word.replace(/tche$/, "tch");

} else if (word.endsWith("ttl") || word.endsWith("rul")) {

// e.g. unsettled, overruling

word += "e";

}

return word;

}

function getFrequencies(response) {

const wordList = response.split("\n");

const commentEnd = wordList.indexOf("//———") + 1;

for (let i = 0; i <= commentEnd; i++) {

wordList[i] = "";

}

const wordListDehyphenated = [];

const wordListDeperioded = [];

for (const word of wordList) {

if (word.includes("-")) {

const wordDehyphenated = word.replaceAll("-", "");

wordListDehyphenated.push(wordDehyphenated);

} else if (word.endsWith(".")) {

const periodCount = word.match(/\./g).length;

if (periodCount == 1) {

const wordDeperioded = word.slice(0, -1);

wordListDeperioded.push(wordDeperioded);

}

}

}

// Get article text and cleanup text we don't want

const bodyContent = $("#mw-content-text .mw-content-ltr").first().clone();

bodyContent.find("p, div, tr, .infobox-data, br").before("\n");

bodyContent.find("li").before(" • ");

bodyContent.find("th, td").before(" ║ ");

bodyContent.find("sub, sup").before(" ");

bodyContent.find("q").before('"'); bodyContent.find("q").after('"');

bodyContent.find("sub, sup, math, style,"

+ " [href='/wiki/Help:Pronunciation_respelling_key'],"

+ " [href*='doi.org'], [href*='arxiv.org'],"

+ " .ambox, .portalbox, .infobox-label, #toc, .texhtml,"

+ " .printfooter, .sidebar, .IPA, .stub, .url, .dmbox,"

+ " .sistersitebox, .mw-hidden-catlinks, .cs1-maint,"

+ " .cs1-prop-foreign-lang-source, .cs1-visible-error,"

+ " .harv-error, .cite-accessibility-label, .mw-editsection,"

+ " .oHL_anchorLink, .oHL_piped, .oHL_added, .oHL_ruby rt,"

+ " .oHL_trailingSpace, #oHL_wd_img, .oHL_shownAnchor,"

+ " .oHL_img_info_dimensions, .oHL_clear,"

+ " .navbox-title .hlist, .mw-tmh-player").remove();

String.prototype.cleanText = function() {

return this.replaceAll("\n", " ")

.replaceAll("’", "'")

.replaceAll(/http[^ \n]*/g, "").replaceAll(/[\w.-]+\.[\w\/]{2,}/g, "")

.replaceAll(/([–—−+×·⋅÷√&\/\\<>{}~$@%_…\|\*=º°^′™©®†‡§←→↔~「」【】()・])/g, " ")

.replaceAll(/(\p{Emoji_Presentation})/ug, "")

.replaceAll(/([-‑‐])/g, " ")

.replaceAll(/[\s​]/g, " ")

.replaceAll("\u2060", "").replaceAll("\u200C", "").replaceAll("\u200D", "").replaceAll("\u200E", "").replaceAll("\u00AD", "") // invisible characters

.replaceAll(/(' '|(?

.replaceAll(/\/g, "")

.replaceAll(/([.,:;"‘’“”„`´«»‹›!¡?¿#|&%。.,、:;?!()])/g, " ")

.replaceAll("æ", "ae").replaceAll("œ", "oe");

};

const bodyContentNavboxless = bodyContent.clone();

bodyContentNavboxless.find(".navbox").remove();

const bodyTextRawNavboxless = bodyContentNavboxless.text();

showUnbalanced(bodyTextRawNavboxless);

// Note: the selector that comes first in the DOM is picked

const plotSelector = "#Plot, [id^=Plot_], #Synopsis";

const bodyContentAsideLess = bodyContent.clone();

bodyContentAsideLess.find(".hatnote, .mw-heading3, .mw-heading4, .mw-heading5, .mw-heading6, figure, .quotebox").remove();

const plotArray = bodyContentAsideLess.find(plotSelector).first().parent().nextUntil(".mw-heading2")

.text().replaceAll("-", "").cleanText()

.replaceAll(/\s{2,}/g, " ").replace(/ $/, "").split(" ");

const wordCount = plotArray.length;

if (wordCount > 5) {

$(plotSelector).first().parent().after("

[Word count: "

+ wordCount.toLocaleString() + "]

");

}

matchDescriptions["oHL-plot-length"] = ["Plot length", "Plot summaries should generally be less than 700 words. (MOS:PLOT)"];

if (wordCount > 700) {

$(".oHL_plotLength").addClass("oHL oHL-plot-length");

updateMatches();

}

let bodyTextRaw = bodyContent.text();

const bodyText = bodyTextRaw.cleanText();

// Create lists of words for filtering

const refTextArray = bodyContent.find(refSectionsSelector + ", #Further_reading, #Additional_reading")

.parent().nextUntil(".mw-heading2, .navbox, .stub").text().cleanText().split(" ");

const italicTextArray = bodyContent.find("i").append(" ").text().cleanText().split(" ");

const wikilinkTextArray = bodyContent.find(".oHL_wikilink, a.new").append(" ").text().cleanText().split(" ");

const externalTextArray = bodyContent.find(".external").append(" ").text().cleanText().split(" ");

const blockTextArray = bodyContent.find("blockquote, .quotebox, .poem:not(blockquote .poem), .oHL_bad-indent").text().cleanText().split(" ");

// TODO: use a different char for single quotes

const quoteMarkTextArray = bodyTextRaw.replaceAll(/([ \(\n])['"]/g, "$1𐑱") // 𐑱: left quote placeholder

.replaceAll(/[‘“]/g, "𐑱")

.replaceAll(/['’"]([ .,;:\)\n])/g, '𐑲$1') // 𐑲: right quote placeholder

.replaceAll("”", "𐑲")

.match(/(?<=𐑱)[^𐑲\n]+(?=𐑲)/g)

?.join(" ").replaceAll(/[𐑱𐑲]/g, "")

.cleanText().split(" ");

const quoteTextArray = blockTextArray.concat(quoteMarkTextArray);

// Create lists of words for whitelisting

const wikilinkPipedTextArray = $("#mw-content-text .oHL_piped small").clone().append(" ").text().cleanText().toLowerCase().split(" ");

const hatnoteTextArray = bodyContent.find(".hatnote .oHL_wikilink").text().cleanText().toLowerCase().split(" ");

const navboxTextArray = bodyContent.find(".navbox").text().cleanText().toLowerCase().split(" ");

const foreignTextArray = bodyContent.find("[lang], .extiw").append(" ").text().cleanText().toLowerCase().split(" ");

const categoryTextArray = $("#catlinks").clone().find("li").append("|").text().cleanText().toLowerCase().split(" ");

const titleTextArray = bodyContent.find(".oHL_title").append(" ").text().cleanText().toLowerCase().split(" ");

const codeTextArray = bodyContent.find("pre, code").append(" ").text().cleanText().toLowerCase().split(" ");

const sicTextArray = bodyText.match(/\w+(?=\s+sic )/g)?.join(" ").toLowerCase().split(" ");

const usernameArray = bodyTextRaw.match(/(?<=@)(\w+)/g)?.join(" ").replaceAll("_", "").toLowerCase().split(" ");

const hashtagArray = bodyTextRaw.match(/(?<=#)(\w+)/g)?.join(" ").toLowerCase().split(" ");

const gitHubArray = bodyContent.find("[href^='https://github.com/']").append(" ").text().split(" ").filter(s => s.includes("/")).join(" ").cleanText().toLowerCase().split(" ");

bodyTextRaw = bodyTextRaw.replaceAll(/[ \n]*\n+[ \n]*/g, " ¶ ")

.replaceAll(/(¶ \^ ){2,}/g, "¶ ^ ");

showSingleQuotes(bodyTextRaw);

// Find editorializing

const bodyContentEditorialized = bodyContent.clone();

bodyContentEditorialized.find("blockquote, .reference-text").remove();

// TODO: make this removal visible in the context

bodyContentEditorialized.find("i, b, .oHL_wikilink, a.new").remove();

let bodyContentEditorializedRaw = bodyContentEditorialized.text();

// Remove quoted text

bodyContentEditorializedRaw = bodyContentEditorializedRaw.replaceAll(/([ \(\n])['"]/g, "$1𐑱") // 𐑱: left quote placeholder

.replaceAll(/[‘“]/g, "𐑱")

.replaceAll(/['’"]([ .,;:\)\n])/g, '𐑲$1') // 𐑲: right quote placeholder

.replaceAll("”", "𐑲")

.replace(/(?<=𐑱)[^𐑲\n]+(?=𐑲)/g, "___")

.replaceAll(/[𐑱𐑲]/g, "`");

bodyContentEditorializedRaw = bodyContentEditorializedRaw.replaceAll(/[ \n]*\n+[ \n]*/g, " ¶ ")

.replaceAll(/(¶ \^ ){2,}/g, "¶ ^ ");

showEditorializing(bodyContentEditorializedRaw);

// Extract names

const bodyContentNames = bodyContent.clone();

const wikilinkedWhitelistElements = $.map($(".oHL_piped small"), $.text).map(s => s.trim()).map(s => s.replace(/ \(.*\)$/, ""));

bodyContentNames.find(".oHL_piped").remove();

const wikilinkedWhitelistElements2 = $.map(bodyContentNames.find(".oHL_wikilink, a.new, .oHL_title, #oHL_redirects ul li"), $.text).map(s => s.trim());

const pageTitle = mw.config.get("wgTitle").replace(/ \([^)]+\)$/, ""); // Remove " (disambiguation)"

wikilinkedWhitelistElements2.push(pageTitle);

bodyContentNames.find(".hatnote, b, a, caption, th, pre, code, .mw-heading, dt, .oHL_reflist, .navbox, .locmap").remove();

const bodyContentNamesRaw = bodyContentNames.text().replaceAll("\n", " ¶ ");

const namesArray = bodyContentNamesRaw.match(/(\p{Upper}\p{Lower}+\W){2,}/gu)?.map(s => s.slice(0, -1)).filter(s => !s.includes("["));

const wikilinkedWhitelistCustom = ["In January", "In February", "In March",

"In April", "In May", "In June", "In July",

"In August", "In September", "In November",

"In October", "In December", "Wikimedia Commons"];

const wikilinkedWhitelistElements3 = [];

// Usually empty because list is generated via async request

$("#oHL_redirects li").each(function getIncomingRedirects() { wikilinkedWhitelistElements3.push($(this).text()); });

const wikilinkedWhitelistArray = wikilinkedWhitelistElements.concat(wikilinkedWhitelistElements2, wikilinkedWhitelistElements3, wikilinkedWhitelistCustom, countryNames, stateNames);

const nameCandidates = namesArray?.filter(n => !wikilinkedWhitelistArray.includes(n));

const nameCandidatesUnique = [ ...new Set(nameCandidates) ];

// TODO: include context

checkWikilinkCandidates(nameCandidatesUnique);

// Convert text into frequency list

const tokens = bodyText.split(" ");

const counts = {};

for (const token of tokens) {

if (token.length <= 1 || /\d/.test(token) || /[A-Z]{2}|[a-z][A-Z]/.test(token)

|| !/\w/.test(token) || token.startsWith("d'") || token.startsWith("l'")) {

continue;

}

if (token in counts) {

counts[token] += 1;

} else {

counts[token] = 1;

}

}

// Merge together variants, e.g. "chopsticks" and "Chopsticks" for "chopstick"

for (const [token, count] of Object.entries(counts)) {

const variants = [token];

const tokenNormalized = token.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); // https://stackoverflow.com/a/37511463/1995949

if (tokenNormalized != token) {

variants.push(tokenNormalized);

}

for (const variant of variants) {

const variantSingular = getSingular(variant);

if (variantSingular != variant) {

variants.push(variantSingular);

}

}

for (const variant of variants) {

const variantStemmed = getStem(variant);

if (variantStemmed != variant) {

variants.push(variantStemmed);

}

}

for (const variant of variants) {

const tokenLowercased = variant.toLowerCase();

if (tokenLowercased != variant) {

variants.push(tokenLowercased);

}

}

for (const variant of variants) {

if (variant != token && variant in counts) {

counts[variant] += count;

delete counts[token];

}

}

}

// Only keep words that don't repeat

const singleCounts = [];

for (const [token, count] of Object.entries(counts)) {

if (count == 1) {

singleCounts.push(token);

}

}

const wordWhiteListArray = wordList.concat(wordListDehyphenated, wordListDeperioded,

navboxTextArray, foreignTextArray,

categoryTextArray, titleTextArray,

sicTextArray, usernameArray,

hashtagArray, gitHubArray,

codeTextArray, hatnoteTextArray,

wikilinkPipedTextArray);

const wordWhitelist = new Set(wordWhiteListArray);

// Filter out common words

const singleCountsUncommon = [];

for (const token of singleCounts) {

const tokenLowercase = token.toLowerCase().replace("æ", "ae").replace("œ", "oe");

const tokenSingular = getSingular(tokenLowercase);

if (!wordWhitelist.has(tokenLowercase)

&& !wordWhitelist.has(tokenSingular)

&& !wordWhitelist.has(getStem(tokenSingular))) {

singleCountsUncommon.push(token);

}

}

if (singleCountsUncommon.length == 0) {

return;

}

// Alphabetize and build list

const finalIndex = bodyTextRaw.length - 1;

singleCountsUncommon.sort((a, b) => a.localeCompare(b, 'en', {'sensitivity': 'base'}));

let listMarkup = "

    ";

    for (const token of singleCountsUncommon) {

    const searchRe = new RegExp("(?<=\\W)" + token + "(?=\\W)");

    const contextIndex = bodyTextRaw.match(searchRe)?.index;

    let context = "";

    if (typeof contextIndex != "undefined") {

    const contextOffset = 35 - (token.length / 2);

    const contextCenterIndex = contextIndex + (token.length / 2);

    let contextStartIndex = contextCenterIndex - contextOffset;

    if (contextStartIndex < 0) { contextStartIndex = 0; }

    let contextEndIndex = contextCenterIndex + contextOffset;

    if (contextEndIndex > finalIndex) { contextStartIndex = finalIndex; }

    const contextText = bodyTextRaw.substring(contextStartIndex, contextEndIndex);

    context = " – "

    + contextText.replace(searchRe, ""

    + token + "");

    }

    let classList = [];

    if (/[A-Z]/.test(token[0])) {

    classList.push("oHL_uncommonWordUppercase");

    }

    if (/[a-z]/.test(token[0])) {

    classList.push("oHL_uncommonWordLowercase");

    }

    if (!/^[A-Za-z']+$/.test(token)) {

    classList.push("oHL_uncommonWordNonASCII");

    }

    if (refTextArray.includes(token)) {

    classList.push("oHL_uncommonWordReference");

    }

    if (italicTextArray.includes(token)) {

    classList.push("oHL_uncommonWordItalic");

    }

    if (quoteTextArray.includes(token)) {

    classList.push("oHL_uncommonWordQuote");

    }

    if (wikilinkTextArray.includes(token)) {

    classList.push("oHL_uncommonWordWikilink");

    }

    if (externalTextArray.includes(token)) {

    classList.push("oHL_uncommonWordExternal");

    }

    listMarkup += "

  • " + token + ""

    + context + "

  • ";

    }

    listMarkup += "

";

searchDescriptions["oHL_uncommon"] = ["Uncommon words", ""];

$("#oHL_results").append("

Uncommon words ("

+ singleCountsUncommon.length + ")

" + listMarkup + "
");

addFrequencyFilters();

}

function addFrequencyFilters() {

const filters = [

{"id": "oHL_lowercaseFilter", "class": "oHL_uncommonWordLowercase", "label": "Lowercase", "symbol": "a"},

{"id": "oHL_uppercaseFilter", "class": "oHL_uncommonWordUppercase", "label": "Uppercase", "symbol": "A"},

{"id": "oHL_UnicodeFilter", "class": "oHL_uncommonWordNonASCII", "label": "Unicode", "symbol": "Ü"},

{"id": "oHL_italicFilter", "class": "oHL_uncommonWordItalic", "label": "Italicized", "symbol": "𝐼"},

{"id": "oHL_quoteFilter", "class": "oHL_uncommonWordQuote", "label": "Quoted", "symbol": "“"},

{"id": "oHL_wikilinkFilter", "class": "oHL_uncommonWordWikilink", "label": "Wikilinked", "symbol": "∞"},

{"id": "oHL_referenceFilter", "class": "oHL_uncommonWordReference", "label": "Reference", "symbol": "^"},

{"id": "oHL_externalFilter", "class": "oHL_uncommonWordExternal", "label": "External", "symbol": "→"}

];

filters.forEach(f => {

if (["a", "A", "Ü"].includes(f.symbol)) { return; }

const symbolMarkup = "" + f.symbol + "";

$("." + f.class + " .oHL_uncommonSymbols").append(symbolMarkup);

});

function updateFilterEnabledStatus() {

if (this.checked) { return; }

const filterId = this.parentElement.parentElement.id;

const filterClass = filters.find(f => f.id == filterId).class;

const isFilterable = $("." + filterClass).not(".oHL_uncommonWordHidden").length > 0;

if (isFilterable) {

this.disabled = false;

$(this).parent().removeClass("oHL_filterDisabled");

} else {

this.disabled = true;

$(this).parent().addClass("oHL_filterDisabled");

}

}

$("#oHL_uncommon summary").after("

Hide:
");

for (const filter of filters) {

$("#oHL_uncommonFilters").append(` `);

}

$("#oHL_uncommonFilters input").each(updateFilterEnabledStatus);

const filterCheckboxSelector = filters.map(f => "#" + f.id + " input").join(", ");

$(filterCheckboxSelector).change(function filterUncommonWords() {

const hideList = [];

const showList = [];

for (const filter of filters) {

if ($(`#${filter.id} input`).is(":checked")) {

hideList.push("." + filter.class);

} else {

showList.push("." + filter.class);

}

}

const hideSelectors = hideList.join(", ");

const showSelectors = showList.join(", ");

$(hideSelectors).addClass("oHL_uncommonWordHidden");

$(showSelectors).not(hideSelectors).removeClass("oHL_uncommonWordHidden");

$("#oHL_uncommonFilters input").each(updateFilterEnabledStatus);

});

$(filterCheckboxSelector).hover(function showFilteredHighlights() {

const filterId = this.parentElement.parentElement.id;

const filterClass = filters.find(f => f.id == filterId).class;

$("." + filterClass + " .oHL_uncommonWord").addClass("oHL_filterableWordHighlighted");

}, function hideFilteredHighlights() {

$(".oHL_filterableWordHighlighted").removeClass("oHL_filterableWordHighlighted");

});

}

function checkWikilinkCandidates(nameCandidates) {

if (nameCandidates.length == 0) {

return;

}

$("#oHL_results").append("

");

// Need to chunk since API has a limit on number of titles

for (let i = 0; i < nameCandidates.length; i+= 50) {

const nameChunk = nameCandidates.slice(i, i+50);

// API docs: https://www.mediawiki.org/w/api.php?action=help&modules=query%2Binfo

const apiUrl = location.origin + "/w/api.php";

$.ajax({

url: apiUrl,

data: {

action: "query",

prop: "description",

titles: nameChunk.join("|"),

redirects: "true",

format: "json",

origin: "*"

},

success: getCandidatesStatus

});

}

}

function getCandidatesStatus(response) {

const redirects = {};

response.query?.redirects?.forEach(r => {

const newTitle = r.to;

const oldTitle = r.from;

redirects[newTitle] = oldTitle;

});

const pages = response.query.pages;

for (const key in pages) {

let page = pages[key].title;

// TODO: make these redirections visible

if (page in redirects) {

page = redirects[page];

}

if (typeof pages[key].invalid != "undefined") {

const warningMessage = "highlightStrings.js: Warning: Invalid wikilink candidate: "

+ page;

console.warn(warningMessage);

printInternalWarning(warningMessage);

continue;

}

// Non-existent pages will have the key "missing"

if (typeof pages[key].missing != "undefined") { continue; }

const link = location.origin + "/wiki/" + page;

let shortDescriptionText = "";

if (typeof pages[key].description != "undefined") {

let shortDescription = pages[key].description;

if (shortDescription == "Topics referred to by the same term") {

shortDescription = "[disambiguation]";

}

shortDescriptionText = " – " + shortDescription;

}

$("#oHL_wikilinkCandidates_items").append("

  • " + page + "" + shortDescriptionText + "
  • ");

    $("#oHL_wikilinkCandidates").show();

    updateSummaryCount("#oHL_wikilinkCandidates");

    }

    }

    function showSingleQuotes(bodyText) {

    // Remove nested quotes

    bodyText = bodyText.replaceAll(/([ \()])"/g, '$1𐑱'); // 𐑱: left quote placeholder

    bodyText = bodyText.replace(/"([ .,;:\n\)])/g, '𐑲$1'); // 𐑲: right quote placeholder

    bodyText = bodyText.replace(/𐑱[^𐑲]+𐑲/g, '"[double quote]"');

    // Guard years

    bodyText = bodyText.replace(/'([0-9]{2,}s)/g, "__$1");

    // Guard possessive

    bodyText = bodyText.replace(/(\p{Upper}[\p{Lower}-]+[sz])' /gu, "$1__ ");

    bodyText = bodyText.replace(/([a-zA-Z])'s/g, "$1__s");

    // Guard contractions

    bodyText = bodyText.replace(/(\w)'(\w)/g, "$1__$2");

    const searchRe = new RegExp(/(?<= )'[^'\n]+'/, "gd");

    const matches = bodyText.matchAll(searchRe);

    const matchesList = [];

    for (const m of matches) {

    const start = m.indices[0][0];

    const end = m.indices[0][1];

    const context = 20;

    const leftContext = bodyText.substring(start-context, start);

    const matchText = bodyText.substring(start, end);

    const rightContext = bodyText.substring(end, end+context);

    const match = [leftContext, matchText, rightContext];

    const matchUnguarded = match.map(m => m.replaceAll("__", "'"));

    matchesList.push(matchUnguarded);

    }

    const results = new Map();

    searchDescriptions["oHL_single_quoted"] = ["Single quoted", ""];

    if (matchesList.length > 0) {

    results.set("oHL_single_quoted", matchesList);

    }

    showWikitextMatches(results);

    }

    function showEditorializing(bodyText) {

    const searches = new Map();

    const results = new Map();

    searchDescriptions["oHL_editorializing"] = ["Editorializing", ""];

    searches.set("oHL_editorializing", "\\b(fortun|sadly|ill-fated|fateful|tragedy|tragic"

    + "|suffer|mirac|lucky|luckily|happily|interesting"

    + "|curious|ironic|definitely|exclaimed|famous|fame|infamy"

    + "|prestigious|renowned|made headlines|iconic|elegant"

    + "|acclaimed|visionary|outstanding|leading|celebrated"

    + "|lauded|legend|exceptional|spectacular|remarkable"

    + "|amazing|amazed|extraordinar|world-class|greatest"

    + "|surprising|unexpect|a twist|bizzare|puzzling|incredibl"

    + "|heroic|brave|courage|daring|beautiful|respected|embod"

    + "|respectable|forefront|tasty|disturbing|ingenious|genius"

    + "|a hit|phenomenal|innovat|pioneer|reput|in fact|brillian"

    + "|strategic|life[ -]changing|state[ -]of[ -]the[ -]art"

    + "|cutting[ -]edge|creatively|awesome|amusing|obvious"

    + "|contrary|mere|so-called|of course|despite|in spite|begs"

    + "|not to mention|should be noted|startling|dominat|flood"

    + "|a testament to|sacrificed|brain ?child|needless|epitome"

    + "|surely|gripped by|embroiled|feasted|quite literally"

    + "|boast|premium|high[ -]end|sensation|attract|vicious"

    + "|violently|bolted|impeccable|decadent|well[ -]known|forever"

    + "|immortaliz|devastat|rave|raving|true calling|talent"

    + "|delcious|undoubtably|profound|elite|distinguished|excit"

    + "|worse|coinciden|flourish|hail|only|knack|enjoy|onslaught"

    + "|insatiable|portent|evil|wonder|proclaim|kids|kid "

    + "|appreciat|greats|excellen|extremely|atroci|awful"

    + "|craze|exclusive|free[ -]think|reinvent|latest|easily|must-"

    + "|comprehensive|un-?paralleled|leader|recognized|luxur"

    + "|constantly|extensive|pinnacle|sophisticated|unlock"

    + "|great deal|good deal|forged|best[ -]?sell|overcome"

    + "|overcame|offer|utterly|achiev|greatly|perfect|ambitious"

    + "|synerg|timeless|emphasi|exemplif|undeniabl|shocking"

    + "|unquestionably|astound|astonish|disastrous|triumph"

    + "|unmatched|game[ -]?chang|legendary|breath-?taking"

    + "|jaw-drop|mind-blowing|underrated|masterful|superb"

    + "|magnificent|dazzling|flawless|unbelievable|and yet"

    + "|monumental|indeed|certainly|notori|signature|beloved"

    + "|hallmark|pivotal|revolutionary|exemplary|trailblaz"

    + "|groundbreaking|gripping)");

    searchDescriptions["oHL_weasel"] = ["Weasel words", ""];

    searches.set("oHL_weasel", "(some say|by some|it is said)");

    searchDescriptions["oHL_euph"] = ["Euphemisms", ""];

    searches.set("oHL_euph", "(passed away|left behind|survived by|mortal remains"

    + "|gave (her|his|their) life|ma(d|k)e love|battle with)");

    searchDescriptions["oHL_instructional"] = ["Instructional words", ""];

    searches.set("oHL_instructional", "\\b(you\\b|your|should\\b|must|do not|are advised"

    + "|we find|our|us\\b)");

    searchDescriptions["oHL_temporal"] = ["Temporal words", ""];

    searches.set("oHL_temporal", "(will |planned|scheduled|is going| current "

    + "|currently| present| now|today|tomorrow)");

    // searches.set("Punctuation marks", "(\\!|\\?)");

    for (const [type, re] of searches) {

    const searchRe = new RegExp(re, "gid");

    const matches = bodyText.matchAll(searchRe);

    const matchesList = [];

    for (const m of matches) {

    const start = m.indices[0][0];

    const end = m.indices[0][1];

    const context = 30;

    const leftContext = bodyText.substring(start-context, start);

    const matchText = bodyText.substring(start, end);

    const rightContext = bodyText.substring(end, end+context);

    matchesList.push([leftContext, matchText, rightContext]);

    }

    if (matchesList.length > 0) {

    results.set(type, matchesList);

    }

    }

    showWikitextMatches(results);

    }

    function showUnbalanced(bodyText) {

    const unbalanced = [];

    const brackets = {

    "\"\"": /"/g,

    "“”": [/“/g, /”/g],

    "()": [/\(/g, /\)/g],

    "[]": [/\[/g, /\]/g]

    };

    for (const line of bodyText.split("\n")) {

    for (const [bracket, bracketRe] of Object.entries(brackets)) {

    const bracketLeft = bracket[0];

    const bracketRight = bracket[1];

    let isUnbalanced = false;

    if (bracketLeft == '"') {

    const count = (line.match(bracketRe) || []).length;

    if (count % 2 != 0) {

    isUnbalanced = true;

    }

    } else {

    const leftCount = (line.match(bracketRe[0]) || []).length;

    const rightCount = (line.match(bracketRe[1]) || []).length;

    if (leftCount != rightCount) {

    isUnbalanced = true;

    }

    }

    if (isUnbalanced) {

    let lineFormatted = line.replaceAll(bracketLeft, ""

    + bracketLeft + "");

    if (bracketRight != bracketLeft) {

    lineFormatted = lineFormatted.replaceAll(bracketRight, ""

    + bracketRight + "");

    }

    unbalanced.push(lineFormatted);

    }

    }

    }

    if (unbalanced.length == 0) { return; }

    let list = "

      ";

      for (const entry of unbalanced) {

      list += "

    • " + entry + "
    • ";

      }

      list += "

    ";

    $("#oHL_results").append("

    Unbalanced quotes and brackets ("

    + unbalanced.length + ")

    " + list + "
    ");

    }

    function showRedirects() {

    // Placeholder

    searchDescriptions["oHL_redirects"] = ["Incoming redirects", ""];

    $("#oHL_results").append("

    Incoming redirects (0)
    ");

    // API docs: https://www.mediawiki.org/w/api.php?action=help&modules=query%2Bredirects

    const apiUrl = location.origin + "/w/api.php";

    $.ajax({

    url: apiUrl,

    data: {

    action: "query",

    prop: "redirects",

    rdprop: "title|fragment",

    rdnamespace: "0",

    rdlimit: "500",

    format: "json",

    titles: mw.config.get("wgPageName")

    },

    success: listRedirects

    });

    }

    function listRedirects(response) {

    const pageId = mw.config.get("wgArticleId");

    const redirects = response.query.pages[pageId].redirects;

    let redirectText = "No redirects.";

    let redirectCount = 0;

    const redirectCandidatesMarkup = $(".oHL_title, p:first-of-type i[lang], p:first-of-type i [lang], .infobox-above .fn").clone();

    redirectCandidatesMarkup.find(".oHL_ruby rt, .oHL_piped, .oHL_added").remove();

    let redirectCandidates = redirectCandidatesMarkup.toArray().map(e => $(e).text()

    .replaceAll("\u2060", "").replaceAll("\u200C", "").replaceAll("\u200D", "").replaceAll("\u200E", "").replaceAll("\u00AD", ""));

    const pageTitle = mw.config.get("wgTitle");

    const pageTitleWithoutParens = pageTitle.replace(/ \(.*\)/, "");

    const pageTitleWithoutSubtitle = pageTitle.replace(/: .*/, "");

    redirectCandidates.push(pageTitleWithoutSubtitle);

    redirectCandidates = redirectCandidates.filter(c => c.toLowerCase() != pageTitle.toLowerCase())

    .filter(c => c.toLowerCase() != pageTitleWithoutParens.toLowerCase());

    if (typeof redirects != "undefined") {

    redirectText = "";

    redirectCount = redirects.length;

    redirects.sort((a, b) => a.title.localeCompare(b.title));

    redirects.forEach(r => {

    redirectText += "

  • + "&redirect=no'>" + r.title + "";

    const candidateAlreadyInRedirects = redirectCandidates.some(c => c.toLowerCase() == r.title.toLowerCase());

    if (candidateAlreadyInRedirects) {

    redirectCandidates = redirectCandidates.filter(c => c.toLowerCase() != r.title.toLowerCase());

    }

    if (typeof r.fragment != "undefined") {

    const fragment = r.fragment.replaceAll(" ", "_");

    const fragmentEscaped = CSS.escape(fragment);

    if ($("#" + fragmentEscaped).length) {

    redirectText += " →

    } else {

    redirectText += " → ❌

    }

    redirectText += " href='#" + fragment + "'>§" + fragment + "";

    }

    redirectText += "

  • ";

    });

    }

    // TODO: just update the children instead of the whole element

    searchDescriptions["oHL_incoming_redirects"] = ["Incoming redirects", ""];

    $("#oHL_redirects").html("

    Incoming redirects ("

    + redirectCount+")

      "

      + redirectText + "

    ");

    if (redirectCandidates.length > 0) {

    let redirectCandidatesText = "";

    for (const candidate of redirectCandidates) {

    redirectCandidatesText += "

  • + "'>" + candidate + "

  • ";

    }

    searchDescriptions["oHL_redirect_candidates"] = ["New redirect candidates", ""];

    $("#oHL_redirects").after("

    New redirect candidates ("

    + redirectCandidates.length +")

      "

      + redirectCandidatesText + "

    ");

    // Note: has a race condition if this request runs first and we don't

    // have the wikilink candidates yet

    $("#oHL_redirects li a:first-of-type").each(function removeRedirectsFromWikilinkCandidates() {

    const linkText = $(this).text().replaceAll("'", "%27");

    $("#oHL_wikilinkCandidates a[href$='" + linkText + "']").parent().remove();

    });

    }

    }

    // Check if page is linked to from disambig page

    function checkDisambigLink() {

    const pageTitle = mw.config.get("wgTitle");

    if (pageTitle.slice(-1) != ")") {

    return;

    }

    // API docs: https://www.mediawiki.org/w/api.php?action=help&modules=query%2Blinkshere

    const apiUrl = location.origin + "/w/api.php";

    $.ajax({

    url: apiUrl,

    data: {

    action: "query",

    prop: "linkshere",

    lhprop: "title",

    lhnamespace: "0",

    lhlimit: "500",

    format: "json",

    titles: mw.config.get("wgPageName")

    },

    success: searchDisambigLink

    });

    }

    function searchDisambigLink(response) {

    const pageId = mw.config.get("wgArticleId");

    const incomingLinks = response.query.pages[pageId].linkshere;

    if (typeof incomingLinks == "undefined") { return; }

    const pageTitle = mw.config.get("wgTitle");

    const pageTitleWithoutParens = pageTitle.replace(/ \(.*\)$/, "");

    const pageTitleDab = pageTitleWithoutParens + " (disambiguation)";

    for (const link of incomingLinks) {

    if (link.title == pageTitleWithoutParens || link.title == pageTitleDab) {

    return;

    }

    }

    const dabMarkup = "

    + "'>" + pageTitleWithoutParens + " or

    + "'>" + pageTitleDab + "";

    searchDescriptions["oHL_noDabLink"] = ["Incoming disambiguation link missing", ""];

    $("#oHL_results").append("

    ");

    }

    function showWikitextMatches(results) {

    if (results.size === 0) {

    return;

    }

    let resultsHTML = "";

    for (const [id, matches] of results) {

    const name = searchDescriptions[id][0];

    resultsHTML += "

    " + name

    + " (" + matches.length

    + ")

      ";

      matches.forEach(m => resultsHTML += "

    • "

      + mw.html.escape(m[0])

      + ""

      + mw.html.escape(m[1]) + ""

      + mw.html.escape(m[2]) + "

    • ");

      resultsHTML += "

    ";

    }

    $("#oHL_results").append(resultsHTML);

    }

    // Check italicization of wikilinks

    function getItalics() {

    const wikilinks = $(".oHL_wikilink").toArray();

    const whitelist = $(".oHL_reflist .oHL_wikilink, .navbox .oHL_wikilink,"

    + " .stub .oHL_wikilink, .hatnote .oHL_wikilink").toArray();

    const filteredWikilinks = wikilinks.filter(wl => !whitelist.includes(wl));

    const links = {};

    const crossNamespaceRe = /[a-z]:[A-Z]/;

    filteredWikilinks.forEach(l => {

    if (l.title === "" || crossNamespaceRe.test(l.title)) { return; }

    links[l.title] = l; // {title: selector}

    });

    searchDescriptions["oHL_italicization"] = ["Italicization", ""];

    $("#oHL_results").append("

    ");

    const titles = Object.keys(links);

    console.log("highlightStrings.js: Getting DefaultSort for " + titles.length + " pages");

    // Need to chunk since API has a limit on number of titles

    for (let i = 0; i < titles.length; i+= 50) {

    const titleChunk = titles.slice(i, i+50);

    getDisplayTitles(links, titleChunk);

    }

    }

    function getDisplayTitles(links, titles) {

    // API docs: https://www.mediawiki.org/w/api.php?action=help&modules=query%2Bpageprops

    const apiUrl = location.origin + "/w/api.php";

    $.ajax({

    url: apiUrl,

    data: {

    action: "query",

    prop: "pageprops",

    ppprop: "displaytitle",

    format: "json",

    titles: titles.join("|"),

    redirects: "yes",

    },

    success: response => checkItalics(links, response)

    });

    }

    function checkItalics(links, response) {

    const redirects = {};

    response.query?.redirects?.forEach(r => {

    const newTitle = r.to;

    const oldTitle = r.from;

    redirects[newTitle] = oldTitle;

    });

    checkSelfRedirects(redirects);

    Object.values(response.query.pages).forEach(p => {

    let title = p.title;

    if (title in redirects) {

    title = redirects[title];

    }

    const element = links[title];

    matchDescriptions["oHL-title-en"] = ["Title en dash", "The wikilinked article uses an en dash. (see MOS:ENDASH)"];

    const elementClone = $(element).clone();

    elementClone.find(".oHL_ruby rt, .oHL_piped, .oHL_added").remove();

    const originalText = $(elementClone).text();

    if (originalText.includes("-") && title.includes("–")) {

    $(element).addClass("oHL oHL-title-en");

    updateMatches();

    }

    const originalItalicized = $(element).parent("i").length;

    let displayItalicization = p?.pageprops?.displaytitle || "[None]";

    if (!displayItalicization.includes("")) {

    displayItalicization = "[None]";

    }

    // Skip Foo (Bar)

    if (/\(/.test(displayItalicization)) {

    return;

    }

    if (!originalItalicized && displayItalicization != "[None]"

    || originalItalicized && displayItalicization == "[None]") {

    const originalDisplay = $(element).clone();

    $(originalDisplay).find("*").each(function cleanOriginalElement() {

    this.removeAttribute("class");

    this.removeAttribute("lang");

    });

    $(originalDisplay).find("rt").remove();

    if (originalItalicized) { originalDisplay.wrapInner(""); }

    const originalDisplayMarkup = $(originalDisplay).html();

    if (originalDisplayMarkup == displayItalicization) { return; }

    $("#oHL_italicization_items").append("

  • " + originalDisplayMarkup

    + " "

    + displayItalicization + "

  • ");

    $("#oHL_italicization").show();

    updateSummaryCount("#oHL_italicization");

    }

    });

    }

    function updateSummaryCount(selector) {

    const element = $(selector);

    const count = element.children("ul").children().length;

    const countElement = element.find(".oHL_summaryCount");

    countElement.text("(" + count + ")");

    }

    // Find any redirects that lead back to article we're on

    function checkSelfRedirects(redirects) {

    const selfRedirects = [];

    const currentPage = mw.config.get("wgTitle");

    matchDescriptions["oHL-self-redirect"] = ["Self-redirect", "Wikilinks should not lead back to the current article."];

    for (const [target, wikilink] of Object.entries(redirects)) {

    if (target == currentPage) {

    let titleEncoded = wikilink.replaceAll(" ", "_");

    titleEncoded = encodeURIComponent(titleEncoded).replaceAll("'", "%27");

    $("[href^='/wiki/" + titleEncoded + "']").addClass("oHL oHL-self-redirect");

    updateMatches();

    }

    }

    }

    // Find dead interwiki links

    function getDeadInterwikis() {

    const links = {};

    $("#mw-content-text .extiw").each(function getInterwikiLinks() {

    const url = new URL(this.href);

    const pageEncoded = url.pathname.replace("/wiki/", "");

    const page = decodeURIComponent(pageEncoded);

    if (page == "") { return true; }

    if (!(url.host in links)) {

    links[url.host] = [page];

    } else {

    links[url.host].push(page);

    }

    });

    if (Object.keys(links).length == 0) {

    return;

    }

    for (const [hostname, pages] of Object.entries(links)) {

    // API docs: https://www.mediawiki.org/w/api.php?action=help&modules=query%2Binfo

    const apiUrl = "https://" + hostname + "/w/api.php";

    $.ajax({

    url: apiUrl,

    data: {

    action: "query",

    prop: "info",

    titles: pages.join("|"),

    format: "json",

    origin: "*"

    },

    success: getLinkStatus

    });

    }

    }

    function getLinkStatus(response) {

    const pages = response.query.pages;

    matchDescriptions["oHL-dead-interwiki"] = ["Dead interwiki link", "Interwiki links should not lead to non-existent pages."];

    for (const key in pages) {

    // Non-existent pages will have the key "missing"

    if (typeof pages[key].missing != "undefined") {

    const page = pages[key].title;

    const titleEncoded = encodeURIComponent(page).replaceAll("'", "%27");

    $("[href*='/wiki/" + titleEncoded + "']").addClass("oHL oHL-dead-interwiki");

    updateMatches();

    }

    }

    }

    // Low-res images

    function getFreeImages() {

    const images = [];

    $(".oHL_image").each(function getImages() {

    let filename = $(this).parent().attr("href")?.split("/").at(-1);

    if (!filename) { return true; }

    if (!filename.includes("File:")) { return true; }

    if (filename.includes(".svg")) { return true; }

    filename = decodeURIComponent(filename);

    images.push(filename);

    });

    // Need to chunk since API has a limit on number of titles

    for (let i = 0; i < images.length; i+= 50) {

    const imageChunk = images.slice(i, i+50);

    // API docs: https://m.mediawiki.org/wiki/API:Imageinfo

    const apiUrl = location.origin + "/w/api.php";

    $.ajax({

    url: apiUrl,

    data: {

    action: "query",

    prop: "imageinfo",

    titles: imageChunk.join("|"),

    iiprop: "extmetadata|url",

    iiextmetadatafilter: "NonFree",

    format: "json",

    origin: "*"

    },

    success: findSmallImages

    });

    }

    }

    function findSmallImages(response) {

    const pages = response.query.pages;

    for (const key in pages) {

    if (typeof pages[key].imageinfo[0].extmetadata.NonFree != "undefined") {

    const filename = pages[key].imageinfo[0].descriptionurl?.split("/").at("-1");

    const filenameEscaped = CSS.escape(filename);

    $("[href*='" + filenameEscaped + "']").children().first("img").addClass("oHL_nonfree");

    }

    }

    matchDescriptions["oHL-low-res"] = ["Low resolution free image", "Images that are free should have higher resolution equivalents used if possible."];

    const lowResWhitelist = "[src*=logo], [src*=Logo], [src*=icon], [src*=Icon],"

    + " [src*=flag], [src*=Flag]";

    $(".oHL_lowResolution").not(".oHL_nonfree").not(lowResWhitelist)

    .parent().next(".oHL_img_info")

    .find(".oHL_img_info_dimensions_original")

    .addClass("oHL oHL-low-res");

    updateMatches();

    }

    // Cosmetic changes

    function tweakDisplay() {

    // Italics

    $("#mw-content-text i, #mw-content-text i a").addClass("oHL_i");

    $("h1 i, sup i, sup a, .stub i, .stub a, .ambox i, .ambox a").removeClass("oHL_i");

    // Clears

    $("div[style='clear:both;']").after("[clear]");

    // Anchors

    $(".anchor").each(function showAnchors() {

    const id = $(this).attr("id");

    const anchorMarkup = "#" + id + "";

    // If it's inside a heading

    $(this).closest(".mw-heading").after(anchorMarkup);

    // If it's inside a regular paragraph

    $(this).closest("p").before(anchorMarkup);

    });

    // Short descriptions

    $(".shortdescription").first().each(function showShortDescriptions() {

    this.style.display = "";

    $(this).prepend("[Short description]: ");

    });

    // Ruby

    wrapRuby("em", "em");

    wrapRuby(".official-website", "official");

    wrapRuby("#mw-content-text big", "big");

    wrapRuby("#mw-content-text small", "small");

    wrapRuby("span[dir=rtl]", "RTL");

    wrapRuby("span.plainlinks", "plainlink");

    wrapRuby(".external[class*='mw-magiclink']", "magic");

    wrapRuby(".vanchor", "#vanchor");

    wrapRuby(".smallcaps", "smallcaps");

    $("#otherImages-pageImage").after("

    [Wikidata image]

    ");

    // Show piped link targets

    showPiped();

    // Show duplicate refs on hover

    $(".oHL-duplicated-ref").on("mouseover", function highlightDupeLinks() {

    const href = $(this).attr("href");

    const hrefCleaned = href.replace(/https?:\/\//, "");

    $("a[href*='" + hrefCleaned + "'].oHL_dupe_ref").addClass("oHL_dupe_ref_active");

    }).on("mouseout", _ => $(".oHL_dupe_ref_active").removeClass("oHL_dupe_ref_active"));

    // Reload scripts since we replaced the HTML

    mw.loader.load("//en.wikipedia.org/w/index.php?title=MediaWiki:Gadget-ReferenceTooltips.js&action=raw&ctype=text/javascript");

    mw.loader.using("ext.popups");

    }

    function wrapRuby(selector, label) {

    document.querySelectorAll(selector)?.forEach(e => {

    const rubyElement = document.createElement("ruby");

    rubyElement.className = "oHL_ruby";

    const innerRb = document.createElement("rb");

    rubyElement.appendChild(innerRb);

    const rtElement = document.createElement("rt");

    rtElement.textContent = label;

    rubyElement.appendChild(rtElement);

    e.parentElement.insertBefore(rubyElement, e);

    innerRb.appendChild(e);

    });

    }

    function displayMatches() {

    $("#mw-content-text").before("

    ");

    $("#mw-content-text").before("

    ");

    $("#mw-content-text").before("


    ");

    $("#oHL_results").append("");

    window.addEventListener("keypress", keyListener, false);

    $("#oHL_info_left_arrow").click(function previousHighlight() {

    advanceHighlight(-1);

    });

    $("#oHL_info_right_arrow").click(function nextHighlight() {

    advanceHighlight(1);

    });

    updateMatches();

    }

    function updateMatches() {

    const matches = getMatchTotal(".oHL");

    const optionalMatches = getMatchTotal(".oHL-opt");

    const totalMatches = matches + optionalMatches;

    let alertMessage;

    if (totalMatches !== 0) {

    alertMessage = "Matches: " + matches + "
    Optional: " + optionalMatches;

    $("#oHL_info_total").text(totalMatches);

    $("#oHL_info").show();

    } else {

    alertMessage = "No matches.";

    $("#oHL_info").hide();

    }

    $("#oHL_matches").html(alertMessage);

    $("#oHL_summary").remove();

    getMatchesSummary(totalMatches);

    const gearIconURL = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3c/OOjs_UI_icon_advanced_apex.svg/20px-OOjs_UI_icon_advanced_apex.svg.png";

    $("#oHL_summary").append("

    ");

    // Hover display

    const oHLElements = $(".oHL, .oHL-opt");

    oHLElements.unbind("mouseenter.oHL").unbind("mouseleave.oHL")

    .on("mouseenter.oHL", showHighlightName)

    .on("mouseleave.oHL", _ => $("#oHL_hover").hide());

    oHLElements.each(function findLeftoverHLClasses() {

    const hlCLasses = getHLClasses([ ...this.classList ]);

    if (hlCLasses.length == 0) {

    const warningMessage = "highlightStrings.js: Warning: Found lone oHL class: "

    + this.outerHTML + ".";

    console.warn(warningMessage);

    printInternalWarning(warningMessage);

    return false;

    }

    });

    }

    function getMatchTotal(selector) {

    let count = 0;

    $(selector).each(function countTotalMatches() {

    const hlClasses = getHLClasses([ ...this.classList ]);

    count += hlClasses.length;

    });

    return count;

    }

    function showHighlightName(e) {

    if ($(e.target).hasClass("oHL_disabled")) { return; }

    const hlClasses = getHLClasses([ ...e.target.classList ]);

    let hlTexts = [];

    for (const hlClass of hlClasses) {

    let hlText = hlClass;

    if (hlClass in matchDescriptions) {

    hlText = matchDescriptions[hlClass][0];

    }

    hlTexts.push(hlText);

    }

    const hlTextsCombined = hlTexts.join(" · ");

    $("#oHL_hover").text(hlTextsCombined);

    $("#oHL_hover").css("top", "calc(" + e.clientY + "px - 2.4em)");

    $("#oHL_hover").css("left", e.clientX);

    $("#oHL_hover").show();

    }

    function getMatchesSummary(totalMatches) {

    // Wikify highlight descriptions

    for (const [hlName, hlProps] of Object.entries(matchDescriptions)) {

    let desc = hlProps[1];

    if (desc.includes("/wiki/")) { continue; }

    desc = desc.replace(/\[\[([^\[]+)\]\]/g, "$1");

    // Remove underscores in section links

    const underscoreRe = />([^<]*?)_([^<]*?)

    while (underscoreRe.test(desc)) {

    desc = desc.replace(underscoreRe, ">$1 $2<");

    }

    desc = desc.replace(/>([^<]*?)#([^<]*?)$1 § $2<"); // section links

    desc = desc.replace(/{{([^{]+)}}/g, "{{$1}}");

    matchDescriptions[hlName][1] = desc;

    }

    const counts = new Map();

    $(".oHL, .oHL-opt").each(function incrementCounts() {

    const hlClasses = getHLClasses([ ...this.classList ]);

    if (typeof hlClasses == "undefined") { return true; }

    for (const hlClass of hlClasses) {

    if (!counts.has(hlClass)) {

    counts.set(hlClass, 1);

    } else {

    const c = counts.get(hlClass);

    counts.set(hlClass, c+1);

    }

    }

    });

    if (counts.size === 0) { return; }

    const countsSorted = Array.from(counts.entries()).sort((a, b) => b[1] - a[1]);

    let tableHTML = "

    ";

    let countsTotal = 0;

    for (const entry of countsSorted) {

    const hlClass = entry[0];

    let hlText = hlClass;

    let hlDesc = "";

    if (hlClass in matchDescriptions) {

    hlText = matchDescriptions[hlClass][0];

    hlDesc = matchDescriptions[hlClass][1];

    }

    const count = entry[1];

    tableHTML += "

    ";

    countsTotal += count;

    }

    tableHTML += "

    "

    + hlText + "

    " + hlDesc + ""

    + count + "

    ";

    $("#oHL_results").prepend("

    Highlights"

    + " (" + totalMatches

    + ")

    " + tableHTML+ "
    ");

    if (countsTotal != totalMatches) {

    const warningMessage = "highlightStrings.js: Warning: Not all oHL matches have a class.";

    console.warn(warningMessage);

    printInternalWarning(warningMessage);

    }

    // Allow toggling specific matches

    $("#oHL_summary input").change(toggleResult);

    // Checkbox to de/select all

    $("#oHL_summary tbody").before(""

    + " NameDescriptionCount");

    $("#oHL_checkAll").data("total", counts.size);

    $("#oHL_checkAll").data("checked", counts.size);

    $("#oHL_checkAll").change(e => {

    if (!e.target.checked) {

    $("#oHL_summary input:checked").not("#oHL_checkAll").click();

    } else {

    $("#oHL_summary input:not(:checked)").not("#oHL_checkAll").click();

    }

    });

    }

    function toggleResult(e) {

    const oHLclass = $(e.target).attr("oHLclass");

    const element = $("." + oHLclass);

    if (!e.target.checked) {

    // Disable

    $(element).addClass("oHL_disabled");

    if ($(element).hasClass("oHL_added")) {

    $(element).hide();

    }

    $("input[oHLclass='" + oHLclass + "']:checked").prop("checked", false);

    updateCheckAllBox(-1);

    } else {

    // Enable

    $(element).removeClass("oHL_disabled");

    if ($(element).hasClass("oHL_added")) {

    $(element).show();

    }

    $("input[oHLclass='" + oHLclass + "']:not(checked)").prop("checked", true);

    updateCheckAllBox(1);

    }

    }

    function updateCheckAllBox(change) {

    const totalBoxes = $("#oHL_checkAll").data("total");

    let checkedBoxes = $("#oHL_checkAll").data("checked");

    checkedBoxes += change;

    $("#oHL_checkAll").data("checked", checkedBoxes);

    if (checkedBoxes == totalBoxes) {

    $("#oHL_checkAll").prop("checked", true);

    $("#oHL_checkAll").prop("indeterminate", false);

    } else if (checkedBoxes == 0) {

    $("#oHL_checkAll").prop("checked", false);

    $("#oHL_checkAll").prop("indeterminate", false);

    } else {

    $("#oHL_checkAll").prop("checked", true);

    $("#oHL_checkAll").prop("indeterminate", true);

    }

    }

    function getHLClasses(classArray) {

    return classArray.filter(c => c != "oHL-opt" && c.startsWith("oHL-"));

    }

    function keyListener(event) {

    let offset;

    event = event || window.event;

    const key = event.key || event.which;

    if (key === "n") {

    offset=1;

    } else if (key === "N") {

    offset=-1;

    } else {

    return;

    }

    advanceHighlight(offset);

    }

    function advanceHighlight(offset) {

    let index;

    const highlightList = $(".oHL, .oHL-opt").toArray();

    const currentHighlight = $(".oHL_keyed").first();

    if (currentHighlight.length == 0) {

    index = -1;

    } else {

    index = highlightList.findIndex(e => e == currentHighlight.get(0));

    }

    let nextIndex = index;

    let nextHighlight;

    let nextIsDisabled = true;

    // Search highlightList until we find an enabled highlight or reach the end

    while(nextIsDisabled) {

    nextIndex += offset;

    nextHighlight = highlightList[nextIndex];

    // No next higlight

    if (typeof nextHighlight == "undefined") {

    // Pulse highlight

    $(".oHL_keyed").animate({"border-width": "4px"}, 150);

    $(".oHL_keyed").animate({"border-width": "2px"}, 100);

    return;

    }

    nextIsDisabled = $(nextHighlight).hasClass("oHL_disabled");

    }

    if (!$(nextHighlight).is(":visible")) {

    const warningMessage = "highlightStrings.js: Warning: Highlighted invisible element: "

    + $(nextHighlight).html() + ".";

    console.warn(warningMessage);

    printInternalWarning(warningMessage);

    // Walk up DOM and make parents visible

    let parent = nextHighlight.parentElement;

    while (parent != null && parent.id != "bodyContent") {

    $(parent).show();

    parent = parent.parentElement;

    }

    }

    $(".oHL_keyed").removeClass("oHL_keyed");

    const hlClasses = getHLClasses([ ...nextHighlight.classList ]);

    let hlTexts = [];

    for (const hlClass of hlClasses) {

    let hlText = hlClass;

    let hlDescription = "";

    if (hlClass in matchDescriptions) {

    hlText = matchDescriptions[hlClass][0];

    hlDescription = matchDescriptions[hlClass][1];

    }

    hlTexts.push("

    " + hlText

    + "

    "

    + hlDescription + "

    ");

    }

    const hlTextsCombined = hlTexts.join("
    ");

    $("#oHL_info_class").html(hlTextsCombined);

    $("#oHL_info_class input").change(toggleResult);

    // Adjust arrows being grayed out

    const finalIndex = parseInt($("#oHL_info_total").text()) - 1;

    if (offset == -1) { // left

    if (index == finalIndex) { // we were at the end

    $("#oHL_info_right_arrow").removeClass("oHL_arrow_disabled");

    }

    if (nextIndex == 0) { // we hit the beginning

    $("#oHL_info_left_arrow").addClass("oHL_arrow_disabled");

    }

    } else { // right

    if (index == 0) { // we were at the beginning

    $("#oHL_info_left_arrow").removeClass("oHL_arrow_disabled");

    }

    if (nextIndex == finalIndex) { // we hit the end

    $("#oHL_info_right_arrow").addClass("oHL_arrow_disabled");

    }

    }

    let displayIndex = nextIndex+1;

    // Add spacing to counter

    const targetDigits = finalIndex.toString().length;

    const currentDigits = displayIndex.toString().length;

    const deltaDigits = targetDigits - currentDigits;

    if (deltaDigits > 0) {

    const padding = "0";

    displayIndex = padding.repeat(deltaDigits) + displayIndex;

    }

    $("#oHL_info_counter").html(displayIndex);

    nextHighlight.classList.add("oHL_keyed");

    nextHighlight.scrollIntoView();

    }

    var mangleIndex = 0;

    const mangled = [];

    var mangleSkipCount = 0;

    var mangleIdIndex = 0;

    var mangleIdReuseCount = 0;

    function mangle(element, attr) {

    const original = element.getAttribute(attr);

    // Empty attribute

    if (!original) {

    return;

    }

    // Don't waste resources on simple attributes

    // But still do titles since wikilinks duplicate text in them

    const simpleAttributeRe = /^[\w]+$/;

    if (attr != "title" && simpleAttributeRe.test(original)) {

    mangleSkipCount++;

    return;

    }

    const placeholder = "mangle" + mangleIndex++;

    if (attr == "id") {

    element.setAttribute("id", placeholder);

    } else {

    // We change the attribute name so imgs aren't reloaded as 404s

    element.setAttribute("hs-" + attr, placeholder);

    element.removeAttribute(attr);

    }

    /*

    * Id lookups are fast so let's reuse them or add our own

    * Ideally we could just cache element references, but we rewrite

    * the HTML, invalidating them

    */

    let targetId;

    if (element.id !== "") {

    targetId = element.id;

    mangleIdReuseCount++;

    } else {

    targetId = "mangleId" + mangleIdIndex++;

    element.id = targetId;

    }

    mangled.push({"selector": targetId, "attr": attr, "value": original});

    }

    function unmangle(original) {

    const element = document.getElementById(original.selector);

    if (element === null) {

    const warningMessage = "highlightStrings.js: Warning: " + original.selector

    + " doesn't exist!";

    console.warn(warningMessage);

    printInternalWarning(warningMessage);

    return;

    }

    element.setAttribute(original.attr, original.value);

    }

    var detachIndex = 0;

    const detached = {};

    function detachTemp() {

    const placeholder = "_hsdetach" + detachIndex++;

    const newElement = document.createElement("span");

    newElement.id = placeholder;

    this.parentNode.insertBefore(newElement, this);

    this.remove();

    detached[placeholder] = this;

    }

    function reattachTemp() {

    for (const [target, html] of Object.entries(detached)) {

    const element = document.getElementById(target);

    if (element == null) {

    const warningMessage = "highlightStrings.js: Warning: Could not reattach "

    + target + " (" + html + "), either because"

    + " element was broken or because it's a"

    + " child of another detached element.";

    console.warn(warningMessage);

    printInternalWarning(warningMessage);

    continue;

    }

    element.parentNode.insertBefore(html, element.nextSibling); // insertAfter

    }

    }

    function removeHLClass(element, highlightClass) {

    let oldHighlights = element.getAttribute("oHL_prev_highlights");

    if (oldHighlights == null) {

    oldHighlights = highlightClass;

    } else {

    oldHighlights += ", " + highlightClass;

    }

    element.setAttribute("oHL_prev_highlights", oldHighlights);

    $(element).removeClass(highlightClass);

    const hlCLasses = getHLClasses([ ...element.classList ]);

    if (hlCLasses.length == 0) {

    $(element).removeClass("oHL oHL-opt");

    }

    }

    function whitelist() {

    for (const selector of filterList) {

    const highlightClass = selector.split(".").at(-1);

    $(selector).each(function whitelistElements() {

    if ($(this).hasClass("oHL_added")) {

    $(this).remove();

    return true;

    }

    removeHLClass(this, highlightClass);

    });

    if (highlightClass in matchDescriptions === false) {

    const warningMessage = "highlightStrings.js: Warning: Invalid filter class: "

    + highlightClass + ".";

    console.warn(warningMessage);

    printInternalWarning(warningMessage);

    }

    }

    // Bolded letter in Further reading and Sources sections

    $("#Further_reading, #Sources").parent().nextUntil(".mw-heading2")

    .find(".oHL-bolded-letter").each(function filterBoldedLetterHighlight() {

    removeHLClass(this, "oHL-bolded-letter");

    });

    // Code and syntax highlighting

    //$("pre .oHL, pre .oHL-opt").removeClass("oHL oHL-opt");

    // Handle cases where dates are followed by refs

    $(".oHL-datecomma + .reference").each(function filterDateCommas() {

    const oHLelement = this.previousElementSibling;

    let finalRefElement = this;

    let sibling = finalRefElement.nextElementSibling;

    while (sibling != null && sibling.classList.contains("reference")) {

    finalRefElement = sibling;

    sibling = finalRefElement.nextElementSibling;

    }

    const textSibling = finalRefElement.nextSibling;

    if (textSibling == null || textSibling.nodeType == 3 && textSibling.textContent.startsWith(")")) {

    $(oHLelement).remove();

    }

    });

    // Full names in first section of biographies

    $("[id^=Early_life], [id^=Early_years], [id^=Early_child], [id^=Biography], [id^=Life], [id^=Personal_life], [id^=Childhood]")

    .first().parent().nextUntil(".mw-heading2").filter("p").first()

    .find(".oHL-fullname").each(function filterFullNames() {

    removeHLClass(this, "oHL-fullname");

    });

    }

    function showImageInfo() {

    // Remove styling on multiple images so size shows

    $(".tmulti").find(".thumbimage").removeAttr("style");

    // Ignore templates

    $(".noviewer img").addClass("noviewer");

    $(".ambox img, .stub img, .dmbox img, .navbox img, .mwe-math-element img, .locmap img, .flagicon img").addClass("noviewer");

    const extensions = ["jpg", "jpeg", "webp", "png", "gif", "tif", "tiff", "svg",

    "avif", "heif", "pdf", "xcf"];

    $("[typeof^='mw:File'] img, .gallery img").not(".noviewer").each(function getImageInfo() {

    const displayWidth = $(this).attr("width");

    const displayHeight = $(this).attr("height");

    const originalWidth = $(this).attr("data-file-width");

    const originalheight = $(this).attr("data-file-height");

    if (displayWidth < 30) { return; }

    $(this).addClass("oHL_image");

    if (!$(this).attr("src").endsWith(".svg.png")) {

    const megaPixels = originalWidth * originalheight / 1000000;

    if (megaPixels < 0.1) {

    $(this).addClass("oHL_lowResolution");

    }

    }

    let imgAlt = $(this).attr("alt");

    // Don't include autogenerated alt text

    if (imgAlt?.includes(".")) {

    const imgAltlower = imgAlt.toLowerCase();

    for (const ext of extensions) {

    if (imgAltlower.endsWith(ext)) {

    imgAlt = null;

    break;

    }

    }

    }

    // MediaWiki autogenerates alt text for galleries

    const galleryBoxElement = $(this).closest(".gallerybox");

    if (galleryBoxElement.length) {

    const galleryCaptionElement = $(galleryBoxElement).find(".gallerytext").clone();

    galleryCaptionElement.find(".oHL_ruby rt, .oHL_piped, .oHL_added").remove();

    const caption = $(galleryCaptionElement).text();

    if (imgAlt == caption) {

    imgAlt = null;

    }

    }

    matchDescriptions["oHL-dupe-alt"] = ["Duplicate ALT text", "Alternative text for images should not repeat the caption. (MOS:ALT)"];

    let captionElement;

    let infoboxElement = $(this).closest(".infobox-image");

    if (infoboxElement.length) {

    captionElement = $(infoboxElement).find(".infobox-caption").clone();

    } else {

    captionElement = $(this).closest(".mw-file-description").siblings("figcaption").clone();

    }

    captionElement.find(".oHL_ruby rt, .oHL_piped, .oHL_added").remove();

    const imgCaption = $(captionElement).text();

    let filenameMatch = $(this).parent(".mw-file-description").attr("href")

    ?.match(/(Image|File):(.*)\.(jpe?g|webp|png|gif|tiff?|svg|avif|heif|pdf|xcf)$/i);

    let filename;

    if (filenameMatch && filenameMatch.length >= 3) {

    filename = filenameMatch[2].replaceAll("_", " ");

    }

    if ((imgAlt && imgCaption && imgAlt.toLowerCase() == imgCaption.toLowerCase())

    || (imgAlt && filename && imgAlt.toLowerCase() == filename.toLowerCase())) {

    imgAlt = "" + imgAlt + "";

    }

    let displayMessage = ""

    + parseInt(displayWidth).toLocaleString() + "×" + parseInt(displayHeight).toLocaleString()

    + " ("

    + parseInt(originalWidth).toLocaleString() + "×" + parseInt(originalheight).toLocaleString()

    + ")";

    if (imgAlt) {

    displayMessage += "
    [Alt: `" + imgAlt + "`]


    ";

    }

    $(this).parent().after("

    " + displayMessage + "
    ");

    matchDescriptions["oHL-missing-upright"] = ["Image missing upright", "Tall images should use the |upright| parameter so they don’t appear larger compared to wide images. (see MOS:UPRIGHT)"];

    const thumbSizeOptions = [120, 150, 180, 200, 220, 250, 300, 400];

    const thumbPreference = mw.user.options.get("thumbsize");

    const thumbSize = thumbSizeOptions[thumbPreference];

    const aspectRatio = displayWidth / displayHeight;

    if (displayWidth == thumbSize && aspectRatio < 0.75) {

    $(this).closest("figure").find(".oHL_img_info_dimensions_display").addClass("oHL oHL-missing-upright");

    }

    });

    }

    function showPiped() {

    // Ignore ISBN labels

    $(".oHL_wikilink[href^='/wiki/ISBN_(identifier)']").addClass("oHL_ISBN_pre");

    $(".navbox a, .sidebar a, .infobox th a, .infobox b a, .oHL_ISBN,"

    + " .oHL_ISBN_pre, sup a, #disambigbox a, .stub a, .ambox a, .portalbox a,"

    + " .cs1-visible-error a, .cs1-maint a, .tfd a").addClass("oHL_no_pipe");

    // Note: doesn't handle redlinks

    $(".oHL_wikilink:not(.oHL_no_pipe)").each(function getPipeInfo() {

    const text = this.textContent;

    const target = this.getAttribute("title");

    if (target && this.textContent !== ""

    && text.toLowerCase() !== target.toLowerCase()) {

    const pipedName = document.createElement("span");

    pipedName.classList.add("oHL_piped");

    const smallText = document.createElement("small");

    smallText.textContent = target;

    pipedName.appendChild(smallText);

    const bigPipe = document.createElement("span");

    bigPipe.textContent = "|";

    bigPipe.classList.add("oHL_piped-pipe");

    pipedName.appendChild(bigPipe);

    this.insertAdjacentElement("afterbegin", pipedName);

    }

    });

    }

    function checkSectionOrder() {

    // First, build a list of all section ids

    const sections = [];

    $("#mw-content-text h2").each(function getSectionIds() {

    sections.push($(this).attr("id"));

    });

    const len = sections.length;

    if (len < 2) { return; } // Too few sections

    // Make sure "External links" section is last

    matchDescriptions["oHL-nonfinal-ext"] = ["Non-final External links section", "The External links section should be the last subsection. (MOS:LAYOUT)"];

    if (sections[len-1] !== "External_links") {

    $("#External_links").parent().after("

    [Move section last↓]

    ");

    }

    // "See also" section last

    matchDescriptions["oHL-misplaced-seeAlso"] = ["Misplaced See also", "The See also section should be in the proper order of sections. (MOS:LAYOUT)"];

    if (sections[len-1] === "See_also") {

    $("#See_also").parent().after("

    [Move section up↑]

    ");

    return;

    }

    const endSections = ["References", "Sources", "Notes", "Explanatory_notes",

    "Footnotes", "Bibliography", "Notes_and_references",

    "Citations"];

    for (let i=0; i

    if (sections[i] === "See_also") {

    if (!endSections.includes(sections[i+1])) {

    $("#See_also").parent().after("

    [Move section↕]

    ");

    }

    break;

    }

    }

    // Further reading not after References

    matchDescriptions["oHL-misplaced-furtherReading"] = ["Misplaced Further reading", "The Further reading section should go after the References section. (MOS:LAYOUT)"];

    for (let i=1; i

    if (sections[i] === "Further_reading") {

    if (!endSections.includes(sections[i-1])) {

    $("#Further_reading").parent().after("

    [Move section↕]

    ");

    }

    break;

    }

    }

    }

    function checkOverlinking() {

    const allWikilinks = $(".oHL_wikilink").toArray();

    const ignoreLinks = $(".infobox .oHL_wikilink, .navbox .oHL_wikilink,"

    + " table .oHL_wikilink,"

    + " .sidebar .oHL_wikilink,"

    + " .quotebox .oHL_wikilink,"

    + " .thumbcaption .oHL_wikilink,"

    + " figcaption .oHL_wikilink,"

    + " .gallery .oHL_wikilink,"

    + " .quotebox .oHL_wikilink,"

    + " .hatnote .oHL_wikilink,"

    + " .succession-box .oHL_wikilink,"

    + " .oHL_reflist .oHL_wikilink,"

    + " .listen .oHL_wikilink,"

    + " .spoken-wikipedia .oHL_wikilink,"

    + " .mw-ext-score .oHL_wikilink,"

    + " .mw-tmh-player .oHL_wikilink,"

    + " .Inline-Template .oHL_wikilink").toArray();

    const seeAlsoLinks = $("#See_also").parent().nextUntil(".mw-heading2").filter("ul").find(".oHL_wikilink").toArray();

    // See also links already in body

    matchDescriptions["oHL-duplicate-seeAlso"] = ["Duplicate See also", "The See also section should not contain wikilinks already in the body. (MOS:NOTSEEALSO)"];

    let whitelist = ignoreLinks.concat(seeAlsoLinks);

    let filteredLinks;

    if ($("#See_also").length) {

    filteredLinks = allWikilinks.filter(link => !whitelist.includes(link));

    const allHrefs = filteredLinks.map(l => l.getAttribute("href"));

    for (const link of seeAlsoLinks) {

    const href = link.getAttribute("href");

    if (allHrefs.includes(href)) {

    $(link).addClass("oHL oHL-duplicate-seeAlso");

    }

    }

    }

    // Any links that occur more than once besides the lead

    const leadLinks = leadMarker.prevAll().find(".oHL_wikilink").toArray();

    whitelist = whitelist.concat(leadLinks);

    const ignoreSections = refSectionsSelector + ", #Cast, #Filmography, #Discography,"

    + " #Bibliography, #Further_reading, #External_links,"

    + " #Works, #Selected_works";

    for (const section of ignoreSections.split(", ")) {

    const sectionLinks = $(section).parent().nextUntil(".mw-heading2").find(".oHL_wikilink").toArray();

    whitelist = whitelist.concat(sectionLinks);

    }

    filteredLinks = allWikilinks.filter(link => !whitelist.includes(link));

    const ignoreHrefs = ["/wiki/ISBN_(identifier)", "/wiki/OCLC_(identifier)"];

    const linkCounts = new Map();

    for (const link of filteredLinks) {

    const href = link.getAttribute("href");

    if (ignoreHrefs.includes(href)) {

    continue;

    }

    const links = linkCounts.get(href);

    if (typeof links == "undefined") {

    linkCounts.set(href, [link]);

    } else {

    links.push(link);

    }

    }

    matchDescriptions["oHL-overlink"] = ["Overlink", "Aside from the lead, the text of an article should generally only contain a link once. (MOS:LINKONCE)"];

    for (const [href, links] of linkCounts) {

    if (links.length < 2) { continue; }

    for (let i = 1; i < links.length; i++) {

    $(links[i]).addClass("oHL-opt oHL-overlink");

    }

    }

    }

    // TODO: Use a blacklist as well

    function checkTitleItalicization() {

    matchDescriptions["oHL-category-italics"] = ["Category italicization", "Based on its categorization, the page’s title might need to be italicized."];

    if ($("#firstHeading > i").length > 0) {

    return;

    }

    let categories = "";

    $("#catlinks li a").each(function getCategories() {

    categories += $(this).text() + " ";

    });

    const works = ["books", "novels", "films", "anime", "Manga series", " plays",

    "television series", "albums", "paintings", "magazines",

    "journals", "graphic novels", "sculptures", "cases",

    "video games", "ships"];

    for (const w of works) {

    if (categories.includes(w)) {

    $("#mw-content-text").prepend("

    [Italicize title] (" + w + " category)

    ");

    return;

    }

    }

    }

    function checkContrast() {

    matchDescriptions["oHL-color-contrast"] = ["Color contrast", "Text must have a minimum contrast (WCAG AA) for accessibility. (MOS:COLOR)"];

    $(".infobox td[style], .infobox th[style], .navbox th[style],"

    + " .wikitable td[style], .wikitable th[style]").each(function checkTemplateContrast() {

    const style = this.style.cssText;

    if (!style.includes("transparent") && /(background:|background-color:|color:|#\w)/.test(style)) {

    checkElementContrast(this);

    }

    });

    $(".quotebox").each(function checkQuoteboxContrast() {

    checkElementContrast(this);

    });

    }

    function checkElementContrast(element) {

    // e.g. "rgb(6, 69, 173)" => [6, 69, 173]

    // can also have alpha, e.g. "rgba(0, 0, 0, 0)"

    const parseColorString = s => {

    const m = s.match(/rgba?\(([0-9]+), ([0-9]+), ([0-9]+)/);

    return [m[1], m[2], m[3]];

    };

    const text = $(element).text();

    if (/^\s+$/.test(text)) {

    return;

    }

    const textColorString = window.getComputedStyle(element).color;

    const bgColorString = window.getComputedStyle(element)["background-color"];

    const textColor = parseColorString(textColorString);

    const bgColor = parseColorString(bgColorString);

    // Want at least 4.5:1 ratio for WCAG AA

    // Reference: https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html

    const contrast = calculateContrastRatio(textColor, bgColor);

    const decToHex = d => Number(d).toString(16).padStart(2, '0');

    if (contrast < 4.5) {

    const textColorHex = textColor.map(decToHex).join("");

    const bgColorHex = bgColor.map(decToHex).join("");

    $(element).append(" [AIM]");

    }

    }

    // Reference: https://stackoverflow.com/questions/596216/formula-to-determine-perceived-brightness-of-rgb-color/

    function calculateContrastRatio(c1RGB, c2RGB) {

    let c1Luminance = rgbToLuminance(c1RGB);

    let c2Luminance = rgbToLuminance(c2RGB);

    if (c1Luminance < c2Luminance) { // want lighter first

    [c1Luminance, c2Luminance] = [c2Luminance, c1Luminance];

    }

    const ratio = (c1Luminance + 0.05) / (c2Luminance + 0.05);

    return ratio;

    }

    function rgbToLuminance(RGB) {

    // Convert integers to decimal

    const RGBdec = RGB.map(i => i / 255);

    // Convert to linear value

    const RGBtoLinear = RGB => {

    if (RGB <= 0.04045) {

    return RGB / 12.92;

    } else {

    return Math.pow(((RGB + 0.055) / 1.055), 2.4);

    }

    };

    const RGBlinear = RGBdec.map(RGBtoLinear);

    // Find luminance

    const luminance = 0.2126 * RGBlinear[0] + 0.7152 * RGBlinear[1] + 0.0722 * RGBlinear[2];

    // Convert to perceived lightness

    let perceivedLuminance;

    if (luminance <= (216/24389)) {

    perceivedLuminance = luminance * (24389/27);

    } else {

    perceivedLuminance = Math.pow(luminance, (1/3)) * 116 - 16;

    }

    return perceivedLuminance;

    }

    // If we're not reading an article, do nothing

    if (mw.config.get("wgAction") === "view"

    && mw.config.get("wgIsArticle")

    && !mw.config.get("wgIsMainPage")) {

    $(mw.util.addPortletLink("p-tb", "#", "Highlight strings", "hStrings",

    "Highlight errors", "h")).click(highlightStrings);

    }

    //