Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
/*
	A script that makes it easier to create scripts
	that perform repetitive and tedious editing tasks.
	
	It inserts buttons above the text box, if certain conditions are fulfilled.
	Pressing the button executes the function.
	
	A variable tracks how many times a change is made,
	then the function adds an edit summary if the change was done;
	if not, it keeps the original edit summary.
*/

/* globals $, CleanupButtons, mw */
// <nowiki>
"use strict";

var mwValues        = mw.config.values;
var action			= mwValues.wgAction;
var namespaceNumber	= mwValues.wgNamespaceNumber;
var contentModel 	= mwValues.wgPageContentModel;
var pageName        = mwValues.wgPageName;

// mw.loader.load('//en.wikipedia.org/w/index.php?title=User:Joeytje50/JWB.js/load.js&action=raw&ctype=text/javascript');

// Notify about footnote errors (such as undefined named footnotes).
var citeErrors = document.getElementsByClassName("mw-ext-cite-error");
if (citeErrors.length > 0)
	mw.notify(Array.from(citeErrors).map(element => element.innerText).join("\n"));

if ( action === "edit" && contentModel === "wikitext"
	// not edit conflict
	&& document.getElementsByClassName("mw-twocolconflict-page").length === 0 )
{
	var wikitext = $("#wpTextbox1").val();

	var startsAndEndsWithWhitespace = /^\s+.+?\s+$/;
	
	// Notify about template errors (such as more than one value for a parameter).
	var previewNote = document.getElementsByClassName("previewnote");
	if (previewNote.length > 0) {
		var notes = previewNote[0].children;
		for (const note of notes) {
			const innerText = note.innerText;
			if (innerText.includes("Template:"))
				mw.notify(innerText);
		}
	}
	
	var match;
	var regex = /\{\{lang\|([^|]+)\|([^}]+)\}\}/g;
	var langs = [];
	var nocat_langs = [];
	while ((match = regex.exec(wikitext)) !== null) {
		if (match[2] && match[2].includes("nocat")) {
			if (!nocat_langs.includes(match[1]))
				nocat_langs.push(match[1]);
		}
		else if (!langs.includes(match[1])) {
			langs.push(match[1]);
		}
	}
	
	var failed_cat = [];
	for (var lang of nocat_langs) {
		if (!langs.includes(lang))
			failed_cat.push(lang);
	}
	
	if (failed_cat.length > 0)
		mw.notify(`Categorization might be failing for the following languages: ${failed_cat.join(", ")}.`,
			{ autoHide: false });
	
	var notifyReplacements = function(count)
	{
		if ( typeof count === "number" )
				mw.notify(`${ count === 0 && "No" || count } replacement${ count !== 1 && "s" || "" } made.`);
		else
			console.log("The function notifyReplacements failed because its argument is not a number.");
	};
	
	var addSummary = function(count, summary)
	{
		if ( typeof count !== "number" )
			console.log("Error: count argument to addSummary is not a number.");
		
		if ( count > 0 )
		{
			$("#wpSummary").val(
				function(index, content)
				{
					var afterSectionName = content.match(/^(?:\/\*[^\*]+\*\/)?\s*(.*?)$/);
					
					var scriptMention = ", with the help of [[User:Erutuon/scripts/cleanup.js|JavaScript]]";
					
					var addition;
					
					if ( afterSectionName && afterSectionName[1].length > 0 )
					{
						if ( content.includes(scriptMention) )
						{
							content = content.replace(scriptMention, "");
							addition = " and " + summary;
						}
						else
							addition = "; " + summary;
					}
					else
						addition = summary;
					
					if ( ( !afterSectionName || !content.includes(summary) ) )
						content += addition;
					
					if ( content.includes(summary) && !content.includes(scriptMention) )
						content += scriptMention;
					
					return content;
				}
			);
		}
	};
	
	var cleanupFunctions = [
		// Template:
		{
			condition: false,
			textBoxIncludes: "",
			button: {
				text: "blah",
			},
			minorEdit: false,
			func:
				function(content)
				{
					return content;
				}
		},
		{
			textBoxIncludes: /^#/m,
			button: {
				text: "ordered to unordered",
			},
			minorEdit: true,
			func:
				function(content)
				{
					var origContent = content;
					
					content = content.replace(/^#/gm, '*');
					
					if (content !== origContent)
						addSummary(1, "ordered list not appropriate here ([[MOS:LIST#Use an unordered list by default]])");
					
					return content;
				}
		},
		{
			textBoxIncludes: /^[\*#;:]+(?=[^\s\*#;:])/m,
			button: {
				text: "spaces after list syntax",
			},
			func:
				function(content)
				{
					content = content.replace(/^([\*#;:]+)\s*(?=[^\s\*#;:])/gm,
						'$1 ');
					
					return content;
				}
		},
		{
			textBoxIncludes: /binomial|sectio/,
			condition: function (wikitext) {
				return /{{[Ss]peciesbox/.test(wikitext)
					&& (wikitext.includes("binomial") || wikitext.includes("sectio"));
			},
			button: {
				text: "fix {{speciesbox}}",
			},
			minorEdit: true,
			func:
				function(content)
				{
					var taxon;
					var pageNameWithSpaces = pageName.replace(/_/g, " ");
					
					var origContent = content;
					
					content = content.replace(
						/{{speciesbox(?:[^{}]+|{{[^}]+}})+}}/g,
						function (template) {
							return template
								.replace(/\|}}$/, "}}")
								.replace(
									/(\n*\|\s*)([\w_]+)(\s*=\s*)(.*?)(?=\s*[|}])/g,
									function (wholeMatch, before, paramName, between, paramValue) {
										// console.log([ wholeMatch, before, paramName, between, paramValue ].map(str => '"' + str + '"').join("\t"));
										switch (paramName) {
											case "binomial": case "taxon": // include taxon because of code below
												paramName = "taxon";
												paramValue = paramValue.replace(/''/g, "");
												taxon = paramValue; break;
											case "binomial_authority":
												paramName = "authority"; break;
											case "subgenus": case "sectio":
											case "subsectio": case "series":
												paramName = "parent"; break;
											case "name": {
												// Delete unnecessary |name= parameter.
												if (paramValue.replace(/''/g, "") === pageNameWithSpaces)
													return "";
												break;
											}
											default:
												return wholeMatch;
										}
										
										return before + paramName + between + paramValue;
									});
						});
					
					// mw.notify([ "taxon", taxon, "pageName", pageName.replace(/_/g, " ") ].join(', '));
					
					var summary = "fix parameters of [[Template:speciesbox]]";
					
					if (taxon === pageNameWithSpaces) {
						var oldContent = content;
						content = content.replace(/{{[Ii]talic ?title}}\n*/g, "");
						if (content !== oldContent)
							summary += " and remove unnecessary [[Template:italic title]]";
					}
					
					if (content === origContent) return;
					
					addSummary(1, summary);
					
					return content;
				}
		},
		{
			textBoxIncludes: /\{\{[Tt]axobox\s*[\||\}\}]/,
			button: {
				text: "{{taxobox}} to {{speciesbox}}",
			},
			func:
				function(content)
				{
					var disallowedParams = new Set(["classis", "divisio",
						"familia", "genus", "ordo", "regnum", "species",
						"subfamilia",  "tribus", "unranked_classis",
						"unranked_divisio", "unranked_ordo"]);
					
					content = content.replace(
						/\{\{[Tt]axobox(?:[^\{\}]+|\{\{[^}]+\}\})+\}\}/g,
						function (template) {
							var whitespace = /^\s+$/;
							var taxon;
							template = template.replace(
								/\|\s*([^=\|\}]+?)(\s*=\s*)((?:\[\[[^\]]+\]\]|\[[^\]]+\]|\{\{[^\}]+\}\}|[^\|\}\[\]]+)+)/g,
								function (wholeMatch, paramName, between, paramValue) {
									if (disallowedParams.has(paramName))
										return "";
									else {
										var ref = "";
										if (paramName === "name") {
											// Remove italics and compare with page name.
											if (paramValue.trim().replace(/''/g, "")
													=== pageName.replace(/_/g, " "))
												return "";
										}
										
										if (paramName === "binomial") {
											paramName = "taxon";
											paramValue = paramValue.replace(
												/''(.+?)''/g, '$1'); // Remove italics.
										} else if (paramName === "binomial_authority")
											paramName = "authority";
										else if (paramName === "subgenus" || paramName === "sectio"
												|| paramName === "subsectio" || paramName === "series")
											paramName = "parent";
										else if (paramName === "synonyms" && paramValue.includes('<br')) {
											var match = paramValue.match(/^(.+?)\s*(<ref.+)$/);
											if (match) {
												ref = match[1], paramValue = match[2];
												
												// Remove any text from the list of refs.
												var refRegex = /<ref(?: [^>]+)?>.+?<\/ref>/g;
												var refs = [];
												var refMatch;
												while ((refMatch = refRegex.exec(ref))) {
													refs.push(refMatch[0]);
												}
												ref = refs.join("");
											}
											paramValue = paramValue
												.split(/(?:,?\s*<br\s*\/?>\n*)+/)
												.map(item => whitespace.test(item) ? "" : "\n* " + item)
												.join("")
												.trim();
											
										}
										
										if (paramName === "taxon")
											taxon = paramValue;
										
										if (!startsAndEndsWithWhitespace.test(between))
											between = " = ";
										
										return "| " + paramName + between + paramValue + ref;
									}
								});
							
							template = template
								.replace(/[Tt]axobox/, "speciesbox")
								.replace(/\|\}\}$/, "}}");
							
							return template;
						});
					
					content = content.replace(/{{[Ii]talic ?title}}\n*/, "");
					
					addSummary(1, "switched to [[Template:speciesbox]]");
					
					return content;
				}
		},
		{
			textBoxIncludes: /<ref|[^\[]\[[^\[]+\](?!\])/i,
			button: {
				text: "specific-source templates",
			},
			func:
				function(content)
				{
					var count1 = 0, count2 = 0;
					var regexes = {};
					regexes.BONAP = /https?:\/\/bonap\.net\/(?:MapGallery|[Nn][Aa][Pp][Aa]\/TaxonMaps\/Genus)\/([cC]ounty|[sS]tate)\/(\w+)(?:%20(.*?)\.png)?/g;
					regexes.PLANTS = /https?:\/\/plants\.usda\.gov\/(?:core|java)\/profile\?symbol=([a-zA-Z]+\d*)/;
					regexes.eFloras = /https?:\/\/(?:www\.)?efloras\.org\/florataxon\.aspx\?flora_id=(\d+)&taxon_id=(\d+)/;
					regexes.wildflowerDotOrg = /https?:\/\/(?:www\.)?wildflower\.org\/plants\/result\.php\?id_plant=([a-zA-Z]+\d*)/;
					regexes.FEIS = /https?:\/\/(?:www\.)?fs\.fed\.us\/database\/feis\/[a-z]+\/([a-z]+)\/[a-z]+\/all\.html/;
					regexes.ThePlantList = /http:\/\/(?:www\.)?theplantlist\.org\/tpl1\.1\/record\/([a-z0-9-]+)/;
					regexes.MissouriPlants = /https?:\/\/(?:www\.)?missouriplants\.com\/(\w+?)(opp|alt)\/([A-Za-z]+)_([A-Za-z]+)_page\.html/;
					regexes.CalPhotos = /https?:\/\/(?:www\.)?calphotos\.berkeley\.edu\/cgi\/img_query\?(?:query_src=photos_index&)?where-taxon=([\w-]+)(?:\+|%20)([\w-]+)/;
					regexes.Tropicos = /http:\/\/(?:www\.)?tropicos\.org\/[Nn]ame\/(\d+)\/?(?:\?projectid=(\d+))?/;
					regexes.EOL = /https?:\/\/(?:www\.)?eol\.org\/pages\/(\d+)(?:\/overview)?/;
					regexes.ITIS = /https?:\/\/(?:www\.)?itis\.gov\/servlet\/SingleRpt\/SingleRpt\?search_topic=TSN&search_value=(\d+)/;
					regexes.GoBotany = /https?:\/\/(?:www\.)?gobotany\.newenglandwild\.org\/(?:genus|species)\/(\w+)(?:\/(\w+))?/;
					regexes.GoOrchids = /https?:\/\/(?:www\.)?goorchids\.northamericanorchidcenter\.org\/\w+\/(\w+)(?:\/(\w+))/;
					regexes.IllinoisWildflowers = /https?:\/\/(?:www\.)?illinoiswildflowers\.info\/([a-z\._\/]+)\.htm/;
					regexes.MinnesotaWildflowers = /https?:\/\/(?:www\.)?minnesotawildflowers\.info\/([a-z\.\/-]+)/;
					regexes.WCSP = /https?:\/\/(?:www\.)?apps\.kew\.org\/wcsp\/namedetail\.do\?name_id=(\d+)/;
					regexes.KansasWildflowers = /https?:\/\/(?:www\.)?kswildflower\.org\/([a-z]+)_details\.php\?[a-z]+ID=(\d+)/;
					// http://ucjeps.berkeley.edu/cgi-bin/get_IJM.pl?tid=80590
					// http://ucjeps.berkeley.edu/eflora/eflora_display.php?tid=80590
					// cgi-bin/get_IJM eflora/eflora
					regexes.Jepson = /https?:\/\/(?:www\.)?ucjeps\.berkeley\.edu\/(?:eflora\/eflora_display\.php|cgi-bin\/get_IJM\.pl)\?(tid|key)=(\d+)/;
					//http://ucjeps.berkeley.edu/cgi-bin/get_JM_treatment.pl?8738,8858,8875
					regexes.JepsonManual = /https?:\/\/(?:www\.)?ucjeps\.berkeley\.edu\/cgi-bin\/get_JM_treatment\.pl\?([\d,]+)/;
					// http://www.calflora.org/cgi-bin/species_query.cgi?where-taxon=Sambucus+racemosa+var.+melanocarpa
					// http://www.calflora.org/cgi-bin/species_query.cgi?where-calrecnum=8838
					regexes.Calflora = /https?:\/\/(?:www\.)?calflora\.org\/cgi-bin\/species_query\.cgi\?(?:where-taxon=([A-Za-z.+-]+)|where-calrecnum=(\d+))/;
					regexes.Gymnosperm = /https?:\/\/(?:www\.)?conifers\.org\/([a-z][a-z])\/([A-Z][a-z]+)(?:_([a-z]+)(?:_([a-z]+))?)?\.php/;
					regexes.IPNI = /https?:\/\/(?:www\.)?ipni\.org\/ipni\/idPlantNameSearch\.do\?id=([\d-]+)/;
					// https://michiganflora.net/species.aspx?id=2796
					// https://michiganflora.net/genus.aspx?id=Viola
					// https://michiganflora.net/family.aspx?id=Violaceae
					regexes.MichiganFlora = /https?:\/\/(?:www\.)?michiganflora\.net\/(family|genus|species)\.aspx\?id=(\w+)/;
					regexes.ConnecticutPlants = /https?:\/\/(?:www\.)?ct-botanical-society\.org\/Plants\/view\/(\d+)/;
					regexes.FloraOfWisconsin = /https?:\/\/(?:www\.)?wisflora\.herbarium\.wisc\.edu\/taxa\/index\.php\?taxon=(\d+)/;
					
					var paramValueRegex = /\s*=\s*([^|]+[^|\s])/;
					
					var pageNameWithSpaces = pageName.replace(/_/g, " ");
					
					content = content.replace(
						/(<ref(?:\s+name\s*=\s*[^\/<>]+)?>)([^\0]*?)(<\/ref>)/gi,
						function (wholeMatch, openRefTag, contents, closeRefTag) {
							if (!(contents.includes("http://") | contents.includes("https://"))) {
								return wholeMatch;
							}
							
							var match, taxon, accessDate;
							++count1;
							
							taxon = contents.match(/title\s*=\s*([^\|\}]*[^\s\|\}])/)
								|| contents.match(/''(.+?)''/); // only species and below
							taxon = taxon ? taxon[1] : pageNameWithSpaces;
							
							accessDate = contents.match(/(?:[Rr]etrieved|[Aa]ccessed|access-?date\s*=)\s*([A-Za-z\d-\/, ]+)/)
								|| contents.match(/([A-Z][a-z]+ \d\d?,? \d\d\d\d)/);
							var accessDateParam = accessDate ? ` |access-date=${accessDate[1]}` : '';
							
							if (contents.includes("bonap.net")) {
								match = regexes.BONAP.exec(contents);
								const year = contents.match(/201\d/);
								if (match && match[1]) {
									return `${openRefTag}{{BONAP |genus=${match[2]}`
										+ (match[3] ? ` |species=${match[3]}` : '')
										+ (match[1].toLowerCase() == "state" ? " |state=1" : "")
										+ (year ? ` |date=${year[0]}` : '')
										+ accessDateParam
										+ `}}${closeRefTag}`;
								}
							}
							else if (contents.includes("plants.usda.gov")) { // ever another capitalization?
								match = regexes.PLANTS.exec(contents);
								if (match) {
									return `${openRefTag}{{PLANTS |symbol=${match[1].toUpperCase()}`
										+ (taxon ? ` |taxon=${taxon}` : '')
										+ accessDateParam
										+ `}}${closeRefTag}`;
								}
							}
							else if (contents.includes("efloras.org")) {
								match = regexes.eFloras.exec(contents);
								if (match) {
									return `${openRefTag}{{eFloras|${match[1]}`
										+ `|${match[2]}`
										+ (taxon ? `|${taxon.replace(/'''?/g, "")}` : '')
										+ accessDateParam
										+ `}}${closeRefTag}`;
								}
							}
							else if (contents.includes("kswildflower.org")) {
								match = regexes.KansasWildflowers.exec(contents);
								if (match) {
									return `${openRefTag}{{Kansas Wildflowers|${match[2]}|${pageNameWithSpaces}`
										+ (match[1] === "flower" ? '' : match[1])
										+ `}}${closeRefTag}`;
								}
							}
							else if (contents.includes("wildflower.org")) {
								match = regexes.wildflowerDotOrg.exec(contents);
								if (match) {
									return `${openRefTag}{{NPIN|${match[1].toUpperCase()}`
										+ `|${taxon}`
										+ accessDateParam
										+ `}}${closeRefTag}`;
								}
							}
							else if (contents.includes("fs.fed.us/database/feis")) {
								match = regexes.FEIS.exec(contents);
								var genusSpecies = taxon.split(/\s+/);
								if (genusSpecies.length !== 2)
									genusSpecies = pageNameWithSpaces.split(' ');
								
								if (match && genusSpecies.length === 2) {
									var params = new Map();
									for (let i = 0, paramName1 = "first", paramName2 = "last";
											contents.includes(paramName1);
											++i, paramName1 = "first" + i, paramName2 = "last" + i) {
										const index1 = contents.indexOf(paramName1);
										if (index1 !== -1) {
											const value = contents.substring(index1 + paramName1.length).match(paramValueRegex);
											if (value)
												params.set(paramName1, value[1]);
										}
										const index2 = contents.indexOf(paramName2);
										if (index2 !== -1) {
											const value = contents.substring(index2 + paramName2.length).match(paramValueRegex);
											if (value)
												params.set(paramName2, value[1]);
										}
									}
									
									{
										const index = contents.indexOf("date");
										if (index !== -1) {
											const dateMatch = contents.substring(index + "date".length).match(paramValueRegex);
											if (dateMatch)
												params.set("date", dateMatch[1]);
										}
									}
									
									var printedParams = [];
									for (const [ key, value ] of params) {
										printedParams.push(` |${key}=${value}`);
									}
									
									return `${openRefTag}{{FEIS |genus=${genusSpecies[0]} |species=${genusSpecies[1]}`
										+ ` |type=${match[1]}`
										+ printedParams.join('')
										+ accessDateParam
										+ `}}${closeRefTag}`;
								}
							}
							else if (contents.includes("theplantlist.org")) {
								match = regexes.ThePlantList.exec(contents);
								if (match) {
									return `${openRefTag}{{ThePlantList |id=${match[1]}`
										+ ` |taxon=${taxon} |authority=`
										+ accessDateParam
										+ `}}${closeRefTag}`;
								}
							}
							else if (contents.includes("tropicos.org")) {
								match = regexes.Tropicos.exec(contents);
								if (match) {
									return `${openRefTag}{{Tropicos|${match[2] ? match[2] + "|" : ""}${match[1]}`
										+ `|${taxon}`
										+ accessDateParam
										+ `}}${closeRefTag}`;
								}
							}
							else if (contents.includes("eol.org")) {
								match = regexes.EOL.exec(contents);
								if (match) {
									return `${openRefTag}{{EOL|${match[1]}`
										+ `|${taxon}`
										+ accessDateParam
										+ `}}${closeRefTag}`;
								}
							}
							else if (contents.includes("itis.gov")) {
								match = regexes.ITIS.exec(contents);
								if (match) {
									return `${openRefTag}{{ITIS |id=${match[1]}`
										+ ` |taxon=${taxon}` + accessDateParam
										+ `}}${closeRefTag}`;
								}
							}
							else if (contents.includes("ipni.org")) {
								match = regexes.IPNI.exec(contents);
								if (match) {
									return `${openRefTag}{{IPNI |id=${match[1]}`
										+ ` |taxon=${taxon}`
										+ accessDateParam
										+ `}}${closeRefTag}`;
								}
							}
							else if (contents.includes("gobotany.newenglandwild.org")) {
								match = regexes.GoBotany.exec(contents);
								if (match) {
									match[1] = match[1].replace(/^./, (char) => char.toUpperCase());
									if (match[2]) {
										return `${openRefTag}{{Go Botany |genus=${match[1]}`
											+ ` |species=${match[2]}` + accessDateParam
											+ `}}${closeRefTag}`;
									}
									else {
										return `${openRefTag}{{Go Botany `
											+ `|genus=${match[1]}`
											+ accessDateParam + `}}${closeRefTag}`;
									}
								}
							}
							else if (contents.includes("goorchids.northamericanorchidcenter.org")) {
								match = regexes.GoOrchids.exec(contents);
								if (match) {
									match[1] = match[1].replace(/^./, (char) => char.toUpperCase()); // genus
									if (match[2]) {
										return `${openRefTag}{{Go Orchids |genus=${match[1]}`
											+ ` |species=${match[2]}` + accessDateParam
											+ `}}${closeRefTag}`;
									}
									else {
										return `${openRefTag}{{Go Orchids`
											+ ` |genus=${match[1]}`
											+ accessDateParam + `}}${closeRefTag}`;
									}
								}
							}
							else if (contents.includes("illinoiswildflowers.info")) {
								match = regexes.IllinoisWildflowers.exec(contents);
								if (match) {
									return `${openRefTag}{{Illinois Wildflowers|${match[1]}`
										+ `|${taxon}` + accessDateParam
										+ `}}${closeRefTag}`;
								}
							}
							else if (contents.includes("minnesotawildflowers.info")) {
								match = regexes.MinnesotaWildflowers.exec(contents);
								if (match) {
									return `${openRefTag}{{Minnesota Wildflowers|${match[1]}`
										+ `|${taxon}` + accessDateParam
										+ `}}${closeRefTag}`;
								}
							}
							else if (contents.includes("pfaf.org")) {
								// No regex needed.
								return `${openRefTag}{{PFAF` + accessDateParam
									+ `}}${closeRefTag}`;
							}
							else if (contents.includes("apps.kew.org/wcsp")) {
								match = regexes.WCSP.exec(contents);
								if (match) {
									return `${openRefTag}{{WCSP|${match[1]}`
										+ `|${taxon}` + accessDateParam
										+ `}}${closeRefTag}`;
								}
							}
							else if (contents.includes("ucjeps.berkeley.edu/cgi-bin/get_IJM")
								  || contents.includes("ucjeps.berkeley.edu/eflora/eflora")) {
								match = regexes.Jepson.exec(contents);
								if (match) {
									return `${openRefTag}{{Jepson eFlora|${match[2]}`
										+ `|${taxon}`
										+ (match[1] === "key" ? " |type=key" : "")
										+ accessDateParam + `}}${closeRefTag}`;
								}
							}
							else if (contents.includes("ucjeps.berkeley.edu/cgi-bin/get_JM_treatment")) {
								match = regexes.JepsonManual.exec(contents);
								if (match) {
									return `${openRefTag}{{Jepson Manual |id=${match[1]}`
										+ ` |taxon=${taxon}` + accessDateParam
										+ `}}${closeRefTag}`;
								}
							}
							else if (contents.includes("calflora.org")) {
								match = regexes.Calflora.exec(contents);
								if (match) {
									return `${openRefTag}{{Calflora${match[1] ? "|" + match[1].replace(/\+/g, ' ') : " |id=" + match[2]}`
										+ `${accessDateParam}}}${closeRefTag}`;
								}
							}
							else if (contents.includes("conifers.org")) {
								match = regexes.Gymnosperm.exec(contents);
								var families = {
									ar: "Araucariaceae", cu: "Cupressaceae",
									cy: "Cycadaceae", ep: "Ephedraceae",
									gi: "Ginkgoaceae", gn: "Gnetaceae",
									pi: "Pinaceae", po: "Podocarpaceae",
									sc: "Sciadopityaceae", ta: "Taxaceae",
									we: "Welwitschiaceae", 
								};
								if (match) {
									if (families[match[1]])
										return `${openRefTag}{{Gymnosperm Database`
											+ ` |family=${families[match[1]]}`
											+ (match[2] ? ` |genus=${match[2]}` : '')
											+ (match[3] ? ` |species=${match[3]}` : '')
											+ (match[4] ? ` |subspecies=${match[4]}` : '')
											+ `${accessDateParam}}}${closeRefTag}`;
									else
										mw.notify(`Family ${match[1]} in ${wholeMatch} not recognized.`);
								}
							}
							else if (contents.includes("michiganflora.net")) {
								match = regexes.MichiganFlora.exec(contents);
								if (match) {
									let params;
									switch (match[1]) {
										case "family": case "genus":
											params = ` |${match[1]}=${match[2]}`; break;
										case "species": {
											params = ` |id=${match[2]}`;
											
											let genusAndSpecies;
											if (taxon) {
												genusAndSpecies = taxon.split(" ");
												params += ` |genus=${genusAndSpecies[0]} |species=${genusAndSpecies[1]}`;
											}
											break;
										}
									}
									return `${openRefTag}{{Michigan Flora${params}${accessDateParam}}}${closeRefTag}`;
								}
							}
							else if (contents.includes("ct-botanical-society.org")) {
								match = regexes.ConnecticutPlants.exec(contents);
								if (match) {
									return `${openRefTag}{{Connecticut Plants|${match[1]}|${taxon}`
										+ `${accessDateParam}}}${closeRefTag}`;
								}
							}
							else if (contents.includes("missouriplants.com")) {
								match = regexes.MissouriPlants.exec(contents);
								// captures: color, leaf, genus, species
								if (match) {
									return `${openRefTag}{{Missouri Plants |color=${match[1]} `
										+ `|leaf=${match[2]} |genus=${match[3]} `
										+ `|species=${match[4]}}}${closeRefTag}`;
								}
							}
							else if (contents.includes("wisflora.herbarium.wisc.edu")) {
								match = regexes.FloraOfWisconsin.exec(contents);
								if (match) {
									return `${openRefTag}{{Flora of Wisconsin`
										+ `|${match[1]}|${taxon}}}${closeRefTag}`;
								}
							}
							
							--count1;
							
							return wholeMatch;
						});
					
					// Look over external links.
					content = content.replace(
						/(^|[^\[])\[([^\[][^ ]+)(\s+[^\]]+)\](?=[^\]]|$)/g,
						function (wholeMatch, before, URL, text) {
							var match;
							++count2;
							
							if (URL.includes("missouriplants.com")) {
								match = regexes.MissouriPlants.exec(URL);
								// captures: color, leaf, genus, species
								if (match) {
									return `${before}{{Missouri Plants |color=${match[1]} `
										+ `|leaf=${match[2]} |genus=${match[3]} `
										+ `|species=${match[4]} |link=1}}`;
								}
							}
							else if (URL.includes("calphotos.berkeley.edu")) {
								match = regexes.CalPhotos.exec(URL);
								// captures: genus, species
								if (match) {
									return `${before}{{CalPhotos|${match[1]}|${match[2]}}}`;
								}
							}
							else if (URL.includes("kswildflower.org")) {
								match = regexes.KansasWildflowers.exec(URL);
								if (match) {
									return `${before}{{Kansas Wildflowers|${match[2]}|`
										+ (match[1] === "flower" ? '' : match[1])
										+ ' |link=1}}';
								}
							}
							else if (URL.includes("illinoiswildflowers.info")) {
								match = regexes.IllinoisWildflowers.exec(URL);
								if (match) {
									return `${before}{{Illinois Wildflowers|${match[1]} |link=1}}`;
								}
							}
							else if (URL.includes("minnesotawildflowers.info")) {
								match = regexes.MinnesotaWildflowers.exec(URL);
								if (match) {
									return `${before}{{Minnesota Wildflowers|${match[1]} |link=1}}`;
								}
							}
							else if (URL.includes("wildflower.org")) {
								match = regexes.wildflowerDotOrg.exec(URL);
								if (match) {
									return `${before}{{NPIN|${match[1].toUpperCase()} |link=1}}`;
								}
							}
							else if (URL.includes("ucjeps.berkeley.edu/cgi-bin/get_JM_treatment")) {
								match = regexes.JepsonManual.exec(URL);
								if (match) {
									return `${before}{{Jepson Manual |id=${match[1]} |link=1}}`;
								}
							}
							else if (URL.includes("ucjeps.berkeley.edu")) { // See also more specific condition above.
								match = regexes.Jepson.exec(URL);
								if (match && match[1] !== "key") {
									return `${before}{{Jepson eFlora|${match[2]} |link=1}}`;
								}
							}
							else if (URL.includes("calflora.org")) {
								match = regexes.Calflora.exec(URL);
								if (match) {
									return `${before}{{Calflora|${match[1].replace(/\+/g, " ")} |link=1}}`;
								}
							}
							else if (URL.includes("gobotany.newenglandwild.org")) {
								match = regexes.GoBotany.exec(URL);
								if (match) {
									match[1] = match[1].replace(/^./, (char) => char.toUpperCase());
									if (match[2]) {
										return `${before}{{Go Botany |genus=${match[1]}`
											+ ` |species=${match[2]}`
											+ ` |link=1}}`;
									}
									else {
										return `${before}{{Go Botany `
											+ `|genus=${match[1]} |link=1}}`;
									}
								}
							}
							else if (URL.includes("goorchids.northamericanorchidcenter.org")) {
								match = regexes.GoOrchids.exec(URL);
								if (match) {
									match[1] = match[1].replace(/^./, (char) => char.toUpperCase()); // genus
									if (match[2]) {
										return `${before}{{Go Orchids |genus=${match[1]}`
											+ ` |species=${match[2]}`
											+ ` |link=1}}`;
									}
									else {
										return `${before}{{Go Orchids `
											+ `|genus=${match[1]} |link=1}}`;
									}
								}
							}
							
							--count2;
							return wholeMatch;
						});
					
					var whatWasDone = [];
					if (count1 > 0) whatWasDone.push("citation" + (count1 === 1 ? "" : "s"));
					if (count2 > 0) whatWasDone.push("external link" + (count2 === 1 ? "" : "s"));
					addSummary(count1 + count2, `templatized ${whatWasDone.join(" and ")}`);
					
					if (count1 === 0 && count2 === 0)
						return;
					
					return content;
				}
		},
		{
			textBoxIncludes: "access_date",
			button: {
				text: "|access_date= &rarr; |access-date=",
			},
			minorEdit: true,
			func:
				function(content)
				{
					content = content.replace(/access_date/g, "access-date");
					
					addSummary(1, "|access_date= &rarr; |access-date=");
					
					return content;
				}
		},
		{
			condition: false,
			textBoxIncludes: /(^|\D)\d\d-\d\d-\d\d\d\d(?=\D|$)/,
			button: {
				text: "MM-DD-YYYY &rarr; YYYY-MM-DD",
			},
			minorEdit: false,
			func:
				function(content)
				{
					var count = 0;
					
					content = content.replace(
						/(^|\D)(\d\d-\d\d)-(\d\d\d\d)(?=\D|$)/,
						function (wholeMatch, before, monthDay, year) {
							++count;
							return before + year + "-" + monthDay;
						});
					
					mw.notify(`${count} numerical date${count === 1 ? "" : "s"} fixed.`);
					
					return content;
				}
		},
		{
			textBoxIncludes: /\{\{[Cc]onvert\|[^}]+\|\s*abbr\s*=\s*on/,
			button: {
				text: "{{convert}} &rarr; {{cvt}}",
			},
			minorEdit: true,
			func:
				function(content)
				{
					content = content.replace(/\{\{[Cc]onvert(\|[^}]+)\|\s*abbr\s*=\s*on\s*/g,
						"{{cvt$1");
					
					addSummary(1, "[[Template:convert]] with |abbr=on &rarr; [[Template:cvt]] for brevity");
					
					return content;
				}
		},
		{
			// condition: namespaceNumber === 0,
			textBoxIncludes: /\[\[([^\|\]]+)\|\1[^\]]+\]\]/,
			button: {
				text: "fix piped links",
			},
			minorEdit: true,
			func:
				function(content)
				{
					let count = 0;
					content = content.replace(
						/\[\[([^|]+)\|(\1)([^\]]*)\]\]/gi,
						function (wholeMatch, _, linkText, suffix) {
							++count;
							return `[[${linkText}]]${suffix}`;
						});
					
					addSummary(count, `fixed piped link${count !== 1 ? 's' : ''}`);
					
					return content;
				}
		},
		{
			textBoxIncludes: /\[\[([^\|]+)\|\1\]\]/,
			button: {
				text: "fix piped links",
			},
			minorEdit: true,
			func:
				function(content)
				{
					var count = 0;
					
					content = content.replace(
						/\[\[([^\|]+)\|\1\]\]/g,
						function(wholeMatch, target)
						{
							count++;
							return "[[" + target + "]]";
						}
					);
					
					notifyReplacements(count);
					addSummary(count, "fixed piped links");
					
					return content;
				}
		},
		{
			textBoxIncludes: /[‹›⟨⟩]/,
			button: {
				text: "add {{angbr}}",
			},
			minorEdit: true,
			func:
				function(content)
				{
					var count = 0;
					
					content = content.replace(
						/⟨([^⟩]+)⟩|‹([^›]+)›/g,
						function(wholematch, insideBrackets1, insideBrackets2)
						{
							count++;
							
							return "{{angbr|" + ( insideBrackets1 || insideBrackets2 ) + "}}";
						}
					);
					
					notifyReplacements(count);
					addSummary(count, "replaced literal angle brackets with [[Template:angbr]]");
					
					return content;
				}
		},
		{
			textBoxIncludes: "{{IPA|",
			button: {
				text: "' &rarr; ˈ",
			},
			minorEdit: true,
			func:
				function(content)
				{
					var count = 0;
					
					content = content.replace(
						/{{IPA\|([^\}\n]+)}}/g,
						function(wholematch, transcription)
						{
							if ( transcription.includes("'") )
							{
								count++;
								return "{{IPA|" + transcription.replace(/'/g, "ˈ") + "}}";
							}
							else
								return wholematch;
						}
					);
					
					notifyReplacements(count);
					addSummary(count, "replaced apostrophe with length mark in IPA transcriptions");
					
					return content;
				}
		},
		{
			textBoxIncludes: /(\/[^\/]+\/|\[[^\]]+\])(?=[\s\.\,\:\;\)\&\-<])/,
			button: {
				text: "add {{IPA}}",
			},
			minorEdit: true,
			func:
				function(content)
				{
					var count = 0;
					
					var escaped = [];
					var i = 0;
					
					var escape = function(text, regexString)
					{
						var regex = new RegExp(regexString, "g");
						text = text.replace(
							regex,
							function(match)
							{
								escaped[i] = match;
								var replacement = "%%" + i + "%%";
								i += 1;
								return replacement;
							}
						);
						return text;
					};
					
					content = escape(content, "(?:https?:)\\/\\/[^\\s\\|]+");
					content = escape(content, "<\\/?[a-z][^>\n]+>");
					content = escape(content, "\\{\\{IPA(?:[^\\{\\}\n]+|\\{\\{[^\\}\\n]+\\}\\})+\\}\\}");
					
					var numberRegEx = /^\d+$/;
					
					content = content.replace(
						/(?:\/[^\/\|%\n]+\/|\[[^\]\|%\n]+\])(?=[\s\.\,\:\;\)\&\-<%])/g,
						function(wholematch)
						{
							count++;
							
							if ( wholematch.includes("http") || wholematch.includes("...")
								|| wholematch.match(numberRegEx))
							{
								count--;
								return wholematch;
							}
							else
								return "{{IPA|" + wholematch + "}}";
						}
					);
					
					content = content.replace(
						/%%(\d+)%%/g,
						function(wholematch, number) {
							number = Number(number);
							return escaped[number];
						}
					);
					content = content.replace(
						/%%(\d+)%%/g,
						function(wholematch, number) {
							number = Number(number);
							return escaped[number];
						}
					);
					
					notifyReplacements(count);
					addSummary(count, "added [[Template:IPA]]");
					
					return content;
				}
		},
		{
			textBoxIncludes: /[\/\[]\{\{IPA|\{\{IPA\|[^\}]*\{\{IPA ?link/,
			button: {
				text: "clean up IPA templates",
			},
			minorEdit: true,
			watch: false,
			func:
				function(content)
				{
					var count1 = 0, count2 = 0, count3 = 0;
					
					content = content.replace(
						/([\/\[])\{\{IPA\|\[\[([^\}]+?)\]\]\|*\}\}([\/\]])/g,
						function(wholematch, openingBracket, transcription, closingBracket)
						{
							count2++;
							var template;
							
							if ( openingBracket === "/" )
								template = "IPAslink";
							else if ( openingBracket === "[" )
								template = "IPAblink";
							
							if ( template )
								return "{{" + template + "|" + transcription + "}}";
							else
							{
								count2--;
								return wholematch;
							}
						}
					);
					
					content = content.replace(
						/([\/\[])\{\{IPA\|([^\}]+?)\|*\}\}([\/\]])/g,
						function(wholematch, openingBracket, transcription, closingBracket)
						{
							count1++;
							
							return "{{IPA|" + openingBracket + transcription + closingBracket + "}}";
						}
					);
					
					/*
					content = content.replace(
						/([\/\[])\{\{IPA\|/g,
						function(wholematch, openingBracket)
						{
							count++;
							
							return "{{IPA|" + openingBracket;
						}
					);
					*/
					
					content = content.replace(
						/(?:\{\{IPA\|)?([\/\[])\{\{IPA ?link\|([^\}]+)\}\}([\/\]])(?:\}\})?/g,
						function(wholematch, openingBracket, transcription, closingBracket)
						{
							count2++;
							
							var template;
							
							if ( openingBracket === "/" )
								template = "IPAslink";
							else if ( openingBracket === "[" )
								template = "IPAblink";
							
							if ( template )
								return "{{" + template + "|" + transcription + "}}";
							else
							{
								count2--;
								return wholematch;
							}
						}
					);
					
					content = content.replace(/\{\{IPA\|((?:[^\{\}]+|\{\{[^\}]+\}\})+)\}\}/g,
						function (wholeMatch, innards) {
							return "{{IPA|"
								+ innards.replace(/\{\{IPA ?link\|/g,
									function (wholeMatch) {
										++count3;
										return "{{IPAplink|";
									})
								+ "}}";
						});
					
					addSummary(count1, "moved brackets inside [[Template:IPA]]");
					addSummary(count2, "replaced [[Template:IPA link]] with [[Template:IPAslink]] or [[Template:IPAblink]]");
					addSummary(count3, "{{[[Template:IPA link|IPA link]]}} → {{[[Template:IPAplink|IPAplink]]}} inside {{[[Template:IPA|IPA]]}} for cleaner HTML");
					
					notifyReplacements(count1 + count2 + count3);
					
					return content;
				}
		},
		{
			textBoxIncludes: /[ʦʣʧʤʨʥ]/,
			button: {
				text: "update deprecated IPA",
			},
			minorEdit: true,
			func:
				function(content)
				{
					var count = 0;
					
					var update = new Map([
						[ "ʧ", "t͡ʃ" ],
						[ "ʤ", "d͡ʒ" ],
						[ "ʦ", "t͡s" ],
						[ "ʣ", "d͡z" ],
						[ "ʨ", "t͡ɕ" ],
						[ "ʥ", "d͡ʑ" ]
					]);
					
					update.forEach(
						function(value, key)
						{
							var regex = new RegExp(key, "g");
							
							content = content.replace(
								regex,
								function(wholematch)
								{
									count++;
									return value;
								}
							);
						}
					);
					
					addSummary(count, "updated deprecated IPA");
					
					return content;
				}
		},
		// From [[User:Erutuon/footnoteCleanup.js]].
		{
			condition: [ 0, 12 ].includes(namespaceNumber),
			button: {
				text: "clean up footnotes",
				id: "footnote-cleanup"
			},
			minorEdit: true,
			func:
				function(content)
				{
					var oldContent = content;
					
					var escaped = [];
					var i = 0;
					
					var replacements = [];
					var count = 0;
					
					var escape = function(text, regexString)
					{
						var regex = new RegExp(regexString, "g");
						text = text.replace(
							regex,
							function(match)
							{
								escaped[i] = match;
								var replacement = "%%" + i + "%%";
								i += 1;
								return replacement;
							}
						);
						return text;
					};
					
					var puncRegex = /((?:%%\d+%%)+)([\.\,\;\:\"]{1,3})/g;
					
					var reorder = function(match, capture1, capture2)
					{
						count += 1;
						var replacement = capture2 + capture1;
						replacements.push(replacement);
						return replacement;
					};
					
					var fixPunctuationPlacement = function(text)
					{
						while ( puncRegex.test(text) )
							text = text.replace(
								/\s*((?:%%\d+%%)+)\s*([\.\,\;\:\"]{1,3})/g,
								reorder
							);
						
						return text;
					};
					
					/*	Escape various things:
						ref tags				*/
					
					content = escape(
						content,
						"<ref[^\\/<>]+\\/>"
					);
					
					content = escape(
						content,
						"<ref[^>]*>[^<]+<\\/ref>"
					);
					
					// citation needed
					content = escape(
						content,
						"\\{\\{(?:[Cc]itation needed|[Cc]n|[Ff]act|[Cc]b|[Cc]tn|[Rr]ef\\?)\\|[^\}]+\\}\\}"
					);
					
					// "dubious"
					content = escape(
						content,
						"\\{\\{(?:[Dd]ubious)\\|[^\}]+\\}\\}"
					);
					
					content = fixPunctuationPlacement(content);
					
					// footnote templates
					/*	Handles up to one level of nested templates.
						Any more, and there may be problems.			*/
					content = escape(
						content,
						"\\{\\{(?:sfn|efn|rfn)\\|(?:[^\\}]*?(?:\\{\\{[^\\}]+\\}\\})?)+\\}\\}"
					);
					
					if ( i > 0 )
						mw.notify(i + " refs or tagging templates found.");
					
					content = fixPunctuationPlacement(content);
					
					if ( count === 0 )
						mw.notify("No misplaced refs or tagging templates were found.");
					else
						mw.notify(count + " correction" + ( ( count > 1 && "s" ) || "" ) + " made: " + replacements.join());
					
					/*	Unescape the various things escaped above.
						This has to be done twice, since escaping was done twice.	*/
					content = content.replace(
						/%%(\d+)%%/g,
						function(wholematch, number) {
							number = Number(number);
							return escaped[number];
						}
					);
					content = content.replace(
						/%%(\d+)%%/g,
						function(wholematch, number) {
							number = Number(number);
							return escaped[number];
						}
					);
					
					addSummary(count, "made sure refs are after punctuation as prescribed by [[WP:REFPUNC]]");
					
					return content;
				}
		},
		
		// From [[User:Erutuon/scripts/imageSize.js]].
		{
			textBoxIncludes: /[=|]\s*\d+px/,
			button: {
				text: "convert pixel to scaling size",
			},
			minorEdit: true,
			watch: false,
			func:
				function(content)
				{
					var count1 = 0;
					var count2 = 0;
					
					// earlier regex: /\[\[(?:[Ff]ile|[Ii]mage)(?:[^\]]+|\[+[^\]]+\]+)+\]\]/g
					// improved version: /\[\[(?:[Ff]ile|[Ii]mage)(?:[^\[\]\n]+|\[+[^\[\[\n]+\]+)+\]\]/g
					// version less prone to backtracking errors: /\[\[(?:[Ff]ile|[Ii]mage).+/g
					content = content.replace(
						/\[\[(?:[Ff]ile|[Ii]mage).+/g,
						function(wholematch)
						{
							var convertedPixels = false;
							
							wholematch = wholematch.replace(
								/\|\s*(\d+)px\s*(?=[\|\]])/g,
								function(match, number)
								{
									// Convert string to number.
									number = Number(number);
									
									if ( number > 99 )
									{
										// Convert to upright value.
										number = number / 220;
										// Round to nearest hundredth.
										number = Math.floor( number * 100 ) / 100;
										
										count1++;
										
										if ( number === 1 )
											return "";
										else {
											convertedPixels = true;
											return "|upright=" + number;
										}
									}
									// Do not convert pixel value if it is less than 100 pixels.
									else
										return match;
								}
							);
							
							if ( convertedPixels && !wholematch.includes("thumb") )
								wholematch = wholematch.replace(
									/\]\]/g,
									function()
									{
										count2++;
										return "|frameless]]";
									}
								);
							
							return wholematch;
						}
					);
					
					content = content.replace(
						/\{\{(?:(?:[Aa]utomatic )?[Tt]axo|[Ss]pecies)box(?:[^\{\}]+|\{\{[^}]+\}\})+\}\}/g,
						function (template) {
							var taxon;
							template = template.replace(
								/\|\s*([^=\|\}]+?)(\s*=\s*)((?:\[\[[^\]]+\]\]|\[[^\]]+\]|\{\{[^\}]+\}\}|[^\|\}\[\]]+)+)/g,
								function (wholeMatch, paramName, between, paramValue) {
									if (!paramName.includes("width"))
										return wholeMatch;
									
									var match = paramValue.match(/(\d+)px/);
									
									// Convert string to number.
									var number = Number(match[1]);
									
									if ( number > 99 )
									{
										// Convert to upright value.
										number = number / 220;
										// Round to nearest hundredth.
										number = Math.floor( number * 100 ) / 100;
										
										count1++;
										
										if ( number === 1 )
											return ""; // Parameter is unnecessary.
										else
											paramValue = number + "\n";
									}
									
									if (!startsAndEndsWithWhitespace.test(between))
										between = " = ";
									
									paramName = paramName.replace("width", "upright");
									
									return "| " + paramName + between + paramValue;
								})
								.replace(/\|\}\}$/, "}}");
							
							return template;
						});
					
					notifyReplacements(count1);
					
					var summary = "converted image sizes in pixels to scaling values, as prescribed by [[WP:IMGSIZE]]";
					
					if ( count2 > 0 )
						summary += ", changing images to frameless as needed";
					
					addSummary(count1, summary);
					
					return content;
				}
		},
	];

	$.when(
		$.getScript("//en.wiktionary.org/w/index.php?title=User:Erutuon/scripts/CleanupButtons.js&action=raw&ctype=text/javascript"),
		$.ready
	).done(function () {
		const buttons = new CleanupButtons();

		for ( const buttonInfo of cleanupFunctions )
			if (CleanupButtons.evaluateConditions(buttonInfo.condition, buttonInfo.textBoxIncludes))
				buttons.addButton(buttonInfo);
	});
}

// </nowiki>