User:Aidan9382/scripts/fixlint.js

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.
/*
General fixer for simple lint errors
Currently attempts to fix:
- Font tags (converts all parameters to their style= versions)
- Strike tags (replaces them with <s>) and tt tags (replaces them with <kbd>)
- Center tags (replaces with the center template for text and style="margin:1em auto" for wikitables)

Nothing else beyond some obselete tags is fixed as of now

A "Fix Lint" button will be added to the More tab - use this to apply fixes

For a variety of reasons, all edits should be supervised and checked after the script has ran
I don't often use JS so expect a wild number of bad practices

Inspired by, and built with help from, [[User:ಮಲ್ನಾಡಾಚ್ ಕೊಂಕ್ಣೊ/center.js]]
*/
// <nowiki>

var ISDEV = (getURIArg('fixlintdev') == 'true');
var fontColours = ['aliceblue', 'antiquewhite', 'aqua', 'aquamarine', 'azure', 'beige', 'bisque', 'black', 'blanchedalmond', 'blue', 'blueviolet', 'brown', 'burlywood', 'cadetblue', 'chartreuse', 'chocolate', 'coral', 'cornflowerblue', 'cornsilk', 'crimson', 'cyan', 'darkblue', 'darkcyan', 'darkgoldenrod', 'darkgray', 'darkgrey', 'darkgreen', 'darkkhaki', 'darkmagenta', 'darkolivegreen', 'darkorange', 'darkorchid', 'darkred', 'darksalmon', 'darkseagreen', 'darkslateblue', 'darkslategray', 'darkslategrey', 'darkturquoise', 'darkviolet', 'deeppink', 'deepskyblue', 'dimgray', 'dimgrey', 'dodgerblue', 'firebrick', 'floralwhite', 'forestgreen', 'fuchsia', 'gainsboro', 'ghostwhite', 'gold', 'goldenrod', 'gray', 'grey', 'green', 'greenyellow', 'honeydew', 'hotpink', 'indianred', 'indigo', 'ivory', 'khaki', 'lavender', 'lavenderblush', 'lawngreen', 'lemonchiffon', 'lightblue', 'lightcoral', 'lightcyan', 'lightgoldenrodyellow', 'lightgray', 'lightgrey', 'lightgreen', 'lightpink', 'lightsalmon', 'lightseagreen', 'lightskyblue', 'lightslategray', 'lightslategrey', 'lightsteelblue', 'lightyellow', 'lime', 'limegreen', 'linen', 'magenta', 'maroon', 'mediumaquamarine', 'mediumblue', 'mediumorchid', 'mediumpurple', 'mediumseagreen', 'mediumslateblue', 'mediumspringgreen', 'mediumturquoise', 'mediumvioletred', 'midnightblue', 'mintcream', 'mistyrose', 'moccasin', 'navajowhite', 'navy', 'oldlace', 'olive', 'olivedrab', 'orange', 'orangered', 'orchid', 'palegoldenrod', 'palegreen', 'paleturquoise', 'palevioletred', 'papayawhip', 'peachpuff', 'peru', 'pink', 'plum', 'powderblue', 'purple', 'rebeccapurple', 'red', 'rosybrown', 'royalblue', 'saddlebrown', 'salmon', 'sandybrown', 'seagreen', 'seashell', 'sienna', 'silver', 'skyblue', 'slateblue', 'slategray', 'slategrey', 'snow', 'springgreen', 'steelblue', 'tan', 'teal', 'thistle', 'tomato', 'turquoise', 'violet', 'wheat', 'white', 'whitesmoke', 'yellow', 'yellowgreen']; //Compiled from https://www.w3schools.com/colors/colors_names.asp

function getURIArg(arg) {
	var re = RegExp('[&?]' + arg + '=([^&]*)');
	var matches = re.exec(document.location);
	if (matches) {
		try { 
			return decodeURI(matches[1]);
		} catch (e) { }
	}
	return null;
}

function ProcessTag(tag) { //Turns a tag into a more usable table, hopefully
	var tagData = {};
	var tagArgs = {};
	tagData.args = tagArgs;
	
	var fspace = tag.search(" ");
	if (fspace == -1)
		tagData.name = tag.substring(1,tag.length-1);
	else
		tagData.name = tag.substring(1,fspace);
	
	var tempData = tag;
	while (true) {
		var argName;
		var argData;
		var tagArg = RegExp("(\\w+) ?= ?(?:'([^']*)'|\"([^\"]*)\")").exec(tempData);
		if (!tagArg) {
			tagArg = RegExp("(\\w+) ?= ?([^ >]+)").exec(tempData);
			if (!tagArg) {
				break;
			} else {
				argName = tagArg[1].toLowerCase();
				argData = tagArg[2];
			}
		} else {
			argName = tagArg[1].toLowerCase();
			argData = tagArg[2] || tagArg[3];
		}
		if (ISDEV)
			console.log("tagArg",tempData,tagArg);
		tagArgs[argName] = argData;
		tempData = tempData.replace(tagArg[0],"");
	}
	return tagData;
}

