User:Opencooper/highlightStrings.js
Appearance
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump. This code will be executed when previewing this page. |
![]() | This user script seems to have a documentation page at User:Opencooper/highlightStrings and an accompanying .css page at User:Opencooper/highlightStrings.css. |
// 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 */
// <nowiki>
// TODO: compare to https://wiki.riteme.site/wiki/Wikipedia:WikiProject_Check_Wikipedia/List_of_errors
// and https://wiki.riteme.site/wiki/Wikipedia:AutoWikiBrowser/General_fixes
"use strict";
function printError(message, source, lineno, colno, error) {
if (source.includes("highlightStrings") || source.includes("jquery")) {
$("#contentSub").after("<div class='oHL_error'>highlightStrings.js: Error: "
+ mw.html.escape(message) + " [line: " + lineno
+ ", column: " + colno + "]</div>");
addReportButton();
}
return false;
}
// Print warnings that are page breaking
function printExternalWarning(message) {
$("#contentSub").after("<div class='oHL_externalWarning'>highlightStrings.js: Warning:"
+ mw.html.escape(message) + "</message>");
addReportButton();
}
// Print warnings that are not important enough to show to users
function printInternalWarning(message) {
$("#contentSub").after("<div class='oHL_internalWarning'>[Internal] "
+ mw.html.escape(message) + "</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("//wiki.riteme.site/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("<button class='oHL_reportButton cdx-button'><a target='_blank' href='"
+ reportLink + "'><span class='cdx-icon'><img src='" + feedbackIconURL
+ "'></span> Report Problem</a></button>");
});
}
// Sanitize to avoid false positives
function preClean() {
// Prefetch
$("head").append("<link rel='prefetch' href='" + wordListURL + "'/>");
// 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("<span class='oHL oHL-nonlist-ref oHL_added'>[*]</span> ");
}
});
// 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("<span class='oHL oHL-seeAlso-portal oHL_added'>[Portal↓]</span>");
}
// 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("<span class='oHL oHL-non-list oHL_added'>[*]</span> ");
}
});
// 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 = "<span class='oHL oHL-list-period'>.</span>";
$("#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("<p><span class='oHL oHL-commons-template oHL_added'>[Needs Commons template]</span></p>");
}
// 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("<p><span class='oHL oHL-wikisource-template oHL_added'>[Needs Wikisource template]</span></p>");
}
// 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("<span class='oHL oHL-commons-inline oHL_added'>[* Make template inline]</span>");
}
});
// 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("<span class='oHL oHL-missing-caption oHL_added'>[Needs caption]</span>");
$(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("<p><span class='oHL oHL-nonlinear-header oHL_added'>[Non-sequential header level]</span></p>");
// 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("<p><span class='oHL oHL-header-level oHL_added'>[Wrong header level]</span></p>");
}
// Thumbnails in infoboxes (uncommon?)
matchDescriptions["oHL-infobox-thumbnail"] = ["Infobox thumbnail", "Instead of embedding another thumbnail, infoboxes support the <code>image_size</code>/<code>image_upright</code> 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 <code><tt></code> 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 <code><html></code> 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("<p><span class='oHL oHL-misplaced-sisterbox oHL_added'>[Relocate box↕]</span></p>");
// 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("<p><span class='oHL oHL-misplaced-template oHL_added'>[Move template up↑]</span></p>");
// 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("<p><span class='oHL oHL-misplaced-quotebox oHL_added'>[Relocate quote box↕]</span></p>");
// Horizontal rules
matchDescriptions["oHL-hr"] = ["Horizontal rule", "Horizontal rules should not be used for separation. Instead, use section headings."];
$("hr").before("<span class='oHL-opt oHL-hr oHL_added'>[horizontal rule]</span>");
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 = " <span class='oHL-opt oHL-insecure-site oHL_added'>[http]</span>";
$(".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(" <span class='oHL-opt oHL-infobox-title-br oHL_added'><br></span>");
$(".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, "<span class='oHL-opt oHL-romaji'>$1</span>");
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("<span class='oHL oHL-hatnote-misuse oHL_added'>[rm]</span> ");
}
});
// 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("<p><span class='oHL oHL-infobox-placement oHL_added'>[Move infobox to top↑]</span></p>");
// 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("<p><span class='oHL oHL-sidebar-placement oHL_added'>[Move sidebar after lead↓]</span></p>");
});
// 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("<p><span class='oHL oHL-low-hatnote oHL_added'>[Move hatnote up↑]</span></p>");
// Hatnote below maintenance template
matchDescriptions["oHL-hatnote-placement"] = ["Hatnote placement", "Hatnotes should be placed above maintenance templates. ([[WP:HNP]])"];
$(".ambox + .hatnote").after("<p><span class='oHL oHL-hatnote-placement oHL_added'>[Move hatnote up↑]</span></p>");
// 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(" <span class='oHL oHL-italquote oHL_added'>[Italicized quote]</span>");
}
});
// Empty sections
matchDescriptions["oHL-empty-section"] = ["Empty section", "Empty sections should be deleted or filled by a placeholder. (e.g. {{Empty section}})"];
const emptySectionMarkup = "<span class='oHL oHL-empty-section oHL_added'>[Empty section]</span>";
$(".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("<span class='oHL-opt oHL-anchor-link oHL_added'>[§]</span> ");
}
});
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(" <span class='oHL oHL-seeAlso-section-link oHL_added'>[§]</span>");
}
});
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("<p><span class='oHL oHL-misplaced-image oHL_added'>[Move images]</span></p>");
}
});
// 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("<p><span class='oHL oHL-ext-reflist oHL_added'>[Wrapped in Refbegin]</span></p>");
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("<p><span class='oHL oHL-spaced-list oHL_added'>[Spaced list]</span></p>");
}
});
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("<span class='oHL oHL-lower-infobox oHL_added'>[↑]</span>");
}
});
// 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("<span class='oHL oHL-lower-seeAlso oHL_added'>[↑]</span>");
}
});
// 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 <code>website</code> parameter at [[Template:Infobox person]])"];
$(".infobox .external").each(function findFullURLs() {
const text = $(this).text();
if (/^http/.test(text)) {
$(this).prepend("<span class='oHL oHL-plain-site oHL_added'>[URL]</span> ");
}
});
// 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(`"</span>'`)) { // {{" '}} template
return true;
}
const text = $(this).text();
if (text.charAt(0) == '"') {
$(this).html(html.slice(1));
$(this).prepend("<span class='oHL oHL-redundant-quotes'>\"</span>");
}
});
// 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("<span class='oHL-opt oHL-wikilink-punc'>" + finalChar + "</span>");
}
});
$("#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 = "<h2><span class='oHL oHL-missing-ref-section oHL_added'>[References]</span></h2>";
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("<span class='oHL oHL-table-header oHL_added'>[Missing table headers]</span>");
}
});
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("<p><span class='oHL oHL-unwrapped-poem oHL_added'>[Unwrapped poem]</span></p>");
}
});
// 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("<span class='oHL oHL-missing-sentence-period oHL_added'>[.]</span>");
}
});
// 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("<span class='oHL oHL-missing-paragraph-period oHL_added'>[.]</span>");
}
});
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 <code><br></code> element) and there should only be a single break between paragraphs. For lists, use <code>*</code> (or {{ubl}} in infoboxes). For poems, use <code><poem></code> tags wrapped in <code><blockquote></code>."];
// $("p br").each(function findParagraphBreaks() {
// if (this.previousSibling === null) {
// $(this).before("<span class='oHL oHL-br'><br></span>");
// }
// });
$("#mw-content-text p br").before("<span class='oHL oHL-br oHL_added'><br></span>");
// 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("<span class='oHL-opt oHL-infobox-br oHL_added'><br></span> ");
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("<span class='oHL-opt oHL-table-br oHL_added'><br></span> ");
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('<span class="cs1-kern-left"></span>"',
'<span class="cs1-kern-left"></span><span class="oHL oHL-nested-quote">"</span>');
html = html.replaceAll('"<span class="cs1-kern-right"></span>',
'<span class="oHL oHL-nested-quote">"</span><span class="cs1-kern-right"></span>');
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(/^""/, '"<span class="oHL oHL-nested-quote">"</span>');
html = html.replace(/""$/, '<span class="oHL oHL-nested-quote">"</span>"');
html = html.replace(/ "/g, ' <span class="oHL oHL-nested-quote">"</span>');
html = html.replace(/" /g, '<span class="oHL oHL-nested-quote">"</span> ');
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 <code>|thumb|</code>, 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("<p><span class='oHL oHL-bad-indent oHL_added'>[Improper indent]</span></p>");
}
});
// 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("<span class='oHL oHL-dl-indent oHL_added'>[→]</span> ");
// Unindented math
matchDescriptions["oHL-math-indent"] = ["Unindented math", "Math placed on its own line should be indented using <code><math display=block></code> ([[MOS:MATH#Using_LaTeX_markup]])"];
$(".oHL_bad-math-indent .mwe-math-element").each(function findUnindentedMath() {
$(this).prepend("<span class='oHL oHL-math-indent oHL_added'>[→]</span> ");
});
// 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 <code><math display=inline></code> ([[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");
// <center> tag
matchDescriptions["oHL-center"] = ["Center tag", "The HTML <code><center></code> tag is deprecated. Content, such as captions, should not be arbitrarily centered."];
$("center").each(function findCenterTags() {
$(this).before("<p><span class='oHL oHL-center oHL_added'>[center tag]</span></p>");
});
// 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("<p><span class='oHL-opt oHL-toc oHL_added'>[Hide ToC]</span></p>");
}
// Missing lead
matchDescriptions["oHL-missing-lead"] = ["Missing lead", "The article is missing a lead. ([[MOS:LEAD]])"];
if (leadMarker.prevAll("p").length === 0) {
leadMarker.before("<p><span class='oHL oHL-missing-lead oHL_added'>[Missing lead]</span></p>");
}
// Table with missing cells
matchDescriptions["oHL-missing-cells"] = ["Missing table cells", "Tables should not have missing cells. Add empty cells with placeholders (<code>—</code>) 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("<p><span class='oHL-opt oHL-missing-cells oHL_added'>[Table missing cells]</span></p>");
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("<p><span class='oHL-opt oHL-redundant-headers oHL_added'>[Table misuses headers]</span></p>");
// }
// });
// Authority control before navboxes
matchDescriptions["oHL-auth-placement"] = ["Authority control placement", "Authority control templates should go after navboxes. ([[MOS:ORDER]])"];
$(".authority-control + .navbox").before("<p><span class='oHL oHL-auth-placement oHL_added'>[Move authority control down↓]</span></p>");
// 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("<p><span class='oHL-opt oHL-div-col oHL_added'>[Split into columns]</span></p>");
// }
// });
// 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("<p><span class='oHL oHL-list-img oHL_added'>[Move image interrupting list]</span></p>");
// 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("<p><span class='oHL oHL-thumb-placement oHL_added'>[Relocate image]</span></p>");
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://wiki.riteme.site/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("<span class='oHL oHL-attrib-dash oHL_added'>[—]</span> ");
// }
// });
// 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(" <span class='oHL oHL-description-dupe oHL_added'>[duplicates title]</span>");
}
const firstChar = text[0];
if (/[a-z]/.test(firstChar)) {
text = text.slice(1);
$(this).text(text);
$(this).prepend("<span class='oHL oHL-description-uppercase'>"
+ firstChar + "</span>");
}
const finalChar = text.at(-1);
if (!text.endsWith("U.S.") && finalChar == ".") {
text = text.slice(0, -1);
$(this).text(text);
$(this).append("<span class='oHL oHL-description-punc'>" + finalChar + "</span>");
}
if (length > 100) {
$(this).append(" <span class='oHL-opt oHL-description-length oHL_added'>[too long] ("
+ length + " chars)</span>");
}
});
// 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("<p><span class='oHL oHL-missing-desc oHL_added'>[Missing short description]</span></p>");
}
// 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(" <span class='oHL oHL-description-placement oHL_added'>[Move up↑]</span>");
}
// 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("<p><span class='oHL oHL-col-count oHL_added'>[Too many columns]</span></p>");
}
});
// Self-ref hatnotes
matchDescriptions["oHL-self-ref"] = ["Self-ref hatnote", "Hatnotes that link to Wikipedia pages should use the <code>|selfref=yes</code> 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("<p><span class='oHL-opt oHL-stub-space oHL_added'>[¶]</span></p>");
// 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(' "', ' <span class="oHL oHL-title-quote-bold">"</span>');
html = html.replaceAll('" ', '<span class="oHL oHL-title-quote-bold">"</span> ');
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(" <span class='oHL oHL-overciting oHL_added'>[Overciting]</span>");
}
});
// 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("<p><span class='oHL oHL-image-sandwich oHL_added'>[Sandwiched images]</span></p>");
// 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("<p class='oHL_orphanedRefs'><span class='oHL oHL-orphaned-refs oHL_added'>[Orphaned references]</span></p>");
}
$(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("<p><span class='oHL oHL-obstructed-reflist oHL_added'>[Obstructed reflist]</span></p>");
}
});
}
// Font size
matchDescriptions["oHL-font-size-change"] = ["Font size", "Reduced or enlarged font sizes should be used sparingly. ([[MOS:SMALL]])"];
$(".div-col-small").before("<p><span class='oHL-font-size-change oHL_added'>[Font size]</span></p>");
// 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, ' <span class="oHL oHL-pp">p.</span>$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<span class="oHL oHL-rangedash">-</span>$2');
html = html.replace(/(\d)-present/g, '$1<span class="oHL oHL-rangedash">-</span>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. (<code>–</code> or <code>—</code>; [[MOS:DASH]])"];
html = html.replace(/(---|--|–-|-–|-—|—-)/g, '<span class="oHL oHL-typewriter-dash">$1</span>');
filterList.push(".oHL_reflist .oHL-typewriter-dash");
matchDescriptions["oHL-spaced-endash"] = ["Spaced dash", "Spaced dashes should use the proper Unicode character for en dashes. (<code>–</code>; [[MOS:ENDASH]])"];
html = html.replaceAll(' - ', '<span class="oHL oHL-spaced-endash"> - </span>');
html = html.replaceAll(' -', ' <span class="oHL oHL-spaced-endash">-</span>');
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(/( —|(?<!—)— )/g, '<span class="oHL oHL-spaced-emdash">$1</span>');
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<span class="oHL oHL-bad-rangedash">—</span>$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 <span class="oHL oHL-spaced-range">–</span> $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<span class="oHL-opt oHL-unspaced-endash">–</span>');
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. (<code>−</code>; [[MOS:NEGATIVE]])"];
html = html.replace(/([ >(])([-–—])(\d)/g, '$1<span class="oHL oHL-bad-minus">$2</span>$3');
html = html.replace(/ ([A-FO]{1,3})([-–—])([.,;:\)\n<"' ])/g, ' $1<span class="oHL oHL-bad-minus">$2</span>$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, '<span class="oHL oHL-bad-quote">$1</span>');
html = html.replaceAll('′s', '<span class="oHL oHL-bad-quote">′</span>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, '<span class="oHL oHL-mismatched-quotes">"</span>$1<span class="oHL_quoteAdditional">\'</span>$2');
html = html.replace(/(\W)'(\w+)"/g, '$1<span class="oHL oHL-mismatched-quotes">\'</span>$2<span class="oHL_quoteAdditional">"</span>');
matchDescriptions["oHL-foreign-quote"] = ["Non-English quote mark", "Wikipedia only uses straight quote marks. ([[MOS:STRAIGHT]])"];
html = html.replace(/([„«»‹›])/g, '<span class="oHL oHL-foreign-quote">$1</span>');
filterList.push(".oHL_reflist .oHL-foreign-quote", ".tfd .oHL-foreign-quote");
// Nested quote mark
html = html.replace(/([.,;:] ?)""/g, '$1<span class="oHL oHL-nested-quote">""</span>');
matchDescriptions["oHL-adj-quote"] = ["Adjacent quote marks", "Adjacent quote marks should have their kerning adjusted. (see {{\" '}} and {{' \"}})"];
html = html.replace(/((?<= )"'|'"(?!>))/g, '<span class="oHL-opt oHL-adj-quote">$1</span>');
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<span class="oHL oHL-punc-in-quote">$2</span>$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, '<span class="oHL oHL-aposS-italics">$1</span></i>');
//</i>'s
html = html.replaceAll(/<\/i>('s)/g, '</i><span class="oHL oHL-aposS-italics">$1</span>');
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, '<span class="oHL-opt oHL-italpunc">$1</span></i>');
html = html.replace(/([,;:\])/])<\/b>/g, '<span class="oHL-opt oHL-boldpunc">$1</span></b>');
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<span class="oHL-opt oHL-formatted-quotemark">$2</span>');
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<span class="oHL-opt oHL-formatted-bracket">$2</span>');
filterList.push(".ambox .oHL-formatted-bracket");
// Text which should use <em>
// html = html.replace(/(<i>[a-z]{2,}<\/i>)/g, '$1 <span class="oHL-opt oHL-emph-italic oHL_added">[em]</span>');
// 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(/(<b>[^<\n]*)"/g, '$1<span class="oHL oHL-bold-quotemark">"</span>');
// 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(')</b>', '<span class="oHL oHL-boldparen">)</span></b>');
// Bolded single letters
matchDescriptions["oHL-bolded-letter"] = ["Bolded letter", "Avoid boldface for emphasis or variables. ([[MOS:NOBOLD]]; [[MOS:TEXT#Mathematics_variables]])"];
html = html.replace(/<b>(\w)<\/b>/g, '<b><span class="oHL oHL-bolded-letter">$1</span></b>');
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, '<span class="oHL oHL-lang-han">$1</span>$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, '<span class="oHL oHL-lang-kor">$1</span>$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, '<span class="oHL oHL-lang-cyrl">$1</span>$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, '<span class="oHL oHL-lang-grk">$1</span>$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, '<span class="oHL oHL-lang-deva">$1</span>$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, '<span class="oHL oHL-lang-ara">$1</span>$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, '<span class="oHL oHL-lang-heb">$1</span>$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, '<span class="oHL oHL-lang-tam">$1</span>$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, '<span class="oHL oHL-lang-arm">$1</span>$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, '<span class="oHL oHL-lang-thai">$1</span>$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,})/, '<span class="oHL-opt oHL-isbn">$1</span>');
// Ellipses
// Two ellipses
matchDescriptions["oHL-two-dots"] = ["Incomplete ellipsis", "Ellipses should have three dots."];
html = html.replace(/([^.])\.\.([^.])/g, '$1<span class="oHL oHL-two-dots">..</span>$2');
filterList.push(".oHL_reflist .oHL-two-dots", "a[href*='adsabs.harvard.edu'] .oHL-two-dots");
// html = html.replaceAll('…', '<span class="oHL oHL-ellipsis-char">…</span>');
// 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<span class="oHL oHL-four-dots">....</span>');
// Interspaced ellipses
matchDescriptions["oHL-spaced-ellipsis"] = ["Interspaced ellipsis", "Ellipses should not have spaces in between. ([[MOS:ELLIPSES]])"];
html = html.replace(/(\. \. \.|\. \. \.)/g, '<span class="oHL oHL-spaced-ellipsis">. . .</span>');
// 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, '<span class="oHL oHL-bracketed-ellipsis">$1</span>');
// 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<span class="oHL oHL-unspaced-ellipsis">...</span>$2');
html = html.replaceAll(/(&[^ "'€])\.\.\. /g, '$1<span class="oHL oHL-unspaced-ellipsis">...</span> ');
html = html.replaceAll(/ \.\.\.([^ .?!:;,)\]])/g, ' <span class="oHL oHL-unspaced-ellipsis">...</span>$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, '<span class="oHL oHL-nbsp-multi">$1</span>');
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, '<span class="oHL oHL-bad-nbsp">$1</span>');
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 <code>&nbsp;</code>. ([[MOS:NBSP]])"];
html = html.replace(/([0-9]) (dozen|hundred|thousand|million|billion|trillion)/g, "$1<span class='oHL-opt oHL-nbsp'> </span>$2");
html = html.replaceAll(" ...", "<span class='oHL-opt oHL-nbsp'> </span>...");
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, '<span class="oHL oHL-unspaced-period">$1</span>$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, '<span class="oHL oHL-spaced-punc">$1</span>');
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(' ', '<span class="oHL oHL-full-space"> </span>');
matchDescriptions["oHL-spaced-chars"] = ["Spaced characters", "Characters should not have spacing between them."];
html = html.replace(/ (["'“‘’”\[\]()]) /g, ' <span class="oHL oHL-spaced-chars">$1</span> ');
matchDescriptions["oHL-spaced-ref"] = ["Spaced reference", "References should not be preceded by a space. ([[MOS:REFSPACE]])"];
html = html.replaceAll(' <sup', '<span class="oHL oHL-spaced-ref"> </span><sup');
matchDescriptions["oHL-unspaced-ref"] = ["Unspaced reference", "References should be followed by a space."];
html = html.replace(/\/sup>(\w)/g, '/sup><span class="oHL oHL-unspaced-ref">$1</span>');
html = html.replace(/\/sup><i>(\w)/g, '/sup><i><span class="oHL oHL-unspaced-ref">$1</span>');
// 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, '<span class="oHL oHL-ft-inch">$1</span>');
// Multiplication
matchDescriptions["oHL-mult-sign"] = ["Multiplication sign", "Multiplication should use the proper Unicode character, <code>×</code>, instead of the Latin letter <i>x</i>. ([[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, '$1<span class="oHL oHL-mult-sign">x</span>');
html = html.replace(/ x /g, ' <span class="oHL oHL-mult-sign">x</span> ');
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, '<span class="oHL oHL-unspaced-paren">$1</span>');
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, '<span class="oHL oHL-unspaced-paren">$1</span>');
matchDescriptions["oHL-adj-parens"] = ["Adjacent parentheses", "Avoid adjacent parenthesis. ([[MOS:PAREN]])"];
html = html.replace(/(\) ?\()/g, '<span class="oHL oHL-adj-parens">$1</span>');
filterList.push(".oHL_reflist .oHL-adj-parens");
matchDescriptions["oHL-nested-parens"] = ["Nested parentheses", "Nested parentheticals should utilize square brackets (<code>[]</code>)."];
html = html.replace(/(\([^)\n]+)\(/g, '$1<span class="oHL oHL-nested-parens">(</span>');
// html = html.replaceAll('))', '<span class="oHL oHL-nested-parens">))</span>');
filterList.push(".oHL_img_info .oHL-nested-parens");
// Minus sign
matchDescriptions["oHL-minus-score"] = ["Minus sign", "The proper Unicode minus sign, <code>−</code>, should be used instead of dashes. ([[MOS:MINUS]])"];
html = html.replace(/( [A-F])([-–—])([.,;: ])/g, ' $1<span class="oHL oHL-minus-score">$2</span>$3');
// Note: have never encountered this highlight.
matchDescriptions["oHL-plus-minus"] = ["Plus/minus sign", "The proper Unicode plus-minus sign, <code>±</code>, should be used."];
html = html.replace(/(\+\/[-–—−])/g, '<span class="oHL oHL-plus-minus">$1</span>');
// Exponents and subscripts
matchDescriptions["oHL-subsup"] = ["Precomposed sub/superscript", "Instead of precomposed Unicode characters for subscripts or superscripts, use <code><ref></code> tags, or <code><sub></code>; and <code><sup></code> tags. ([[MOS:SUPERSCRIPT]])"];
html = html.replace(/([²³¹⁰ⁱ⁴⁵⁶⁷⁸⁹⁺⁻⁼⁽⁾ⁿ₀₁₂₃₄₅₆₇₈₉₊₋₌₍₎ₐₑₒₓₔₕₖₗₘₙₚₛₜᴬᴮᴰᴱᴳᴴᴵᴶᴷᴸᴹᴺᴼᴾᴿᵀᵁⱽᵂᶦᶫᶰᶸᵃᵇᶜᵈᵉᶠᵍʰⁱʲᵏˡᵐⁿᵒᵖʳˢᵗᵘᵛʷˣʸᶻₐₑₕᵢⱼₖₗₘₙₒₚᵣₛₜᵤᵥₓᵝᵞᵟᶿᶥᵠᵡᵦᵧᵨᵩᵪ⏨])/g, '<span class="oHL oHL-subsup">$1</span>');
// Fractions
// html = html.replace(/([½¼¾⅐⅑⅒⅓⅔⅕⅖⅗⅘⅙⅚⅛⅜⅝⅞⅟↉])/g, '<span class="oHL oHL-frac-char">$1</span> ');
matchDescriptions["oHL-frac-slash"] = ["Fraction slash", "Fractions should use the {{frac}} template. ([[MOS:FRAC]])"];
html = html.replace(/([0-9])\/([0-9])/g, '$1<span class="oHL oHL-frac-slash">/</span>$2');
html = html.replaceAll("sup>/<sub", 'sup><span class="oHL oHL-frac-slash">/</span><sub');
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(/<sup>(th|st|[nr]d)/g, '<sup><span class="oHL oHL-ordinal">$1</span>');
html = html.replace(/([ªº])/g, '<span class="oHL oHL-ordinal">$1</span>');
// Substed nihongo question mark
matchDescriptions["oHL-nihongo-question"] = ["Substed nihongo template", "The {{nihongo}} template should be be substituted."];
html = html.replaceAll('<sup>?', '<sup><span class="oHL oHL-nihongo-question">?</span>');
// Precomposed units
matchDescriptions["oHL-unit-char"] = ["Precomposed unit", "Do not use precomposed unit symbols. ([[MOS:UNITSYMBOLS]])"];
html = html.replace(/([㎚㎛㎜㎝㎞㏌㎟㎠㎡㎢㎣㎤㎥㎦㎕㎖㎗㎘㏄㎰㎱㎲㎳㎍㎎㎏㎅㎆㎇㎐㎑㎒㎓㎔㎴㎵㎶㎷㎸㎹㎺㎻㎼㎽㎾㎿㏀㏁㎀㎁㎂㎃㎄㎧㎨㎭㎮㎯㎩㎪㎫㎬㎈㎉㍷㍸㍹㎙㍱㍲㍳㍴㍵㍶㍺㎊㎋㎌㏃㏅㏆㏇㏈㏉㏊㏋㏍㏎㏏㏐㏑㏒㏓㏔㏕㏖㏗㏚㏛㏜㏝㏞㏟㏿㏂㏘㏙])/g, '<span class="oHL oHL-unit-char">$1</span>');
// Unspaced unit
matchDescriptions["oHL-unit-space"] = ["Unspaced unit", "Unit symbols should usually be preceded by a non-breaking (<code>&nbsp;</code>) 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<span class="oHL oHL-unit-space oHL_added">[ ]</span>$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<span class="oHL oHL-unit-hyphen">-</span>$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<span class="oHL oHL-dotted-unit">.</span>');
// Brackets
matchDescriptions["oHL-bracket"] = ["Unparsed brackets", "Unparsed brackets likely indicate a broken template or wikilink."];
html = html.replace(/(\{\{|\[\[|\]\]|\}\}|\{\||\|\})/g, '<span class="oHL oHL-bracket">$1</span>');
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, '<span class="oHL oHL-bad-tag">$1</span>');
// 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, '<span class="oHL oHL-broken-header">=</span>$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<span class="oHL oHL-unspaced-comma">,</span>$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<span class="oHL oHL-money-comma">$2</span>');
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('€€', '<span class="oHL oHL-digit-comma oHL_added">[,]</span>');
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<span class="oHL oHL-currency-postfix">$3</span>');
// 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<span class="oHL oHL-excess-precision">$2</span>$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, '<span class="oHL oHL-doublepunc">$1</span>');
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, '</sup><span class="oHL oHL-cite-punc">$1</span>');
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<span class="oHL oHL-extra-punc">.</span>');
html = html.replace(/(\.["'’”])([,.])/g, '$1<span class="oHL oHL-extra-punc">$2</span>');
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<span class="oHL oHL-extra-period">.</span>');
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, '<span class="oHL oHL-curly-quote">$1</span>');
// 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<span class="oHL oHL-datecomma oHL_added">[,]</span>$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|<sup)/g, '$1<span class="oHL oHL-datecomma oHL_added">[,]</span>$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|<sup)/g, '$1<span class="oHL oHL-yearcomma oHL_added">[,]</span>$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<span class="oHL oHL-word-comma oHL_added">[,]</span> ');
html = html.replaceAll(/Originally (?!known)/g, 'Originally<span class="oHL oHL-word-comma oHL_added">[,]</span> ');
// html = html.replace(/([a-z]) (but|whereas) /g, '$1<span class="oHL oHL oHL-word-comma oHL_added">[,]</span> $2 ');
filterList.push(".oHL_reflist .oHL-word-comma", ".oHL_wikilink .oHL-word-comma");
// Double conjunction
// html = html.replace(/ and ([^"'.,;:!()[\]—–\n]+) and/g, ' and $1 <span class="oHL oHL-double-conjunc">and</span>');
// html = html.replace(/ or ([^"'.,;:!()[\]—–\n]+) or/g, ' or $1 <span class="oHL oHL-double-conjunc">or</span>');
// 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, '<span class="oHL oHL-month-abbr">$1</span> $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, '<span class="oHL oHL-day-abbr">$1</span>$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, '<span class="oHL-opt oHL-allcaps">$1</span>');
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('<td>-', '<td><span class="oHL oHL-table-hyphen">-</span>');
// 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(/<td>\s*<\/td>/g, '<td><span class="oHL oHL-missing-placeholder oHL_added">[—]</span></td>');
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.', ' <span class="oHL oHL-circa">ca.</span>');
html = html.replaceAll(' ca ', ' <span class="oHL oHL-circa">ca</span> ');
html = html.replace(/([ \(])c\.(\w)/g, '$1<span class="oHL oHL-circa">c.</span>$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<span class="oHL oHL-abbr-period oHL_added">[.]</span>$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<span class="oHL oHL-slash-space">/ </span>');
html = html.replace(/ \/([^\/ ])/g, '<span class="oHL oHL-slash-space"> /</span>$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, '<span class="oHL oHL-fullwidth">$1</span>');
filterList.push("[lang='ja'] .oHL-fullwidth",
".oHL_reflist .oHL-fullwidth");
// Time
matchDescriptions["oHL-time-space"] = ["Time space", "Times should have a non-breaking space (<code>&nbsp;</code>) before a.m./p.m. ([[MOS:AMPM]])"];
html = html.replace(/([0-9])([ap]\.?m)/gi, '$1<span class="oHL oHL-time-space oHL_added">[ ]</span>$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 <span class="oHL oHL-time-uppercase">$2</span>');
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<span class="oHL oHL-time-dot oHL_added">[.]</span>$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("(, (?:<a[^>\n]*?>))(" + stateNamesRe + ")<", "g"); // wikilinked
const countryRe = new RegExp(", (" + countryNamesRe + ")([ <])", "g");
const countryRe2 = new RegExp("(, (?:<a[^>\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('€€</a>', '</a>€€'); // neater
html = html.replaceAll('€€<', '<'); // clean
html = html.replaceAll('€€\n<', '\n<'); // clean
html = html.replaceAll('€€ <a id="sectiontitlecopy', ' <a id="sectiontitlecopy'); // clean
html = html.replaceAll('€€ –', ' –'); // clean
html = html.replace(/€€([,.:;)\]])/g, '$1'); // clean
html = html.replaceAll('€€', '<span class="oHL oHL-place-comma oHL_added">[,]</span>');
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<span class="oHL oHL-title-italics">$2</span>');
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, ' <span class="oHL oHL-court-italics">$1</span> ');
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") || 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, '<span class="oHL oHL-fullname">' + name + '</span>');
}
}
// 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 <code><ref></code> tags."];
html = html.replace(/(\[\d{1,2}\])/g, '<span class="oHL oHL-pseudo-ref">$1</span>');
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. (<code>°</code>; [[MOS:NUM#Specific_units]])"];
html = html.replaceAll('˚', '<span class="oHL oHL-bad-degree">˚</span>');
matchDescriptions["oHL-missing-degree"] = ["Missing degrees symbol", "Temperatures should have the degrees symbol. (<code>°</code>; [[MOS:NUM#Specific_units]])"];
html = html.replace(/([0-9]) (C|F)([ .,;:)])/g, '$1 <span class="oHL oHL-missing-degree oHL_added">[°]</span>$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<span class="oHL oHL-unspaced-degree oHL_added">[ ]</span>°$2');
// Misc
matchDescriptions["oHL-corporate"] = ["Corporate symbol", "Avoid using symbols like <code>™</code>, etc. ([[MOS:TMRULES]])"];
html = html.replace(/([™©®]|\(TM\)|\(C\)|\(R\))/ig, '<span class="oHL oHL-corporate">$1</span>');
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, '<span class="oHL oHL-prime">$1</span>');
*/
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<span class="oHL oHL-unit-symbol">$2</span>');
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('•', '<span class="oHL oHL-bad-bullet">•</span>');
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(' & ', ' <span class="oHL oHL-spaced-amper">&</span> ');
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” (<code>l</code>) seems to have been typed instead of “eye” (<code>I</code>)."];
html = html.replaceAll(' l ', '<span class="oHL oHL-spaced-el"> l </span>');
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.<span class="oHL oHL-unspaced-pgnum oHL_added">[ ]</span>$1');
html = html.replace(/pp\.([0-9])/g, 'pp.<span class="oHL oHL-unspaced-pgnum oHL_added">[ ]</span>$1');
matchDescriptions["oHL-unspaced-ordinal"] = ["Unspaced ordinal", "Ordinal abbreviations should be followed by a space."];
html = html.replace(/([Nn]o\.)([0-9])/g, '$1<span class="oHL oHL-unspaced-ordinal oHL_added">[ ]</span>$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<span class="oHL oHL-ascii-symbol">-></span>');
html = html.replace(/<-( |-)/g, '<span class="oHL oHL-ascii-symbol"><-</span>$1');
html = html.replace(/ (>|<|~)=/g, ' <span class="oHL oHL-ascii-symbol">$1=</span>');
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 <span class="foo"> 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", "<span class='oHL-opt oHL-contraction'>n't</span>");
html = html.replaceAll("'ve", "<span class='oHL-opt oHL-contraction'>'ve</span>");
html = html.replace(/(\w)'d/g, '$1<span class="oHL-opt oHL-contraction">\'d</span>');
html = html.replaceAll("'ll", "<span class='oHL-opt oHL-contraction'>'ll</span>");
html = html.replaceAll("they're", "<span class='oHL-opt oHL-contraction'>they're</span>");
html = html.replaceAll("might've", "<span class='oHL-opt oHL-contraction'>might've</span>");
html = html.replaceAll("that's", "<span class='oHL-opt oHL-contraction'>that's</span>");
html = html.replaceAll(/ (t?here's)/g, " <span class='oHL-opt oHL-contraction'>$1</span>");
html = html.replace(/ (s?he's)/g, " <span class='oHL-opt oHL-contraction'>$1</span>");
html = html.replaceAll(" it's", " <span class='oHL-opt oHL-contraction'>it's</span>");
html = html.replaceAll("who's", "<span class='oHL-opt oHL-contraction'>who's</span>");
html = html.replaceAll("something's", "<span class='oHL-opt oHL-contraction'>something's</span>");
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', '<span class="oHL oHL-and-or">$&</span>');
// 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('×', '<ruby class="oHL_ruby"><rb>×</rb><rt>mult</rt></ruby>');
html = html.replaceAll(' ', '<ruby class="oHL_ruby"><rb> </rb><rt>_nbsp_</rt></ruby>');
html = html.replaceAll(' ', '<ruby class="oHL_ruby"><rb> </rb><rt>_thinsp_</rt></ruby>');
html = html.replaceAll(' ', '<ruby class="oHL_ruby"><rb> </rb><rt>_hairsp_</rt></ruby>');
html = html.replaceAll('​', '<ruby class="oHL_ruby"><rb>​</rb><rt>_ZeroWidthSpace_</rt></ruby>');
html = html.replaceAll('­', '<ruby class="oHL_ruby"><rb>­</rb><rt>_shy_</rt></ruby>');
html = html.replaceAll('–', '<ruby class="oHL_ruby"><rb>–</rb><rt>en</rt></ruby>');
html = html.replaceAll('—', '<ruby class="oHL_ruby"><rb>—</rb><rt>em</rt></ruby>');
html = html.replaceAll('−', '<ruby class="oHL_ruby"><rb>−</rb><rt>minus</rt></ruby>');
html = html.replaceAll('‐', '<ruby class="oHL_ruby"><rb>‐</rb><rt>hyphen</rt></ruby>');
html = html.replaceAll('\u2011', '<ruby class="oHL_ruby"><rb>‑</rb><rt><abbr title="non-breaking">nb<abbr> hyphen</rt></ruby>');
html = html.replaceAll('ʼ', '<ruby class="oHL_ruby"><rb>ʼ</rb><rt>glottal</rt></ruby>');
// Trailing spaces
html = html.replace(/ \n/g, '<span class="oHL_trailingSpace" title="Whitespace">_</span>\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(/<span[^>]+<span.{5,60}/);
if (brokenHTML != null && brokenHTML.length != 0) {
const brokenHTMLMessage = mw.html.escape(brokenHTML.toString());
printExternalWarning(`Might have broken the page HTML: <code>${brokenHTMLMessage}</code>`);
}
}
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", "<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", '<ref name=[^>]+></ref>');
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", "<span id=[^>]+>");
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", "<!--[\\s\\S]*?-->");
searchDescriptions["oHL_math"] = ["Math", ""];
searches.set("oHL_math", "<math>");
searchDescriptions["oHL_tag"] = ["HTML tags", ""];
searches.set("oHL_tag", "</(i|b|em|strong|ul|ol|dl|table|tr|td|abbr|col|video|"
+ "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", '<ref name=":[0-9]+"');
searchDescriptions["oHL_wikt_link"] = ["Wiktionary links", ""];
searches.set("oHL_wikt_link", "\\[\\[(wikt|wiktionary):[^\\]\\n]+\\]\\]");
searchDescriptions["oHL_commaless"] = ["Comma-less numbers", ""];
searches.set("oHL_commaless", "(?<!((January|February|March|April|May|" // not preceded by a month
+ "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}(?<!{{[^}\\n]*)(?<=\\w+ \\w+)[.,;:][\"']");
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", "(?<![-/]|{{[^}]*|File:[^|\\n]*|Category:.*|<ref name=[^>]*)"
+ "[A-Z][a-z]+-[A-Z][a-z]+"
+ "(?![-/])");
searchDescriptions["oHL_adj_num"] = ["Adjacent numbers", ""];
searches.set("oHL_adj_num", "(?<!File:[^|\\n]*)" // not preceded by an image
+ "(?<= )[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", "(?<!<ref[^<]*|{{cite[^{]*)" // not in a DOI
+ "([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("<div><label><input type='checkbox' id='oHL_commalessFilter'> Hide Years</label></div>");
$("#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 <tags>
wikitext = wikitext.replaceAll('>"<', '>₭<'); // guard single highlighted quotes
wikitext = wikitext.replace(/([ >\()])"/g, '$1𐑱'); // 𐑱: left quote placeholder
wikitext = wikitext.replace(/"([ .,;:\n\)]|<ref)/g, '𐑲$1'); // 𐑲: right quote placeholder
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://wiki.riteme.site/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("<details id='oHL_defaultSort'><summary>DefaultSort mismatch"
+ " <span class='oHL_summaryCount'>(1)</span></summary>"
+ "<ul><li>" + mismatchText + "</li></ul></details>");
}
}
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 = "<ul>";
for (const [quote, wordCount] of longQuotes) {
list += "<li>" + quote.substring(1) + " [~" + wordCount + " words]</li>";
}
list += "</ul>";
searchDescriptions["oHL_longQuotes"] = ["Long quotes", ""];
$("#oHL_results").append("<details id='oHL_longQuotes'><summary>Long quotes <span class='oHL_summaryCount'>("
+ longQuotes.length + ")</span></summary>" + list + "</details>");
}
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 = /<ref[^>]*>\s*{{(cite |citation)[^<]+<\/ref>/gi;
const templateRefs = wikitext.match(templateRefRe) || [];
const templateRefcount = templateRefs.length;
const plainRefRe = /<ref[^>]*>[^<]+<\/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 = "<table class='wikitable oHL_refTable'><tr><th>#</th><th>Template</th>"
+ "<th>Author</th><th>Date</th><th>Access</th><th>Title</th>"
+ "<th>Work</th><th>Publisher</th><th><abbr title='Language'>Lang</abbr></th>"
+ "<th><abbr title='HTTP protocol'>Proto</abbr></th></tr>";
let tableContent = "";
for (const ref of templateRefs) {
const template = ref.match(/{cite ([^|}]+)/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 = `<a href="${url}" title="${refEscaped}">${title}</a>`; }
if (title != "—") { title = `<span title="${refEscaped}">${title}</span>`; }
let protocol = "—";
if (url != "—") { protocol = url.startsWith("https:") ? "<span title='Secure'>🔐</span>" : "<span title='Insecure'>🔓</span>"; }
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 += `<tr><td>${ordinal}</td><td>${template}</td><td>${author}</td><td>${date}</td>
<td>${accessdate}</td><td>${title}</td><td>${work}</td>
<td>${publisher}</td><td>${language}</td><td>${protocol}</td></tr>`;
}
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 = `<a href="${url}" title="${refEscaped}">${title}</a>`;
} else {
title = `<span title="${refEscaped}">${title}</span>`;
}
const ordinal = getRefOrdinalFromURL(url, ref, "N/A");
let protocol = "—";
if (url != "—") { protocol = url.startsWith("https:") ? "🔐" : "🔓"; }
tableContent += `<tr><td>${ordinal}</td><td>—</td><td>—</td><td>—</td>
<td>—</td><td>${title}</td><td>—</td>
<td>—</td><td>—</td><td>${protocol}</td></tr>`;
}
const table = tableHeader + tableContent + "</table>";
let countString = foundCount;
const totalCount = $(".reference-text").last().closest("li").index() + 1;
if (foundCount != totalCount) { countString += "/" + totalCount; }
searchDescriptions["oHL_refTable"] = ["References", ""];
$("#oHL_results").append("<details id='oHL_refTable'><summary>References <span class='oHL_summaryCount'>("
+ countString + ")</span></summary>" + table + "</details>");
catchFinalOrdinal(foundCount, totalCount);
matchDescriptions["oHL-redundant-title"] = ["Redundant reference parameter", "Citations should not repeat the <code>work</code> parameter."];
for (const redundantRef of redundantRefs) {
$(".oHL_reflist a[href*='" + redundantRef + "']").closest("cite").after(" <span class='oHL oHL-redundant-title oHL_added'>[Redundant parameter]</span>");
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(" <span class='oHL oHL-missing-work oHL_added'>[Missing work]</span>");
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(" <span class='oHL oHL-missing-accessdate oHL_added'>[Missing access date (<a target='_blank' class='external' href='"
+ blameURL + "'>Blame</a>)]</span>");
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(" <span class='oHL oHL-insecure-ref oHL_added'>[http]</span>");
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 = `<a href='#${refId}'>${ordinalNumber}</a>`;
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 = `<a href="#${ordinalId}">${counter}</a>`;
$(blankOrdinal).html(markup);
break;
}
}
}
// Reference: https://wiki.riteme.site/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 = "<ul>";
for (const [link, text] of Object.entries(linkText)) {
list += "<li><a href='" + link + "' class='new'>" + text + "</a></li>";
}
list += "</ul>";
searchDescriptions["oHL_redlinks"] = ["Redlinks", ""];
$("#oHL_results").append("<details id='oHL_redlinks'><summary>Redlinks <span class='oHL_summaryCount'>("
+ linkTextSize + ")</span></summary>" + list + "</details>");
}
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(/(' '|(?<![A-Za-z])'|'(?![A-Za-z]))/g, " ")
.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("<p><span class='oHL_plotLength oHL_added'>[Word count: "
+ wordCount.toLocaleString() + "]</span></p>");
}
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 = "<ul>";
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, "<span class='oHL_wikitext-match-text'>"
+ token + "</span>");
}
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 += "<li class='" + classList.join(" ")
+ "'><span class='oHL_uncommonWord'>" + token + "</span>"
+ context + "<span class='oHL_uncommonSymbols'></span></li>";
}
listMarkup += "</ul>";
searchDescriptions["oHL_uncommon"] = ["Uncommon words", ""];
$("#oHL_results").append("<details id='oHL_uncommon'><summary>Uncommon words <span class='oHL_summaryCount'>("
+ singleCountsUncommon.length + ")</span></summary>" + listMarkup + "</details>");
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 = "<span title='" + f.label + "'>" + f.symbol + "</span>";
$("." + 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("<fieldset id='oHL_uncommonFilters'><legend>Hide:</legend></fieldset>");
for (const filter of filters) {
$("#oHL_uncommonFilters").append(` <span id='${filter.id}'><label><input type='checkbox'> <span class='oHL_filterLabel'>${filter.label}</span></label></span>`);
}
$("#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("<details id='oHL_wikilinkCandidates' style='display: none;'>"
+ "<summary>Wikilink candidates <span class='oHL_summaryCount'></span>"
+ "</summary><ul id='oHL_wikilinkCandidates_items'></ul></details>");
// 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("<li><a href='" + link +"'>" + page + "</a>" + shortDescriptionText + "</li>");
$("#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, "<span class='oHL_wikitext-match-text'>"
+ bracketLeft + "</span>");
if (bracketRight != bracketLeft) {
lineFormatted = lineFormatted.replaceAll(bracketRight, "<span class='oHL_wikitext-match-text'>"
+ bracketRight + "</span>");
}
unbalanced.push(lineFormatted);
}
}
}
if (unbalanced.length == 0) { return; }
let list = "<ul>";
for (const entry of unbalanced) {
list += "<li>" + entry + "</li>";
}
list += "</ul>";
$("#oHL_results").append("<details id='oHL_unbalanced'><summary>Unbalanced quotes and brackets <span class='oHL_summaryCount'>("
+ unbalanced.length + ")</span></summary>" + list + "</details>");
}
function showRedirects() {
// Placeholder
searchDescriptions["oHL_redirects"] = ["Incoming redirects", ""];
$("#oHL_results").append("<details id='oHL_redirects'><summary>Incoming redirects <span class='oHL_summaryCount'>(0)</span></summary></details>");
// 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 += "<li><a href='/w/index.php?title="
+ encodeURIComponent(r.title).replaceAll("'", "%27")
+ "&redirect=no'>" + r.title + "</a>";
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 += " → <a";
} else {
redirectText += " → ❌ <a class='new'";
}
redirectText += " href='#" + fragment + "'>§" + fragment + "</a>";
}
redirectText += "</li>";
});
}
// TODO: just update the children instead of the whole element
searchDescriptions["oHL_incoming_redirects"] = ["Incoming redirects", ""];
$("#oHL_redirects").html("<summary>Incoming redirects <span class='oHL_summaryCount'>("
+ redirectCount+")</span></summary><ul>"
+ redirectText + "</ul>");
if (redirectCandidates.length > 0) {
let redirectCandidatesText = "";
for (const candidate of redirectCandidates) {
redirectCandidatesText += "<li><a href='/wiki/" + encodeURIComponent(candidate).replaceAll("'", "%27")
+ "'>" + candidate + "</a></li>";
}
searchDescriptions["oHL_redirect_candidates"] = ["New redirect candidates", ""];
$("#oHL_redirects").after("<details id='oHL_redirect_candidates'><summary>New redirect candidates <span class='oHL_summaryCount'>("
+ redirectCandidates.length +")</span></summary><ul>"
+ redirectCandidatesText + "</ul></details>");
// 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 = "<a href='/wiki/" + encodeURIComponent(pageTitleWithoutParens).replaceAll("'", "%27")
+ "'>" + pageTitleWithoutParens + "</a> or <a href='/wiki/"
+ encodeURIComponent(pageTitleDab).replaceAll("'", "%27")
+ "'>" + pageTitleDab + "</a>";
searchDescriptions["oHL_noDabLink"] = ["Incoming disambiguation link missing", ""];
$("#oHL_results").append("<details id='oHL_noDabLink'><summary>Incoming disambiguation link missing <span class='oHL_summaryCount'>(1)</span></summary>"
+ "<ul><li>Either " + dabMarkup + " need to link to this page.</i></ul></details>");
}
function showWikitextMatches(results) {
if (results.size === 0) {
return;
}
let resultsHTML = "";
for (const [id, matches] of results) {
const name = searchDescriptions[id][0];
resultsHTML += "<details id='" + id + "'><summary>" + name
+ " <span class='oHL_summaryCount'>(" + matches.length
+ ")</span></summary><ul>";
matches.forEach(m => resultsHTML += "<li class='oHL_wikitext-match'>"
+ mw.html.escape(m[0])
+ "<span class='oHL_wikitext-match-text'>"
+ mw.html.escape(m[1]) + "</span>"
+ mw.html.escape(m[2]) + "</li>");
resultsHTML += "</ul></details>";
}
$("#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("<details id='oHL_italicization' style='display: none;'><summary>Italicization <span class='oHL_summaryCount'></span></summary><ul id='oHL_italicization_items'></ul></details>");
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("<i>")) {
displayItalicization = "[None]";
}
// Skip Foo (<i>Bar</i>)
if (/\(<i>/.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("<i></i>"); }
const originalDisplayMarkup = $(originalDisplay).html();
if (originalDisplayMarkup == displayItalicization) { return; }
$("#oHL_italicization_items").append("<li>" + originalDisplayMarkup
+ " <a href='#"+ element.id + "'>→</a> "
+ displayItalicization + "</li>");
$("#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("<span class='oHL_clear'>[clear]</span>");
// Anchors
$(".anchor").each(function showAnchors() {
const id = $(this).attr("id");
const anchorMarkup = "<a class='oHL_shownAnchor' title='Anchor' href='#"
+ id + "'>#" + id + "</a>";
// 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("<span class='oHL_added'>[Short description]: </span>");
});
// 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("<p id='oHL_wd_img'>[Wikidata image]</p>");
// 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("//wiki.riteme.site/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("<div id='oHL_info' style='display: none'><div><span id='oHL_info_counter'>0</span>"
+ "⁄<span id='oHL_info_total'>?</span> <span id='oHL_info_arrows'>"
+ "<span id='oHL_info_left_arrow' title='Previous highlight [shift-n]' class='oHL_arrow_disabled'>←</span> "
+ "<span id='oHL_info_right_arrow' title='Next highlight [n]'>→</span>"
+ "</span></div><div id='oHL_info_class'></div></div>");
$("#mw-content-text").before("<div id='oHL_matches'></div>");
$("#mw-content-text").before("<div id='oHL_results'></div><hr/>");
$("#oHL_results").append("<span id='oHL_hover'></span>");
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 + "<br>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("<div><button id='oHL_configureButton' class='cdx-button'><span class='cdx-icon'><img src='"
+ gearIconURL + "'></span> Configure</button></div>");
// 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, "<a href='/wiki/$1' target='_blank' class='oHL_autoWikilink'>$1</a>");
// Remove underscores in section links
const underscoreRe = />([^<]*?)_([^<]*?)</;
while (underscoreRe.test(desc)) {
desc = desc.replace(underscoreRe, ">$1 $2<");
}
desc = desc.replace(/>([^<]*?)#([^<]*?)</g, ">$1 <span class='oHL_autoSectionLink'>§ $2</span><"); // section links
desc = desc.replace(/{{([^{]+)}}/g, "<code><a href='/wiki/Template:$1' target='_blank' class='oHL_autoTemplatelink'>{{$1}}</a></code>");
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 = "<table class='wikitable'>";
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 += "<tr><td><input oHLclass='" + hlClass
+ "' type='checkbox' checked></td><td>"
+ hlText + "</td><td>" + hlDesc + "</td><td class='oHL_tableCount'>"
+ count + "</td></tr>";
countsTotal += count;
}
tableHTML += "</table>";
$("#oHL_results").prepend("<details id='oHL_summary'><summary>Highlights"
+ " <span class='oHL_summaryCount'>(" + totalMatches
+ ")</span></summary>" + tableHTML+ "</details>");
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("<thead><tr><th><input title='(De)select all' id='oHL_checkAll' type='checkbox' checked></th>"
+ " <th>Name</th><th>Description</th><th>Count</th></tr></thead>");
$("#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("<div id='oHL_info_class_name'><input oHLclass='" + hlClass
+ "' type='checkbox' checked> " + hlText
+ "</div><div id='oHL_info_class_desc'>"
+ hlDescription + "</div>");
}
const hlTextsCombined = hlTexts.join("<br>");
$("#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 = "<span class='oHL_counter_padding'>0</span>";
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 = "<span class='oHL oHL-dupe-alt'>" + imgAlt + "</span>";
}
let displayMessage = "<span class='oHL_img_info_dimensions' title='Image resolution'><span class='oHL_img_info_dimensions_display'>"
+ parseInt(displayWidth).toLocaleString() + "×" + parseInt(displayHeight).toLocaleString()
+ "</span> (<span class='oHL_img_info_dimensions_original'>"
+ parseInt(originalWidth).toLocaleString() + "×" + parseInt(originalheight).toLocaleString()
+ "</span>)</span>";
if (imgAlt) {
displayMessage += "<br>[Alt: `" + imgAlt + "`]<hr>";
}
$(this).parent().after("<div class='oHL_img_info'>" + displayMessage + "</div>");
matchDescriptions["oHL-missing-upright"] = ["Image missing upright", "Tall images should use the <code>|upright|</code> 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("<p><span class='oHL oHL-nonfinal-ext oHL_added'>[Move section last↓]</span></p>");
}
// "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("<p><span class='oHL oHL-misplaced-seeAlso oHL_added'>[Move section up↑]</span></p>");
return;
}
const endSections = ["References", "Sources", "Notes", "Explanatory_notes",
"Footnotes", "Bibliography", "Notes_and_references",
"Citations"];
for (let i=0; i<len-1; i++) {
if (sections[i] === "See_also") {
if (!endSections.includes(sections[i+1])) {
$("#See_also").parent().after("<p><span class='oHL oHL-misplaced-seeAlso oHL_added'>[Move section↕]</span></p>");
}
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<len-1; i++) {
if (sections[i] === "Further_reading") {
if (!endSections.includes(sections[i-1])) {
$("#Further_reading").parent().after("<p><span class='oHL oHL-misplaced-furtherReading oHL_added'>[Move section↕]</span></p>");
}
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("<p><span class='oHL-opt oHL-category-italics oHL_added'>[Italicize title] (" + w + " category)</span></p>");
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(" <a class='oHL oHL-color-contrast oHL_added external' oHL_contrast='"
+ contrast + "' href='https://webaim.org/resources/contrastchecker/?fcolor="
+ textColorHex + "&bcolor=" + bgColorHex + "'>[AIM]</a>");
}
}
// 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);
}
// </nowiki>