User:Fuddle/Advisor-fuddle.js

// // See http://en.wikipedia.org/wiki/User:Cameltrader/Advisor.js/Description // for details and installation instructions. // // The script consists of three major parts: // * some helper functions // * the core of the user interface, including code that collects suggestions from a set of rules // * the rule implementations // // All functions, variables, and constants belonging to the script are // encapsulated in a private namespace object---``ct for ``Cameltrader:

var ct = ct || {};

// == Helpers ==

// === DOM manipulation ===

// Browsers offer means to highlight text between two given offsets (``start // and ``end) in a textarea, but some of them do not automatically scroll to it. // This function is an attempt to simulate cross-browser selection and scrolling. ct.setSelectionRange = function (ta, start, end) { // Initialise static variables used within this function var _static = arguments.callee; // this is the Function we are in. It will be used as a poor man's function-local static scope. ta.focus; ta.setSelectionRange(start, end); if(ct.noscroll)return; if (ta.setSelectionRange) { // Guess the vertical scroll offset by creating a		// separate hidden clone of the original textarea, filling it with the text // before ``start'' and computing its height. if (_static.NEWLINES == null) { _static.NEWLINES = '\n'; // 64 of them should be enough. for (var i = 0; i < 6; i++) { _static.NEWLINES += _static.NEWLINES; }		}		if (_static.helperTextarea == null) { _static.helperTextarea = document.createElement('TEXTAREA'); _static.helperTextarea.style.display = 'none'; document.body.appendChild(_static.helperTextarea); }		var hta = _static.helperTextarea; hta.style.display = ''; hta.style.width = ta.clientWidth + 'px'; hta.style.height = ta.clientHeight + 'px'; hta.value = _static.NEWLINES.substring(0, ta.rows) + ta.value.substring(0, start/*+(end-start)/2*/); var yOffset = hta.scrollHeight; hta.style.display = 'none'; //if(yOffset>ta.scrollTop||yOffset<=ta.scrollTop-ta.clientHeight) { if (yOffset > ta.clientHeight) { ta.scrollTop = yOffset - Math.floor(ta.clientHeight / 2); // Opera does not support setting the scrollTop property if (ta.scrollTop != yOffset) { // todo: Warn the user or apply a workaround }			} else { ta.scrollTop = 0; }		//}	} else { // IE incorrectly counts '\r\n' as a signle character start -= ta.value.substring(0, start).split('\r').length - 1; end -= ta.value.substring(0, end).split('\r').length - 1; var range = ta.createTextRange; range.collapse(true); range.moveStart('character', start); range.moveEnd('character', end - start); range.select; } };

// getPosition(e), observe(e, x, f), stopObserving(e, x, f), // and stopEvent(event) are inspired by the prototype.js framework // http://prototypejs.org/ ct.getPosition = function (e) { var x = 0; var y = 0; do { x += e.offsetLeft || 0; y += e.offsetTop || 0; e = e.offsetParent; } while (e); return {x: x, y: y}; };

ct.observe = function (e, eventName, f) { if (e.addEventListener) { e.addEventListener(eventName, f, false); } else { e.attachEvent('on' + eventName, f); } };

ct.stopObserving = function (e, eventName, f) { if (e.removeEventListener) { e.removeEventListener(eventName, f, false); } else { e.detachEvent('on' + eventName, f); } };

ct.stopEvent = function (event) { if (event.preventDefault) { event.preventDefault; event.stopPropagation; } else { event.returnValue = false; event.cancelBubble = true; } };

// ct.anchor is a shortcut to creating a link as a DOM node: ct.anchor = function (text, href, title) { var e = document.createElement('A'); e.href = href; e.appendChild(document.createTextNode(text)); e.title = title || ''; return e; };

// ct.link produces the HTML for a link to a Wikipedia article as a string. // It is convenient to embed in a help popup. ct.hlink = function (toWhat, text) { var wgServer = mw.config.get('wgServer') || 'http://en.wikipedia.org'; var wgArticlePath = mw.config.get('wgArticlePath') || '/wiki/$1'; var url = (wgServer + wgArticlePath).replace('$1', toWhat); return '' + (text || toWhat) + ''; };

// === Helpers a la functional programming === // A higher-order function---produces a cached version of a one-arg function. ct.makeCached = function (f) { var cache = {}; // a closure; the cache is private for f	return function (x) { return (cache[x] != null) ? cache[x] : (cache[x] = f(x)); }; };

// === Regular expressions === // Regular expressions can sometimes become inconveniently large. // In order to make complex ones easier to read, we introduce // a set of macros. Tokens enclosed with ``{ and ``} will be // replaced according to the hashtable below. // // To do the replacements, one must pass the RegExp object // through fixRegExp and use the result instead, like this: // //	var re = ct.fixRegExp(/It happened in {month}/); // // Also, for the sake of convenience, we add the "getAllMatches(re, s)" // method, which is a quick means to find all occurrences of a // regex in some text. It returns an array containing the results // of applying RegExp.exec(..).