function StringifyTag(tagData) { //The inverse of the above
	var final = "<" + tagData.name;
	for (var argName in tagData.args) {
		var argData = tagData.args[argName];
		if (argData != null) {
			final = final + " " + argName + "=\"" + argData + "\"";
		}
	}
	return final + ">";
}

//Add a tab to activate
if(mw.config.get('wgArticleId') != 0 ) {
	section = getURIArg("section");
	mw.util.addPortletLink(
		'p-cactions', 
		mw.util.getUrl(null,{action:'edit',section:section,fixlint:true}),
		'Fix Lint',
		'ca-fixlint',
		'Fix basic Lint Errors'
	);
}

function notify(title,message,type,autohide,autohidetime) {
	if (mw.notification) {
		console.log("notify",title,message);
		autohide = (autohide==null && true) || autohide;
		autohidetime = (autohidetime==null && "short") || autohidetime;
		if (!mw.notification.autoHideSeconds[autohidetime])
			mw.notification.autoHideSeconds[autohidetime] = autohidetime; //Enable custom arbitrary lengths
		mw.notification.notify(message,{autoHide:autohide,autoHideSeconds:autohidetime,title:title,type:type});
	}
}

function modifyPageContent(notifyOfEvents) {
	function notifyIfAllowed(title,message,type,autohide,autohidetime) {
		if (notifyOfEvents) {
			notify(title,message,type,autohide,autohidetime);
		}
	}
	var myContent = document.getElementById('wpTextbox1').value;
	var noIssues = true;
	
	//Remove unprocessed content (content inside nowiki or syntaxhighlight)
	//This content is never actively displayed and we should therefore never judge it
	var removedText = {};
	var tag;
	var i;
	var unprocessedTags = ["[Ss][Yy][Nn][Tt][Aa][Xx][Hh][Ii][Gg][Hh][Ll][Ii][Gg][Hh][Tt]","[Nn][Oo][Ww][Ii][Kk][Ii]","[Pp][Rr][Ee]"];
	for (tag in unprocessedTags) {
		tag = unprocessedTags[tag];
		i = Object.keys(removedText).length;
		removedText[i] = [];
		if (ISDEV)
			console.log("Handling",tag,i,removedText[i]);
		while (true) {
			var foundTag = RegExp('<'+tag+'[^>]*>[\\s\\S]+?</'+tag+'>').exec(myContent);
			if (!foundTag) {
				break;
			} else {
				foundTag = foundTag[0];
				removedText[i][removedText[i].length] = foundTag;
				myContent = myContent.replace(foundTag,"REMOVED_TAG_"+i+"_"+(removedText[i].length-1));
			}
		}
	}
	if (ISDEV)
		console.log("removed content:",removedText);
	
	// Fix font tags - START
	var fontTagBalance = myContent.split(/<font/gi).length-myContent.split(/<\/font/gi).length;
	if (fontTagBalance > 0) {
		notifyIfAllowed("Lint Fixer","There were "+fontTagBalance+" too many opening font tags","warn",false);
		noIssues = false;
	} else if (fontTagBalance < 0) {
		notifyIfAllowed("Lint Fixer","There were "+(-fontTagBalance)+" too many closing font tags","warn",false);
		noIssues = false;
	}
	while (true) {
		var fontTag = RegExp('<[Ff][Oo][Nn][Tt][^>]*>').exec(myContent);
		if (!fontTag) { //Out of font tags, we are done here
			break;
		} else {
			fontTag = fontTag[0];
		}
		if (ISDEV)
			console.log("fontTag",fontTag);
		
		var tagData = ProcessTag(fontTag);
		var style;
		
		var fontColour = tagData.args.color;
		if (fontColour) {
			style = tagData.args.style || "";
			if (style.length > 0 && style.substring(style.length-1) != ";") {
				style = style + ";";
			}
			if (fontColour.substring(0,1) != "#" && !isNaN(parseInt(fontColour,16)) && !fontColours.includes(fontColour.toLowerCase())) {
				fontColour = "#" + fontColour;
			}
			style = style + "color:" + fontColour;
			tagData.args.style = style;
			tagData.args.color = null;
		}
		
		var fontFace = tagData.args.face;
		if (fontFace) {
			style = tagData.args.style || "";
			if (style.length > 0 && style.substring(style.length-1) != ";") {
				style = style + ";";
			}
			style = style + "font-family:" + fontFace;
			tagData.args.style = style;
			tagData.args.face = null;
		}
		
		var fontSize = tagData.args.size;
		if (fontSize) { //This requires manual conversion, as the metric is a little different
			//Logic is based off of the exact px provided by [[mw:Help:Lint errors/obsolete-tag]]
			//Reinforced by the behaviour seen in [[Special:Permalink/1118333555]]
			var sizes = {1:"x-small",2:"small",3:"medium",4:"large",5:"x-large",6:"xx-large",7:"xxx-large"};
			style = tagData.args.style || "";
			if (style.length > 0 && style.substring(style.length-1) != ";") {
				style = style + ";";
			}
			if (fontSize.substring(fontSize.length-2) == "px") {
				fontSize = fontSize.substring(0,fontSize.length-2);
			}
			if (fontSize.substring(0,1) == "+") {
				fontSize = 3 + (parseInt(fontSize.substring(1)));
			} else if (fontSize.substring(0,1) == "-") {
				fontSize = 3 - (parseInt(fontSize.substring(1)));
			}
			fontSize = Math.max(1,Math.min(7,fontSize)); //Limit 1 -> 7
			if (!sizes[fontSize]) {
				notifyIfAllowed("Lint Fixer","Unable to recognise font size of "+fontSize+" ("+tagData.args.size+")","warn",false);
				noIssues = false;
			} else {
				style = style + "font-size:" + sizes[fontSize];
				tagData.args.style = style;
				tagData.args.size = null;
			}
		}

		tagData.name = "span";
		myContent = myContent.replace(fontTag,StringifyTag(tagData));
	}
	myContent = myContent.replaceAll(/<\/font>/gi,"</span>"); //Don't overengineer, this'll do
	// Fix font tags - END
	 
	// Fix strike tags - START
	myContent = myContent.replaceAll(/<strike>/gi,"<s>"); //Simple approach does the job
	myContent = myContent.replaceAll(/<\/strike>/gi,"</s>");
	// Fix strike tags - END
	// Fix tt tags - START
	myContent = myContent.replaceAll(/<tt>/gi,"<kbd>");
	myContent = myContent.replaceAll(/<\/tt>/gi,"</kbd>");
	// Fix tt tags - END
	
	// Fix center tags - START
	while (true) {
		var centerTag = RegExp('<[Cc][Ee][Nn][Tt][Ee][Rr]>([\\s\\S]+?)</[Cc][Ee][Nn][Tt][Ee][Rr]>').exec(myContent);
		var centerContent;
		if (!centerTag) {
			break;
		} else {
			centerContent = centerTag[1];
			centerTag = centerTag[0];
		}
		if (centerContent.search("{\\|") > -1) {
			wikitableStyle = RegExp('(\n{\\|.*?)\n').exec(centerContent)[1];
			styleTag = RegExp("(style=['\"].*?);?['\"]").exec(wikitableStyle);
			if (styleTag) { //These are messy lines, but it's supervised so it's good enough
				centerContent = centerContent.replace(wikitableStyle,wikitableStyle.replace(styleTag[0],styleTag[1]+";margin:1em auto\""));
			} else {
				centerContent = centerContent.replace(wikitableStyle,wikitableStyle+" style=\"margin:1em auto\"");
			}
			if (centerContent.substring(0,1) == "\n") {
				centerContent = centerContent.substring(1);
			}
			if (centerContent.substring(centerContent.length-1) == "\n") {
				centerContent = centerContent.substring(0,centerContent.length-1);
			}
			myContent = myContent.replace(centerTag,centerContent);
		} else {
			if (centerContent.search("=") > -1 && centerContent.search("{") == -1) { //Definite fix needed
				myContent = myContent.replace(centerTag,"{{center|1="+centerContent+"}}");
			} else { //May still need fix, but who knows :)
				myContent = myContent.replace(centerTag,"{{center|"+centerContent+"}}");
			}
		}
	}
	// Fix center tags - END
	
	//Bring back unprocessed content in the reverse order to avoid bad nesting fails
	for (i=Object.keys(removedText).length-1; i>=0; i--) {
		var tags = removedText[i];
		for (i2=0; i2<tags.length; i2++) {
			myContent = myContent.replace("REMOVED_TAG_"+i+"_"+i2,tags[i2]);
		}
	}
	
	return {myContent:myContent, wasChanged:document.getElementById('wpTextbox1').value != myContent, noIssues:noIssues};
}

if(mw.config.get('wgAction') == 'edit') {
	if (getURIArg('fixlint') == 'true' || ISDEV) {
		data = modifyPageContent(true);
		document.getElementById('wpTextbox1').value = data.myContent;
		if (data.wasChanged) {
			document.getElementById('wpSummary').value = '[[User:Aidan9382/scripts/fixlint.js|→]]Fix [[WP:Linter|Lint]] Errors';
			document.getElementById('wpMinoredit').checked = true;
			//document.getElementById('wpWatchthis').checked = true;
			//document.getElementById('wpWatchlistExpiry').selectedIndex = 2;
			if (!ISDEV && data.noIssues)
				document.forms.editform.wpDiff.click();
			else if (!data.noIssues)
				notify("Lint Fixer","There were some potential issues found during fixing","warn",false);
		} else {
			notify("Lint Fixer","No lint errors were found","success");
		}
	} else {
		data = modifyPageContent(false);
		if (data.wasChanged) {
			if (data.noIssues) {
				notify("Lint Fixer","Fixable lint errors were found on this page");
			} else {
				notify("Lint Fixer","Fixable lint errors were found on this page (supervision required)","warn");
			}
		} else {
			//notify("Lint Fixer","No lint errors were found","success");
		}
	}
}

// </nowiki>