ct.REG_EXP_REPLACEMENTS = { '{letter}': // all Unicode letters // http://www.codeproject.com/dotnet/UnicodeCharCatHelper.asp '\\u0041-\\u005a\\u0061-\\u007a\\u00aa' + '\\u00b5\\u00ba\\u00c0-\\u00d6' + '\\u00d8-\\u00f6\\u00f8-\\u01ba\\u01bc-\\u01bf' + '\\u01c4-\\u02ad\\u0386\\u0388-\\u0481\\u048c-\\u0556' + '\\u0561-\\u0587\\u10a0-\\u10c5\\u1e00-\\u1fbc\\u1fbe' + '\\u1fc2-\\u1fcc\\u1fd0-\\u1fdb\\u1fe0-\\u1fec' + '\\u1ff2-\\u1ffc\\u207f\\u2102\\u2107\\u210a-\\u2113' + '\\u2115\\u2119-\\u211d\\u2124\\u2126\\u2128' + '\\u212a-\\u212d\\u212f-\\u2131\\u2133\\u2134\\u2139' + '\\ufb00-\\ufb17\\uff21-\\uff3a\\uff41-\\uff5a', '{month}': // English only '(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec|'			+ 'January|February|March|April|June|July|August|September|'			+ 'October|November|December)', '{year}': '[12][0-9]{3}' };

ct.fixRegExp = function (re) { // : RegExp if (re.__fixedRE != null) { return re.__fixedRE; }	var s = re.source; for (var alias in ct.REG_EXP_REPLACEMENTS) { s = s.replace(				new RegExp(ct.escapeRegExp(alias), 'g'),				ct.REG_EXP_REPLACEMENTS[alias]		); }	re.__fixedRE = new RegExp(s); // the fixed copy is cached re.__fixedRE.global = re.global; re.__fixedRE.ignoreCase = re.ignoreCase; re.__fixedRE.multiline = re.multiline; return re.__fixedRE; };

ct.escapeRegExp = ct.makeCached(function (s) { // : RegExp	var r = '';	for (var i = 0; i < s.length; i++) {		var code = s.charCodeAt(i).toString(16);		r += '\\u' + '0000'.substring(code.length) + code;	}	return r; });

ct.getAllMatches = function (re, s) { // : Match[] var p = 0; var a = []; while (true) { re.lastIndex = 0; var m = re.exec(s.substring(p)); if (m == null) { return a;		} m.start = p + m.index; m.end = p + m.index + m[0].length; a.push(m); p = m.end; } };

// == Advisor core == // This is the basic functionality of showing and fixing suggestions.

// === Global constants and variables === ct.DEFAULT_MAX_SUGGESTIONS = 8; ct.maxSuggestions = ct.DEFAULT_MAX_SUGGESTIONS; ct.suggestions; // : Suggestion[] ct.eSuggestions; // : Element; that's where suggestions are rendered ct.eAddToSummary; // : Element; the proposed edit summary appears there ct.eTextarea; // : Element; the one with id="wpTextbox1" ct.appliedSuggestions = {}; // : Map

ct.scannedText = null; // remember what we scan, to check if it is                      // still the same when we try to fix it

ct.BIG_THRESHOLD = 100 * 1024; ct.isBigScanConfirmed = false; // is the warning about a big article confirmed ct.isTalkPageScanConfirmed = false;

ct.scanTimeoutId = null; // a timeout is set after a keystroke and before // a scan, this variable tracks its id

// === int main === // This is the entry point ct.observe(window, 'load', function {	ct.eTextarea = document.getElementById('wpTextbox1');	if (ct.eTextarea == null) {		// This is not an ``?action=edit'' page		return;	}	ct.eSuggestions = document.createElement('DIV');	ct.eSuggestions.style.border = 'dashed #ccc 1px';	ct.eSuggestions.style.color = '#888';	var e = document.getElementById('editform');	while (true) {		var p = e.previousSibling;		if ( (p == null) || ((p.nodeType == 1) && (p.id != 'toolbar')) ) {			break;		}		e = p;	}	e.parentNode.insertBefore(ct.eSuggestions, e);	ct.eAddToSummary = document.createElement('DIV');	ct.eAddToSummary.style.border = 'dashed #ccc 1px';	ct.eAddToSummary.style.color = '#888';	ct.eAddToSummary.style.display = 'none';	var wpSummaryLabel = document.getElementById('wpSummaryLabel');	wpSummaryLabel.parentNode.insertBefore(ct.eAddToSummary, wpSummaryLabel);	ct.scan; // do a scan now ...	ct.observeWikiText(ct.delayScan); // ... and every time the user pauses typing });

// === Internationalisation === // ct._ is a gettext-style internationalisation helper // (http://en.wikipedia.org/wiki/gettext) // If no translation is found for the parameter, it is returned as is. // Additionally, subsequent parameters are substituted for $1, $2, and so on. ct._ = function (s) { if (ct.translation && ct.translation[s]) { s = ct.translation[s]; }	var index = 1; while (arguments[index]) { s = s.replace('$' + index, arguments[index]); // todo: replace all? index++; }	return s; };

// === Editor compatibility layer === // Controlling access to wpTextbox1 helps abstract out compatibility // with editors like wikEd (http://en.wikipedia.org/wiki/User:Cacycle/wikEd)

ct.getWikiText = function { if (window.wikEdUseWikEd) { var obj = {sel: WikEdGetSelection}; WikEdParseDOM(obj, wikEdFrameBody); return obj.plain; }	return ct.eTextarea.value; };

ct.setWikiText = function (s) { if (window.wikEdUseWikEd) { // todo: wikEd compatibility alert(ct._('Changing text in wikEd is not yet supported.')); return; };	ct.eTextarea.value = s; };

ct.focusWikiText = function { if (window.wikEdUseWikEd) { wikEdFrameWindow.focus; return; }	ct.eTextarea.focus; };

ct.selectWikiText = function (start, end) { if (window.wikEdUseWikEd) { var obj = x = {sel: WikEdGetSelection, changed: {}}; WikEdParseDOM(obj, wikEdFrameBody); var i = 0; while ((obj.plainStart[i + 1] != null) && (obj.plainStart[i + 1] <= start)) { i++; }		var j = i;		while ((obj.plainStart[j + 1] != null) && (obj.plainStart[j + 1] <= end)) { j++; }		obj.changed.range = document.createRange; obj.changed.range.setStart(obj.plainNode[i], start - obj.plainStart[i]); obj.changed.range.setEnd(obj.plainNode[j], end - obj.plainStart[j]); WikEdRemoveAllRanges(obj.sel); obj.sel.addRange(obj.changed.range); return; }	ct.setSelectionRange(ct.eTextarea, start, end); };

ct.observeWikiText = function (callback) { // todo: wikEd compatibility ct.observe(ct.eTextarea, 'keyup', ct.delayScan); };

// === Interaction with the user === // ct.scan analyses the text and handles how the proposals are reflected in the UI. ct.scan = function (force) { ct.scanTimeoutId = null; var s = ct.getWikiText; if ((s === ct.scannedText) && !force) { return; // Nothing to do, we've already scanned the very same text }	ct.scannedText = s;	while (ct.eSuggestions.firstChild != null) { ct.eSuggestions.removeChild(ct.eSuggestions.firstChild); }	// Warn about scanning a big article if ((s.length > ct.BIG_THRESHOLD) && !ct.isBigScanConfirmed) { ct.eSuggestions.appendChild(document.createTextNode( ct._('This article is rather long. Advisor.js may consume a lot of '				+ 'RAM and CPU resources while trying to parse the text.  You could limit '				+ 'your edit to a single section, or ') ));		ct.eSuggestions.appendChild(ct.anchor( ct._('scan the text anyway.'), 'javascript: ct.isBigScanConfirmed = true; ct.scan(true); void(0);', ct._('Ignore this warning.') ));		return; }	// Warn about scanning a talk page var wgCanonicalNamespace = mw.config.get('wgCanonicalNamespace'); if ((wgCanonicalNamespace != null)				&& /(\b|_)talk$/i.test(wgCanonicalNamespace)				&& !ct.isTalkPageScanConfirmed) { ct.eSuggestions.appendChild(document.createTextNode( ct._('Advisor.js is disabled on talk pages, because ' +				'it might suggest changing other users\' comments. That would be ' +				'something against talk page conventions.  If you promise to be ' +				'careful, you can ') ));		ct.eSuggestions.appendChild(ct.anchor( ct._('scan the text anyway.'), 'javascript: ct.isTalkPageScanConfirmed = true; ct.scan(true); void(0);', ct._('Ignore this warning.') ));		return; }	ct.suggestions = ct.getSuggestions(s); if (ct.suggestions.length == 0) { ct.eSuggestions.appendChild(document.createTextNode( ct._('OK \u2014 Advisor+.js found no issues with the text.') // U+2014 is an mdash ));		return; }	var nSuggestions = Math.min(ct.maxSuggestions, ct.suggestions.length); ct.eSuggestions.appendChild(document.createTextNode( (ct.suggestions.length == 1) ? ct._('1 suggestion: ') : ct._('$1 suggestions: ', ct.suggestions.length) ));	for (var i = 0; i < nSuggestions; i++) { var suggestion = ct.suggestions[i]; var eA = ct.anchor(				suggestion.name,				'javascript:ct.showSuggestion(' + i + '); void(0);',				suggestion.description		); suggestion.element = eA; ct.eSuggestions.appendChild(eA); if (suggestion.replacement != null) { var eSup = document.createElement('SUP'); ct.eSuggestions.appendChild(eSup); eSup.appendChild(ct.anchor( ct._('fix'), 'javascript:ct.fixSuggestion(' + i + '); void(0);' ));		}		ct.eSuggestions.appendChild(document.createTextNode(' ')); }	if (ct.suggestions.length > ct.maxSuggestions) { ct.eSuggestions.appendChild(ct.anchor( '...', 'javascript: ct.maxSuggestions = 1000; ct.scan(true); void(0);', ct._('Show All') ));	} };

// getSuggestions returns the raw data used by scan. // It is convenient for unit testing. ct.getSuggestions = function (s) { var suggestions = []; for (var i = 0; i < ct.rules.length; i++) { var a = ct.rules[i](s); for (var j = 0; j < a.length; j++) { suggestions.push(a[j]); }	}	suggestions.sort(function (x, y) {		return (x.start < y.start) ? -1 :		      (x.start > y.start) ? 1 :		       (x.end < y.end) ? -1 :		       (x.end > y.end) ? 1 : 0;	}); return suggestions; };

// delayScan postpones the invocation of scan with a certain timeout. // If delayScan is invoked once again during that time, the original // timeout is cancelled, and another, clean timeout is started from zero. // // delayScan will normally be invoked when a key is pressed---this // prevents frequent re-scans while the user is typing. ct.delayScan = function { if (ct.scanTimeoutId != null) { clearTimeout(ct.scanTimeoutId); ct.scanTimeoutId = null; }	ct.scanTimeoutId = setTimeout(ct.scan, 1500); };

// showSuggestion handles clicks on the suggestions above the edit area // This does one of two things: // * on first click---highlight the corresponding text in the textarea // * on a second click, no later than a fixed number milliseconds after the // 		first one---show the help popup ct.showSuggestion = function (k) { if (ct.getWikiText != ct.scannedText) { // The text has changed - just do another scan and don't change selection ct.scan; return; }	var suggestion = ct.suggestions[k]; var now = new Date.getTime; if ((suggestion.help != null) && (ct.lastShownSuggestionIndex === k) && (now - ct.lastShownSuggestionTime < 5000)) { // Show help var p = ct.getPosition(suggestion.element); var POPUP_WIDTH = 300; var eDiv = document.createElement('DIV'); eDiv.innerHTML = suggestion.help; eDiv.style.position = 'absolute'; eDiv.style.left = Math.max(0, Math.min(p.x, document.body.clientWidth - POPUP_WIDTH)) + 'px'; eDiv.style.top = (p.y + suggestion.element.offsetHeight) + 'px'; eDiv.style.border = 'solid ThreeDShadow 1px'; eDiv.style.backgroundColor = 'InfoBackground'; eDiv.style.fontSize = '12px'; eDiv.style.color = 'InfoText'; eDiv.style.width = POPUP_WIDTH + 'px'; eDiv.style.padding = '0.3em'; eDiv.style.zIndex = 10; document.body.appendChild(eDiv); ct.observe(document.body, 'click', function (event) {			event = event || window.event;			var target = event.target || event.srcElement;			var e = target;			while (e != null) {				if (e == eDiv) {					return;				}				e = e.parentNode;			}			document.body.removeChild(eDiv);			ct.stopObserving(document.body, 'click', arguments.callee);		}); ct.focusWikiText; return; }	ct.lastShownSuggestionIndex = k;	ct.lastShownSuggestionTime = now; ct.selectWikiText(suggestion.start, suggestion.end); };

// Usually, there is a ``fix'' link next to each suggestion. It is handled by: ct.fixSuggestion = function (k) { var s = ct.getWikiText; if (s != ct.scannedText) { ct.scan; return; }	var editform = document.getElementById('editform'), st=editform.wpTextbox1.scrollTop, suggestion = ct.suggestions[k]; if (suggestion.replacement == null) { // the issue is not automatically fixable return; }	ct.setWikiText(			s.substring(0, suggestion.start)			+ suggestion.replacement			+ s.substring(suggestion.end)	); ct.selectWikiText(			suggestion.start,			suggestion.start + suggestion.replacement.length	); if(ct.noscroll)editform.wpTextbox1.scrollTop=st; //to help automatic scrolling do its job better // Propose an edit summary unless it's a new section if (!editform['wpSection'] || (editform['wpSection'].value != 'new')) { if (ct.appliedSuggestions[suggestion.name] == null) { ct.appliedSuggestions[suggestion.name] = 1; } else { ct.appliedSuggestions[suggestion.name]++; }		var a = []; for (var i in ct.appliedSuggestions) { a.push(i); }		a.sort(function (x, y) {			return (ct.appliedSuggestions[x] > ct.appliedSuggestions[y]) ? -1 :				  (ct.appliedSuggestions[x] < ct.appliedSuggestions[y]) ? 1 :				   (x < y) ? -1 : (x > y) ? 1 : 0;		}); var s = ''; for (var i = 0; i < a.length; i++) { var count = ct.appliedSuggestions[a[i]]; s += ', ' + ((count == 1) ? a[i] : (count + 'x ' + a[i])); }		// Cut off the leading ``,  and add ``formatting:  and ``using Advisor+.js'' s = ct._(				'formatting: $1 (using Advisor+.js)',				s.substring(2)		); // Render in DOM while (ct.eAddToSummary.firstChild != null) { ct.eAddToSummary.removeChild(ct.eAddToSummary.firstChild); }		ct.eAddToSummary.style.display = ''; ct.eAddToSummary.appendChild(ct.anchor( ct._('Add to summary'), 'javascript:ct.addToSummary(unescape("' + escape(s) + '"));', ct._('Append the proposed summary to the input field below') ));		ct.eAddToSummary.appendChild(document.createTextNode(': "' + s + '"')); }	// Re-scan immediately ct.scan; };

// The mnemonics of the accepted suggestions are accumulated in ct.appliedSuggestions // and the user is presented with a sample edit summary. If she accepts it, // addToSummary gets called. ct.addToSummary = function (summary) { var wpSummary = document.getElementById('wpSummary'); if (wpSummary.value != '') { summary = wpSummary.value + (wpSummary.value.search(/^\/\*.*\*\/ *$/)<0?'; ':'') + summary; }	if ((wpSummary.maxLength > 0) && (summary.length > wpSummary.maxLength)) { alert(ct._( 'Error: If the proposed text is added to the summary, ' + 'its length will exceed the $1-character maximum by $2 characters.', /* $1 = */ wpSummary.maxLength, /* $2 = */ summary.length - wpSummary.maxLength ));		return; }	wpSummary.value = summary; ct.eAddToSummary.style.display = 'none'; };

// == Rules ==

// This chapter contains the ``rules'' that produce suggestions---this is where // most of the load resides. Each rule is a javascript function that accepts a // string as a parameter (the wikitext of the page being edited) and returns an // array of ``suggestion'' objects. A suggestion object must have the following // properties: // * start---the 0-based inclusive index of the first character to be replaced // * end---analogous to start, but exclusive // * replacement---the proposed wikitext // * name---this is what appears at the top of the page // * description---used as a tooltip for the name of the suggestion

// The rules are stored in an array: ct.rules = ct.rules||[]; // : Function[] // and are grouped into categories.

// The set of rules to apply depends on the content language. Different // languages have different formatting conventions, therefore this is not // a matter of internationalisation like the UI core, but of unrelated // implementations. What follows is the implementation for the English-language // Wikipedia.

if (!ct.noDefaultRules && (!mw.config.get('wgContentLanguage') || (mw.config.get('wgContentLanguage') === 'en'))) { // switch to turn off default rules for replacement by custom ones // from this line on, a level of indent is spared

// === Linking rules ===

ct.rules.push(function (s) {	var re = /\[\[([{letter} ,\(\)\-]+)\|\1\]\]/g;	re = ct.fixRegExp(re);	var a = ct.getAllMatches(re, s);	for (var i = 0; i < a.length; i++) {		var m = a[i];		a[i] = {				start: m.start,				end: m.end,				replacement:  + m[1] + ,				name: 'A|A',				description: '"A|A" can be simplified to A.',				help: ct.hlink('WP:Syntax#Wiki_markup', 'MediaWiki syntax')					+ ' allows links of the form A|A to be abbreviated as A. '		};	}	return a; });

ct.rules.push(function (s) {	var re = /\[\[([{letter} ,\(\)\-]+)\|\1([{letter}]+)\]\]/g;	re = ct.fixRegExp(re);	var a = ct.getAllMatches(re, s);	for (var i = 0; i < a.length; i++) {		var m = a[i];		a[i] = {				start: m.start,				end: m.end,				replacement:  + m[1] +  + m[2],				name: 'A|AB',				description: '"A|AB" can be simplified to AB.',				help: ct.hlink('WP:Syntax#Wiki_markup', 'MediaWiki syntax')					+ ' allows links of the form A|AB to be abbreviated as AB.'		};	}	return a; });

ct.rules.push(function (s) {	// Initialise statics	var _static = arguments.callee;	if (_static.MONTH_MAP == null) {		_static.MONTH_MAP = {				Jan: 'January', Feb: 'February', Mar: 'March', Apr: 'April', May: 'May',				Jun: 'June', Jul: 'July', Aug: 'August', Sep: 'September', Oct: 'October',				Nov: 'November', Dec: 'December', January: 'January', February: 'February',				March: 'March', April: 'April', June: 'June', July: 'July',				August: 'August', September: 'September', October: 'October',				November: 'November', December: 'December'		};	}	// This will match either a date+year or just a year, and will not match solitary dates.	// If the year is part of an ISO date of the form yyyy-mm-dd, the remainder is included.	// The rule only controls the transition from linked to unlinked, as practice has shown	// that improper linking is significantly more common than leaving linkable dates as plain text. var re = /(?:\[\[((?:(\d\d?) +({month}))|(?:({month}) +(\d\d?)))\]\],?? *)?\[\[({year})\]\](-\[\[\d\d-\d\d\]\])?/; re = ct.fixRegExp(re); var a = ct.getAllMatches(re, s); var b = []; for (var i = 0; i < a.length; i++) { var m = a[i]; var date = m[1] || null; var year = m[7] || null; if (date == null) { if (!m[8]) { // protect ISO dates---m[8] is the ISO remainder b.push({						start: m.start,						end: m.end,						replacement: year,						name: 'year link',						description: 'Convert link to normal text',						help: 'It is useless to link a year unless it is preceded by a day and month.'							+ ' Years with a day and month are normally linked so that the user '							+ 'preferences for date format can be applied, but linking a year alone '							+ 'has no effect.'				}); }		} else { var isAmerican = !m[2]; var day = (isAmerican) ? m[5] : m[2]; var month = _static.MONTH_MAP[(isAmerican) ? m[4] : m[3]]; var ws = m[6] || ''; // whitespace between date and year var replacement = (isAmerican) ? ( + month + ' ' + day + ',' + ws +  + year + '') : ( + day + ' ' + month +  + ws +  + year + ); if (replacement != m[0]) { b.push({						start: m.start,						end: m.end,						replacement: replacement,						name: 'date format',						description: 'Fix date format',						help: 'Commas in dates should follow one of these styles: '								+ '1 January 1970 '								+ 'January 1, 1970 '								+ 'and month names should not be abbreviated.'				}); }		}	}	return b; });

ct.rules.push(function (s) {	// Matches decades in the range 1000s ... 2990s,	// linked either as xxx0s or as xxx0s	var re = /\[\[([12][0-9][0-9]0)(\]\]s\b|'?s\]\])/g;	var a = ct.getAllMatches(re, s);	for (var i = 0; i < a.length; i++) {		var m = a[i];		a[i] = {				start: m.start,				end: m.end,				replacement: m[1] + 's',				name: 'decade link',				description: 'Convert link to normal text',				help: 'Decades should not be linked, unless they deepen the '					+ 'readers\' understanding of the topic.'		};	}	return a; });

ct.rules.push(function (s) {	// Matches decades in the range 1000s ... 2990s	var re = /\bthe +([12][0-9][0-9]0)'s\b/g;	var a = ct.getAllMatches(re, s);	for (var i = 0; i < a.length; i++) {		var m = a[i];		a[i] = {				start: m.start,				end: m.end,				replacement: m[1] + 's',				name: 'decade format',				description: 'Remove the apostrophe from the decade',				help: 'The preferred decade format is without an apostrophe, per '						+ ct.hlink('WP:DATE#Longer_periods') + '.'		};	}	return a; });

ct.rules.push(function (s) {	var re = /\[\[([0-9]{1,2}(st|nd|rd|th) century)\]\]/g	var a = ct.getAllMatches(re, s);	for (var i = 0; i < a.length; i++) {		var m = a[i];		a[i] = {				start: m.start,				end: m.end,				replacement: m[1],				name: 'century link',				description: 'Convert link to normal text',				help: 'Centuries should not be linked, unless they deepen the '					+ 'readers\' understanding of the topic.'		};	}	return a; });

// === Character formatting rules ===

ct.rules.push(function (s) {	var a = ct.getAllMatches(/ +$/gm, s);	var b = [];	for (var i = 0; i < a.length; i++) {		var m = a[i];		if (/^[=\|]$/.test(s[m.start - 1])) { // this can be tolerated, it happens too often in templates			continue;		}		b.push({ start: m.start, end: m.end, replacement: '', name: 'whitespace', description: 'Delete trailing whitespace', help: 'Trailing whitespace at the end of a line is unnecessary.' });	}	return b; });

ct.rules.push(function (s) {	var re = /[^0-9]({year}) *(?:-|\u2014|&mdash;|--) *({year})[^0-9]/g; // U+2014 is an mdash	var a = ct.getAllMatches(re, s);	for (var i = 0; i < a.length; i++) {		var m = a[i];		a[i] = {				start: m.start + 1,				end: m.end - 1,				replacement: m[1] + '\u2013' + m[2], // U+2013 is an ndash				name: 'ndash',				description: 'Year ranges look better with an n-dash.'		};	}	return a; });

ct.rules.push(function (s) {	var a = ct.getAllMatches( /(\{\{\s*(?:IPA[0-3]?|IPAAusE|IPAEng|IPAHe|[Pp]ronAusE|[Pp]ronEng|[Pp]ronounced)\s*\|\s*)([^\|\}]+)/gi, s	);	var b = [];	var ipaSubstitions = {			':': {					replacement: '\u02d0', // U+02D0 is a ``Modifier letter triangular colon (used to denote vowel lengthening in IPA)					additionalHelp: " In this case the triangular colon (``\u02d0, U+02D0</tt>), "						+ "used to denote vowel lengthening, looks like a regular colon (``:, <tt>U+003A</tt>)."			},			'\: {					replacement: '\u02c8', // U+02C8 is a ``Modifier letter vertical line (put before a stresses syllable)					additionalHelp: " In this case the vertical line (``\u02c8, <tt>U+02c8</tt>), "						+ " which is put before a stressed syllable, looks like an apostrophe (`` ' '', <tt>U+0027</tt>)."			}	};	for (var i = 0; i < a.length; i++) {		var m = a[i];		var ipaText = m[2];		for (var j = 0; j < ipaText.length; j++) {			var ch = ipaText[j]; if (ipaSubstitions[ch] != null) { b.push({						start: m.start + m[1].length + j,						end: m.start + m[1].length + j + 1,						replacement: ipaSubstitions[ch].replacement,						name: 'IPA character',						description: "Replace ``false friend with the correct IPA character",						help: 'The correct IPA character '							+ ct.hlink('WP:IPA#Entering_IPA_characters', 'should be used')							+ " instead of its ``false friend."							+ ' Unicode contains a reserved range of characters for '							+ ct.hlink('International Phonetic Alphabet', 'IPA')							+ ' transcription. Some of them look very similar to other, '							+ 'more commonly used, alphabetic or punctuation characters (' + ct.hlink('False friend', 'false friends') + ').' + (ipaSubstitions[ch].additionalHelp || '')				}); }		}	}	return b; });

ct.rules.push(function (s) {	var re = /&#(([1-9][0-9]{0,4})|x([a-fA-F0-9]{1,4}));/g;	var a = ct.getAllMatches(re, s);	var b = [];	for (var i = 0; i < a.length; i++) {		var m = a[i];		var charCode = (m[2]) ? parseInt(m[2]) : parseInt(m[3], 16);		if ((charCode < 128) || (charCode > 0xffff)) {			continue;		}		var ch = String.fromCharCode(charCode);		var chHex = charCode.toString(16).toUpperCase;		chHex = '0000'.substring(chHex.length) + chHex;		b.push({ start: m.start, end: m.end, replacement: ch, name: 'unicode-escape', description: 'Replace with an inline Unicode character', help: ct.hlink('WP:EDIT#Character_formatting', 'HTML-style escapes') + " like ``<tt>&amp;#" + m[1] + ";</tt>'' can be written inline using a Unicode character&mdash;in this case ``" + ch + "'' (<tt>U+" + chHex + "</tt>)." });	}	return b; });

ct.rules.push(function (s) {	var re = /&([A-Za-z]+);/g;	var a = ct.getAllMatches(re, s);	var b = [];	// Use a DOM element and its innerHTML property to do	// the unescaping, let the browser do the dirty job.	var e = document.createElement('DIV');	for (var i = 0; i < a.length; i++) {		var m = a[i];		if (m[1] == 'nbsp') {			// Opera incorrectly replaces nbsp-s with regular spaces:			// http://en.wikipedia.org/w/index.php?title=User_talk%3ACameltrader&diff=179233698&oldid=175946199			continue;		}		e.innerHTML = m[0];		var ch = e.innerHTML;		if (ch.length != 1) {			// The entity is not a single Unicode character---ignore it			continue;		}		var chHex = ch.charCodeAt(0).toString(16).toUpperCase;		chHex = '0000'.substring(chHex.length) + chHex;		b.push({ start: m.start, end: m.end, replacement: e.innerHTML, // the entity, unescaped name: 'HTML entity', description: 'Replace with an inline Unicode character', help: ct.hlink('WP:EDIT#Character_formatting', 'HTML-style escapes') + " like ``<tt>&amp;" + m[1] + ";</tt>'' can be written inline using a Unicode character&mdash;in this case ``" + ch + "'' (<tt>U+" + chHex + "</tt>)." });	}	return b; });

ct.rules.push(function (s) {	var a = ct.getAllMatches(/\u2026/g, s); // ellipsis	var b = [];	for (var i = 0; i < a.length; i++) {		var m = a[i];		b.push({ start: m.start, end: m.end, replacement: '...', name: 'ellipsis', description: 'Replace ellipsis with three periods/full stops', help: "The ellipsis character (``\u2026'', U+2026) should be replaced with " + "three periods/full stops per " + ct.hlink('WP:MOS#Ellipses') });	}	return b; });

// === Template usage rules ===

ct.rules.push(function (s) {	// Initialise statics	var _static = arguments.callee;	if (_static.LANGUAGE_MAP == null) {		_static.LANGUAGE_MAP = { // : Hashtable<String, String>			// From http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes			// Note, that not all of these have a lang-xx template, but finding a reference			// to such a language is a good reason to create the template.			aa: 'Afar', ab: 'Abkhazian', ae: 'Avestan', af: 'Afrikaans', ak: 'Akan', am: 'Amharic', an: 'Aragonese', ar: 'Arabic',			as: 'Assamese', av: 'Avaric', ay: 'Aymara', az: 'Azerbaijani', ba: 'Bashkir', be: 'Belarusian', bg: 'Bulgarian',			bh: 'Bihari', bi: 'Bislama', bm: 'Bambara', bn: 'Bengali', bo: 'Tibetan', br: 'Breton', bs: 'Bosnian', ca: 'Catalan',			ce: 'Chechen', ch: 'Chamorro', co: 'Corsican', cr: 'Cree', cs: 'Czech', cu: 'Church Slavic', cv: 'Chuvash', cy: 'Welsh',			da: 'Danish', de: 'German', dv: 'Divehi', dz: 'Dzongkha', ee: 'Ewe', el: 'Greek', en: 'English', eo: 'Esperanto', es: 'Spanish', et: 'Estonian', eu: 'Basque', fa: 'Persian', ff: 'Fulah', fi: 'Finnish', fj: 'Fijian', fo: 'Faroese', fr: 'French', fy: 'Western Frisian', ga: 'Irish', gd: 'Gaelic', gl: 'Galician', gn: 'Guaran\u00ed', gu: 'Gujarati', gv: 'Manx', ha: 'Hausa', he: 'Hebrew', hi: 'Hindi', ho: 'Hiri Motu', hr: 'Croatian', ht: 'Haitian', hu: 'Hungarian', hy: 'Armenian', hz: 'Herero', ia: 'Interlingua (International Auxiliary Language Association)', id: 'Indonesian', ie: 'Interlingue', ig: 'Igbo', ii: 'Sichuan Yi', ik: 'Inupiaq', io: 'Ido', is: 'Icelandic', it: 'Italian', iu: 'Inuktitut', ja: 'Japanese', jv: 'Javanese', ka: 'Georgian', kg: 'Kongo', ki: 'Kikuyu', kj: 'Kuanyama', kk: 'Kazakh', kl: 'Kalaallisut', km: 'Khmer', kn: 'Kannada', ko: 'Korean', kr: 'Kanuri', ks: 'Kashmiri', ku: 'Kurdish', kv: 'Komi', kw: 'Cornish', ky: 'Kirghiz', la: 'Latin', lb: 'Luxembourgish', lg: 'Ganda', li: 'Limburgish', ln: 'Lingala', lo: 'Lao', lt: 'Lithuanian', lu: 'Luba-Katanga', lv: 'Latvian', mg: 'Malagasy', mh: 'Marshallese', mi: 'M\u0101ori', mk: 'Macedonian', ml: 'Malayalam', mn: 'Mongolian', mo: 'Moldavian', mr: 'Marathi', ms: 'Malay', mt: 'Maltese', my: 'Burmese', na: 'Nauru', nb: 'Norwegian Bokm\u00e5l', nd: 'North Ndebele', ne: 'Nepali', ng: 'Ndonga', nl: 'Dutch', nn: 'Norwegian Nynorsk', no: 'Norwegian', nr: 'South Ndebele', nv: 'Navajo', ny: 'Chichewa', oc: 'Occitan', oj: 'Ojibwa', om: 'Oromo', or: 'Oriya', os: 'Ossetian', pa: 'Panjabi', pi: 'P\u0101li', pl: 'Polish', ps: 'Pashto', pt: 'Portuguese', qu: 'Quechua', rm: 'Raeto-Romance', rn: 'Kirundi', ro: 'Romanian', ru: 'Russian', rw: 'Kinyarwanda', sa: 'Sanskrit', sc: 'Sardinian', sd: 'Sindhi', se: 'Northern Sami', sg: 'Sango', sh: 'Serbo-Croatian', si: 'Sinhala', sk: 'Slovak', sl: 'Slovenian', sm: 'Samoan', sn: 'Shona', so: 'Somali', sq: 'Albanian', sr: 'Serbian', ss: 'Swati', st: 'Southern Sotho', su: 'Sundanese', sv: 'Swedish', sw: 'Swahili', ta: 'Tamil', te: 'Telugu', tg: 'Tajik', th: 'Thai', ti: 'Tigrinya', tk: 'Turkmen', tl: 'Tagalog', tn: 'Tswana', to: 'Tonga', tr: 'Turkish', ts: 'Tsonga', tt: 'Tatar', tw: 'Twi', ty: 'Tahitian', ug: 'Uighur', uk: 'Ukrainian', ur: 'Urdu', uz: 'Uzbek', ve: 'Venda', vi: 'Vietnamese', vo: 'Volap\u00fck', wa: 'Walloon', wo: 'Wolof', xh: 'Xhosa', yi: 'Yiddish', yo: 'Yoruba', za: 'Zhuang', zh: 'Chinese', zu: 'Zulu' };		_static.REVERSE_LANGUAGE_MAP = {}; // : Hashtable<String, String> for (var i in _static.LANGUAGE_MAP) { _static.REVERSE_LANGUAGE_MAP[_static.LANGUAGE_MAP[i]] = i;		} }

// U+201e and U+201c are opening and closing double quotes // U+2013 and U+2014 are an ndash and an mdash var re = /\[\[(\w+) language\|\1\]\] *: (\'+)*([{letter} \"\'\u201e\u201c\/\u2014\u2013\-]+)(?:\2)/g;	re = ct.fixRegExp(re);	var a = ct.getAllMatches(re, s);	var b = [];	for (var i = 0; i < a.length; i++) {		var m = a[i];		if (_static.REVERSE_LANGUAGE_MAP[m[1]] == null) {			continue;		}		var code = _static.REVERSE_LANGUAGE_MAP[m[1]];		// Markers for italics and bold are stripped off		b.push({				start: m.start,				end: m.end,				replacement: ,				name: 'lang-' + code,				description: 'Apply the template',				help: 'The <tt>' + ct.hlink('Template:lang-' + code, )					+ '</tt> template can be applied for this text.'					+ ' Similar templates are available in the '					+ ct.hlink('Category:Multilingual_support_templates', 'multilingual support templates category')					+ '.'		});	}	return b; });

ct.rules.push(function (s) {	var re = /^[ ':]*(?:Main +article)[ ']*:[ ']*\[\[([^\]]+)\]\][ ']*$/mig;	var a = ct.getAllMatches(re, s);	var b = [];	for (var i = 0; i < a.length; i++) {		var m = a[i];		if ((m[1] != null) && (m[1] != "")) {			b.push({ start: m.start, end: m.end, replacement: '', name: 'template-main', description: 'Use the template', help: 'Template <tt>' + ct.hlink('Template:Main', '') + '</tt> can be used in this place.' });		}	}	return b; });

ct.rules.push(function (s) {	var exceptions = {};	var wgTitle = mw.config.get('wgTitle') || '';	if (exceptions[wgTitle]) {		return [];	}	var re0 = /^([{letter}\-]+(?: [{letter}\-]+\.?)?) ([{letter}\-]+(?:ov|ev|ski))$/;	re0 = ct.fixRegExp(re0);	var m0 = re0.exec(wgTitle);	if (m0 == null) {		return [];	}	if (s.indexOf('DEFAULTSORT:') != -1) {		return [];	}	var firstNames = m0[1];	var lastName = m0[2];	var re1 = new RegExp( '\\[\\[(Category:[\\w _\\(\\),\\-]+)\\| *' + ct.escapeRegExp(lastName) + ', *' + ct.escapeRegExp(firstNames) + ' *\\]\\]', 'gi' );	var a = ct.getAllMatches(re1, s);	if (a.length == 0) {		return [];	}	var aStart = a[0].start;	var aEnd = a[a.length - 1].end;	var original = s.substring(aStart, aEnd);	var replacement = '\n'	               + original.replace(re1, '$1');	return [{			start: aStart,			end: aEnd,			replacement: replacement,			name: 'default-sort',			description: 'Use DEFAULTSORT to specify the common sort key',			help: 'The <tt>' + ct.hlink('Help:Categories#Default_sort_key', 'DEFAULTSORT')				+ '</tt> magic word can be used to specify sort keys for categories.  It was '				+ ct.hlink('Wikipedia:Wikipedia_Signpost/2007-01-02/Technology_report', 'announced in January 2007')				+ '.'	}]; });

ct.rules.push(function (s) {	var wgTitle = mw.config.get('wgTitle') || ;	var reTitle = /^(a|an|the|le|lo|l\'|l\’|el|la|los|las|les|de|den|der|das|die|det|o|os|um|uma|as|y|yn|yr|het|i|te|il) (.*)$/i;	if (!reTitle.test(wgTitle) || (s.indexOf('DEFAULTSORT') !== -1)) {		return [];	}	var a = ct.getAllMatches(/(\[\[)[Cc]ategory:[^\]]+\]\]/g, s);	if (a.length === 0) {		return [];	}	var mTitle = ct.getAllMatches(reTitle, wgTitle)[0]; // the match object for the title	var article = mTitle[1];	var nounPhrase = mTitle[2];	var highlightStart = a[0].start;	var highlightEnd = a[a.length - 1].end;	return [{			start: highlightStart,			end: highlightEnd,			replacement: '\n'						+ s.substring(highlightStart, highlightEnd),			name: 'defaultsort-' + article.toLowerCase,			description: 'Add DEFAULTSORT',			help: "Articles starting with ``a or ``the'' should participate in categories without the first word." }]; });

ct.rules.push(function (s) {	var wgTitle = mw.config.get('wgTitle') || ;	var reTitle = /^(l\'|al-|an-|ha-)(.*)$/i;	if (!reTitle.test(wgTitle) || (s.indexOf('DEFAULTSORT') !== -1)) {		return [];	}	var a = ct.getAllMatches(/(\[\[)[Cc]ategory:[^\]]+\]\]/g, s);	if (a.length === 0) {		return [];	}	var mTitle = ct.getAllMatches(reTitle, wgTitle)[0]; // the match object for the title	var article = mTitle[1];	var nounPhrase = mTitle[2].replace(/&/g, "and");	var highlightStart = a[0].start;	var highlightEnd = a[a.length - 1].end;	return [{			start: highlightStart,			end: highlightEnd,			replacement: '\n'						+ s.substring(highlightStart, highlightEnd),			name: 'defaultsort-' + article.toLowerCase,			description: 'Add DEFAULTSORT',			help: "Articles starting with ``a or ``the'' should participate in categories without the first word."	}]; });

ct.rules.push(function (s) {	var re = /(\{\{\s*)DEFAULTSORT\s*\|/g;	var a = ct.getAllMatches(re, s);	var b = [];	for (var i = 0; i < a.length; i++) {		var m = a[i];		b.push({ start: m.start, end: m.end, replacement: m[1] + 'DEFAULTSORT:', name: 'default-sort-magic-word', description: 'Replace the template with a magic word', help: 'Usage of the <tt></tt> template is discouraged. The magic word with the same name should be used instead.' });	}	return b; });

// === Other rules ===

ct.rules.push(function (s) {	var re = /^(?: *)(==+)( *)([^=]*[^= ])( *)\1/gm;	var a = ct.getAllMatches(re, s);	if (a.length == 0) {		return [];	}	var b = [];	var level = 0; // == Level 1 ==, === Level 2 ===, ==== Level 3 ====, etc.	var editform = document.getElementById('editform');	// If we are editing a section, we have to be tolerant to the first heading's level	var isSection = editform &&	               (editform['wpSection'] != null) &&	                (editform['wpSection'].value != '');	// Count spaced and non-spaced headings to find out the majority	var counters = {spaced: 0, nonSpaced: 0, unclear: 0};	for (var i = 0; i < a.length; i++) {		var m = a[i];		counters[(!m[2] && !m[4]) ? 'nonSpaced' : (m[2] && m[4]) ? 'spaced' : 'unclear']++;	}	var predominantSpacingStyle;	if (counters.spaced > counters.nonSpaced) {		predominantSpacingStyle = 'spaced';	} else if (counters.spaced < counters.nonSpaced) { predominantSpacingStyle = 'nonSpaced'; } else { predominantSpacingStyle = 'unclear'; // We cannot decide which spacing style is predominant, // so we show a suggestion attached to the first heading, // recommending consistent spacing: b.push({				start: a[0].start,				end: a[0].end,				replacement: null,				name: 'heading',				description: 'Consider using consistent heading spacing',				help: 'Heading style should be either '					+ "``<tt>== Heading ==</tt> or ``<tt>==Heading==</tt>. "					+ "Headings in this article use an equal number of both.  "					+ "Consider choosing a heading style and using it consistently."		}); }	var titleSet = {}; // a set of title names, will be used to detect duplicates for (var i = 0; i < a.length; i++) { var m = a[i]; if (m[2] != m[4]) { var spacer = (predominantSpacingStyle == 'spaced') ? ' ' : (predominantSpacingStyle == 'nonSpaced') ? '' : m[2]; b.push({					start: m.start,					end: m.end,					replacement: m[1] + spacer + m[3] + spacer + m[1],					name: 'heading',					description: 'Fix whitespace',					help: 'Heading style should be either '						+ "``<tt>== Heading ==</tt> or ``<tt>==Heading==</tt>."			}); } else if ((m[2] && (predominantSpacingStyle == 'nonSpaced'))		      || (!m[2] && (predominantSpacingStyle == 'spaced'))) { var spacer = (m[2]) ? '' : ' ';			b.push({					start: m.start,					end: m.end,					replacement: m[1] + spacer + m[3] + spacer + m[1],					name: 'heading-style',					description: 'Conform to the existing majority of '						+ ((m[2]) ? 'non-spaced' : 'spaced') + ' headings',					help: 'There are two styles of writing headings in wikitext:<tt><ul><li>== Spaced ==<li>==Non-spaced==</ul>'						+ 'Most of the headings in this article are '						+ ((m[2]) ? 'non-spaced' : 'spaced')						+ ' (' + counters.spaced + ' vs ' + counters.nonSpaced + ').  '						+ 'It is recommended that you adapt your style to the majority.'			}); }		var oldLevel = level; level = m[1].length - 1; if ( (level - oldLevel > 1) && (!isSection || (oldLevel > 0)) ) { var h = '======='.substring(0, oldLevel + 2); b.push({					start: m.start,					end: m.end,					replacement: h + m[2] + m[3] + m[2] + h,					name: 'heading-nesting',					description: 'Fix improper nesting',					help: 'A heading ' + ct.hlink('WP:MOS#Section_headings', 'should be')						+ ' nested one level deeper than its parent heading.'			}); }		var frequentMistakes = [ { code: 'see-also', wrong: /^see *al+so$/i,          correct: 'See also' }, { code: 'ext-links', wrong: /^external links?$/i,    correct: 'External links' }, { code: 'refs',     wrong: /^ref+e?r+en(c|s)es?$/i,  correct: 'References' } ];		for (var j = 0; j < frequentMistakes.length; j++) { var fm = frequentMistakes[j]; if (fm.wrong.test(m[3]) && (m[3] != fm.correct)) { var r = m[1] + m[2] + fm.correct + m[2] + m[1]; if (r != m[0]) { b.push({							start: m.start,							end: m.end,							replacement: r,							name: fm.code,							description: 'Change to ``' + fm.correct + ".",							help: 'The correct spelling/capitalisation is ``<tt>' + fm.correct + "</tt>."					}); }			}		}		if (titleSet[m[3]] != null) { b.push({					start: m.start + (m[1] || ).length + (m[2] || ).length,					end: m.start + (m[1] || ).length + (m[2] || ).length + m[3].length,					replacement: null, // we cannot propose anything, it's the editor who has to choose a different title					name: 'duplicate-title',					description: 'Avoid duplicate section titles',					help: 'Section names '						+ ct.hlink('WP:MOS#Section_headings', 'should preferably be unique')						+ ' within a page; this applies even for the names of subsections.'			}); }		titleSet[m[3]] = true; }	return b; });

ct.rules.push(function (s) {	// U+2013 and U+2014 are an ndash and an mdash	var re = /\( *(?:b\.? *)?({year}) *(?:[\-\\u2013\\u2014]|–|&mdash;|--) *\)/g;	var a = ct.getAllMatches(re, s);	for (var i = 0; i < a.length; i++) {		var m = a[i];		a[i] = {				start: m.start,				end: m.end,				replacement: '(born ' + m[1] + ')',				name: 'born',				description: 'The word \'born\' should be fully written.',				help: 'According to '					+ ct.hlink('WP:DATE#Dates_of_birth_and_death', 'WP:DATE')					+ ', the word born should be fully written.'		};	}	return a; });

ct.rules.push(function (s) {	var a = ct.getAllMatches(/\[\[FOX\]\]/g, s); 	var b = [];	for (var i = 0; i < a.length; i++) {		var m = a[i];		b.push({ start: m.start, end: m.end, replacement: '\[\[Fox Broadcasting Company\|Fox\]\]', name: 'Fox is not an acronym', description: 'Fox is not an acronym', help: "" });	}	return b; });

ct.rules.push(function (s) {	var a = ct.getAllMatches(/[Oo]fficial [Ww]ebsite\]/g, s); 	var b = [];	for (var i = 0; i < a.length; i++) {		var m = a[i];		b.push({ start: m.start, end: m.end, replacement: '\{\{official website\|', name: 'official website', description: 'official website', help: "" });	}	return b; });

ct.rules.push(function (s) {	var a = ct.getAllMatches(/[Aa]llmusic/g, s); 	var b = [];	for (var i = 0; i < a.length; i++) {		var m = a[i];		b.push({ start: m.start, end: m.end, replacement: 'AllMusic', name: 'AllMusic', description: 'AllMusic', help: "" });	}	return b; });

ct.rules.push(function (s) {	// ISBN: ten or thirteen digits, each digit optionally followed by a hyphen, the last digit can be 'X' or 'x'	var a = ct.getAllMatches(/ISBN *=? *(([0-9Xx]-?)+)/gi, s);	var b = [];	for (var i = 0; i < a.length; i++) {		var m = a[i];		var s = m[1].replace(/[^0-9Xx]+/g, '').toUpperCase; // remove all non-digits		if ((s.length !== 10) && (s.length !== 13)) {			b.push({ start: m.start, end: m.end, name: 'ISBN', replacement: m[1]+" ", description: 'Should be either 10 or 13 digits long', help: 'ISBN numbers should be either 10 or 13 digits long. '							+ 'This one consists of ' + s.length + ' digits: <tt>' + m[1] + '</tt>' });			continue;		}		var isNew = (s.length === 13); // old (10 digits) or new (13 digits)		var xIndex = s.indexOf('X');		if ((xIndex !== -1) && ((xIndex !== 9) || isNew)) {			b.push({ start: m.start, end: m.end, name: 'ISBN', replacement: "", description: 'Improper usage of X as a digit', help: "``<tt>X</tt>'' can only be used in 10-digit ISBN numbers " + ' as the last digit: <tt>' + m[1] + '</tt>' });			continue;		}		var computedChecksum = 0;		var modulus = (isNew) ? 10 : 11;		for (var j = s.length - 2; j >= 0; j--) {			var digit = s.charCodeAt(j) - 48; // 48 is the ASCII code of '0'			var quotient = (isNew)								? ((j & 1) ? 3 : 1) // the new way: 1 for even, 3 for odd								: (10 - j);        // the old way: 10, 9, 8, etc			computedChecksum = (computedChecksum + (quotient * digit)) % modulus;		}		computedChecksum = (modulus - computedChecksum) % modulus;		var c = s.charCodeAt(s.length - 1) - 48;		var actualChecksum = ((c < 0) || (9 < c)) ? 10 : c;		if (computedChecksum === actualChecksum) {			continue;		}		b.push({ start: m.start, end: m.end, name: 'ISBN', replacement: "", description: 'Bad ISBN checksum', help: 'Bad ISBN checksum for <tt>' + m[1] + '</tt> ' });	}	return b; });

ct.rules.push(function (s) {	var a = ct.getAllMatches(/\[htt(p|ps)\:\/\/(www.imdb|imdb).com\/title\/tt/g, s); 	var b = [];	for (var i = 0; i < a.length; i++) {		var m = a[i];		b.push({ start: m.start, end: m.end, replacement: '\{\{IMDb title\|', name: 'IMDb-title', description: 'IMDb', help: "use IMDb template" });	}       	return b; });

ct.rules.push(function (s) {	var a = ct.getAllMatches(/\[htt(p|ps)\:\/\/(www.imdb|imdb).com\/company\/co/g, s); 	var b = [];	for (var i = 0; i < a.length; i++) {		var m = a[i];		b.push({ start: m.start, end: m.end, replacement: '\{\{IMDb company\|', name: 'IMDb-company', description: 'IMDb', help: "use IMDb template" });	}       	return b; });

ct.rules.push(function (s) {	var a = ct.getAllMatches(/Blu-Ray/g, s); 	var b = [];	for (var i = 0; i < a.length; i++) {		var m = a[i];		b.push({ start: m.start, end: m.end, replacement: 'Blu-ray', name: 'Blu-ray', description: 'Blu-ray', help: "trademarked" });	}       	return b; });

} // end if (mw.config.get('wgContentLanguage') === 'en') // end of default rules //