Jump to content

User:David Condrey/editing/scripts.js

From Wikipedia, the free encyclopedia
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.
/*<nowiki>*/
/* Links */
var LinkClassifier={
    /* This object maps classes to the categories for which to apply them. Values may be an array of strings or a regex. */
    cats:{
        deletion:[
            'Category:All articles proposed for deletion',
            'Category:All books proposed for deletion',
            'Category:All disputed non-free Wikipedia files',
            'Category:All orphaned non-free use Wikipedia files',
            'Category:All possibly unfree Wikipedia files',
            'Category:All replaceable non-free use Wikipedia files',
            'Category:All Wikipedia files with no copyright tag',
            'Category:All Wikipedia files with no non-free use rationale',
            'Category:All Wikipedia files with unknown copyright status',
            'Category:All Wikipedia files with unknown source',
            'Category:Articles for deletion',
            'Category:Articles on deletion review',
            'Category:Candidates for speedy deletion',
            'Category:Candidates for undeletion',
            'Category:Categories for conversion',
            'Category:Categories for deletion',
            'Category:Categories for listifying',
            'Category:Categories for merging',
            'Category:Categories for renaming',
            'Category:Categories for speedy renaming',
            'Category:Categories to be listified then deleted',
            'Category:Duplicate or hardcoded templates awaiting deletion',
            'Category:Items pending OTRS confirmation of permission for over 30 days',
            'Category:Miscellaneous pages for deletion',
            'Category:Redirects for discussion',
            'Category:Stub categories for deletion',
            'Category:Stub template deletion candidates',
            'Category:Templates for deletion',
            'Category:Wikipedia deprecated and orphaned templates',
            'Category:Wikipedia files for deletion',
            'Category:Wikipedia files with unknown source for deletion in dispute',
            'Category:Wikipedia templates for deletion'
        ].sort(),
        disambiguation:[
            'Category:All disambiguation pages'
        ].sort(),
        'set-index':[
            'Category:All set index articles'
        ].sort(),
        'featured-content':[
            'Category:Featured articles',
            'Category:Featured lists',
            'Category:Featured pictures',
            'Category:Featured sounds',
            'Category:Featured videos',
            'Category:Featured portals'
        ].sort(),
        'good-content':[
            'Category:Good articles'
        ].sort(),
        'soft-redirect-cats':[
            'Category:Wikipedia soft redirected categories',
        ].sort(),
        'spoken-articles':[
            'Category:Spoken articles'
        ].sort(),
        stubcls:/^Category:.* stubs$/,
        'nonfree-media':[
            'Category:All non-free media'
        ].sort(),
        unprintworthy:[
            'Category:Unprintworthy redirects',
            'Category:Middle-earth redirects from redundant titles'
        ].sort(),
        'unprintworthy-shortcut':[
            'Category:Redirects from shortcuts',
        ].sort()
    },

    /* This object maps page props to CSS classes for which to apply them. Values may be an array of strings or a function returning such. */
    props: {
        disambiguation:[
            'disambiguation'
        ],
    },

    /* This regex matches page titles to be marked as intentional links to disambiguation pages */
    intentionaldab: / \(disambiguation\)$/,
 
    callback:function(r, sts, xhr){
        if(!r.query) {
            if(typeof(window.console)=='undefined' || typeof(window.console.error)!='function')
                throw new Error('Bad response');
            window.console.error("Bad response", r);
            return;
        }
        if(r['query-continue']){
            var cc=this.rawdata;
            for(var k in r['query-continue']){
                for(var k2 in r['query-continue'][k]){
                    cc[k2]=r['query-continue'][k][k2];
                }
            }
            $.ajax({
                url:mw.util.wikiScript('api'),
                dataType:'json',
                type:'POST',
                data:cc,
                rawdata:cc,
                success:arguments.callee,
                error:function(xhr,textStatus,errorThrown){
                    throw new Error('AJAX error: '+textStatus+' '+errorThrown);
                }
            });
        }
        r=r.query;

        var a=document.getElementById('wikiPreview');
        if(!a) a=document.getElementById('bodyContent');
        if(!a) throw new Error('Huh? No body content?');
        a=a.getElementsByTagName('A');
        if(a.length==0) return;

        var redir={};
        var redirlist=[];
        if(r.redirects) for(var i=r.redirects.length-1; i>=0; i--){
            redir[r.redirects[i].from]=r.redirects[i].to;
            redirlist.push(r.redirects[i].from);
        }
        if(redirlist.length>0) {
            var q = {
                format:'json',
                action:'query',
                titles:redirlist.join('|'),
                prop:'categories|info',
                inprop:'protection',
                cllimit:'max'
            };
            $.ajax({
                url:mw.util.wikiScript('api'),
                dataType:'json',
                type:'POST',
                data:q,
                rawdata:q,
                success:arguments.callee,
                error:function(xhr,textStatus,errorThrown){
                    throw new Error('AJAX error: '+textStatus+' '+errorThrown);
                }
            });
        }

        var prefix=(this.rawdata.redirects?'':'redir-');
        var cats={};
        var missing={};
        var classes={};
        if(r.pages) for(var i in r.pages){
            classes[r.pages[i].title] = [];
            missing[r.pages[i].title]=(typeof(r.pages[i].missing)!='undefined');
            if(typeof(r.pages[i].categories)!='undefined'){
                cats[r.pages[i].title]=r.pages[i].categories.map(function(a){ return a.title; }).sort();
            }
            if(typeof(r.pages[i].pageprops)!='undefined'){
                for ( var k in r.pages[i].pageprops ) {
                        if ( !LinkClassifier.props[k] ) {
                                continue;
                        }
                        var v = LinkClassifier.props[k];
                        if ( $.isFunction( v ) ) {
                                v = v( r.pages[i].pageprops[k], k, r.pages[i].title );
                        }
                        classes[r.pages[i].title].push.apply( classes[r.pages[i].title], v );
                }
            }
            if(typeof(r.pages[i].protection)!='undefined'){
                var x={};
                for(var j=r.pages[i].protection.length-1; j>=0; j--){
                    var p=prefix+'protection-'+r.pages[i].protection[j].type+'-'+r.pages[i].protection[j].level;
                    if(typeof(x[p])=='undefined'){
                        x[p]=1;
                        classes[r.pages[i].title].push(p);
                    }
                    if(r.pages[i].protection[j].expiry=='infinity'){
                        p+='-indef';
                        if(typeof(x[p])=='undefined'){
                            x[p]=1;
                            classes[r.pages[i].title].push(p);
                        }
                    }
                }
            }
            if(typeof(r.pages[i].flagged)!='undefined'){
                if(r.pages[i].lastrevid!=r.pages[i].flagged.stable_revid){
                    classes[r.pages[i].title].push('needs-review');
                }
            }
        }
        Array.prototype.forEach.call(a, function(a){
            if(typeof(a.wikipage)=='undefined') return;
            if(typeof(redir[a.wikipage])!='undefined'){
                $(a).addClass('redirect');
                a.wikipage=redir[a.wikipage];
                a.title=a.wikipage;
                var cns=mw.config.get('wgCanonicalNamespace');
                if(a.wikipage==(cns?cns+':':'')+mw.config.get('wgTitle'))
                    $(a).addClass('self-redirect');
                if(missing[a.wikipage])
                    $(a).addClass('broken-redirect');
            }
            var m=a.href.match(/#.*/);
            if(m && m[0].substr(0,10)!=="#cite_note"){
                a.title=a.title.replace(/#.*/,'')+m[0].replace(/_/g,' ').replace(/\.([0-9A-F][0-9A-F])/gi, function(x,n){ return String.fromCharCode(parseInt(n,16)); });
            }
            if(LinkClassifier.intentionaldab.test(a.origwikipage)){
                $(a).addClass('intentional-disambiguation');
            }
            if(typeof(classes[a.wikipage])!='undefined'){
                for(var j=classes[a.wikipage].length-1; j>=0; j--)
                    $(a).addClass(classes[a.wikipage][j]);
            }
            if(a.wikipage!=a.origwikipage && typeof(classes[a.origwikipage])!='undefined'){
                for(var j=classes[a.origwikipage].length-1; j>=0; j--)
                    $(a).addClass(classes[a.origwikipage][j]);
            }
            var c1=[];
            if(typeof(cats[a.wikipage])!='undefined'){
                c1=c1.concat(cats[a.wikipage]);
            }
            if(a.wikipage!=a.origwikipage && typeof(cats[a.origwikipage])!='undefined'){
                c1=c1.concat(cats[a.origwikipage]);
            }
            if(c1.length>0){
                c1=c1.sort();
                for(var cls in LinkClassifier.cats){
                    var i1=c1.length-1;
                    var c2=LinkClassifier.cats[cls];
                    if(c2 instanceof RegExp){
                        while(i1>=0){
                            if(c2.test(c1[i1])){
                                $(a).addClass(cls);
                                break;
                            }
                            i1--;
                        }
                    } else {
                        var i2=c2.length-1;
                        while(i1>=0 && i2>=0){
                            if(c1[i1]==c2[i2]){
                                $(a).addClass(cls);
                                break;
                            }
                            (c1[i1]>c2[i2])?--i1:--i2;
                        }
                    }
                }
            }
        });
    },

    getPageName:function(url){
        var m=url.match(/\/wiki\/([^?#]+)/);
        if(!m) m=url.match(/\/w\/index.php\?(?:.*&)?title=([^&#]+)/);
        if(!m) return '';
        var t=decodeURIComponent(m[1]).replace(/_/g,' ');
        if(t.substr(0,6)=='Image:') t='File:'+t.substr(6);
        if(t.substr(0,11)=='Image talk:') t='File talk:'+t.substr(6);
        if(t.substr(0,8)=='Special:') t='';
        return t;
    },

    classifyChildren:function(node){
        mw.loader.using(['mediawiki.util','mediawiki.user'], function(){
            var a=node.getElementsByTagName('A');
            if(a.length==0) return;
            var self=LinkClassifier.getPageName(location.href);
            a=Array.prototype.map.call(a, function(a){
                a.wikipage='';
                if(/(^|\s)(external|extiw)(\s|$)/.test(a.className)) return '';
                if(!/(^|\s)(image)(\s|$)/.test(a.className)) a.className+=" nonimage";
                a.wikipage=LinkClassifier.getPageName(a.href);
                if(a.wikipage==self) a.wikipage='';
                a.origwikipage=a.wikipage;
                return a.wikipage;
            }).sort().filter(function(e,i,a){
                return e!=='' && (i==0 || a[i-1]!==e);
            });

            function processLinks(limit){
                var props = [];
                for ( var k in LinkClassifier.props ) {
                        props.push( k );
                }
                while(a.length>0){
                    var q={
                        format:'json',
                        action:'query',
                        rawcontinue:'',
                        titles:a.splice(0,limit).join('|'),
                        prop:'categories|pageprops|info|flagged',
                        redirects:1,
                        cllimit:'max',
                        inprop:'protection'
                    };
                    if ( props.length <= limit ) {
                        q.ppprop = props.join( '|' );
                    }
                    $.ajax({
                        url:mw.util.wikiScript('api'),
                        dataType:'json',
                        type:'POST',
                        data:q,
                        rawdata:q,
                        success:LinkClassifier.callback,
                        error:function(xhr,textStatus,errorThrown){
                            throw new Error('AJAX error: '+textStatus+' '+errorThrown);
                        }
                    });
                }
            }

            if(a.length<=100){
                // Not worth querying the API to see if the user has apihighlimits
                processLinks(50);
            } else {
                // Note mw.user.getRights queries the API
                mw.user.getRights(function(rights){
                    processLinks( (rights.indexOf('apihighlimits')>=0) ? 500 : 50 );
                });
            }
        });
    },
 
    onLoad:function(){
        if(window.LinkClassifierOnDemand) return;
        if(window.AJAXPreview) window.AJAXPreview.AddOnLoadHook(LinkClassifier.classifyChildren);
        LinkClassifier.onDemand();
    },
 
    onDemand:function(){
        var node=document.getElementById('wikiPreview');
        if(!node) node=document.getElementById('bodyContent');
        if(node) LinkClassifier.classifyChildren(node);
    }
};

if(!window.LinkClassifierOnDemand) $(document).ready(LinkClassifier.onLoad);


/* Dup Links */
$( function($) {
    if((wgNamespaceNumber != 0) && (wgNamespaceNumber != 2)) {
        // only check links in mainspace and userspace (for userspace drafts)
        return;
    }
    var portletlink = mw.util.addPortletLink('p-tb', '#', 'Highlight duplicate links', 'ca-findduplicatelinks');
    $(portletlink).click( function(e) {
        e.preventDefault();
        // create a separate div surrounding the lead
        // first get the element immediately surrounding the article text. Unfortunately, MW doesn't seem to provide a non-fragile way for that.
        var content = ".mw-content-ltr";
        $(content).prepend(document.createElement('div'));
        var lead = $(content).children()[0];
        $(lead).attr('id', 'lead');
        $(content).children().each( function() {
            if(this.nodeName.toLowerCase() == 'h2') {
                return false;
            }
            if($(this).attr('id') != 'lead') {
                $(lead).append(this);
            }
            return true;
        });
       
        // detect duplicate links
        mw.util.addCSS(".duplicate-link { border: 1px solid red; }");
        var finddups = function() {
            var href = $(this).attr('href');
            if(href != undefined && href.indexOf('#') != 0) {
                if(seen[href]) {
                    $(this).addClass("duplicate-link");
                }
                else {
                    seen[href] = true;
                }
            }
            return true;
        };
        // array to keep track of whether we've seen a link before
        var seen = [];
        mw.util.$content.find('p a').not('#lead *, .infobox *, .navbox *').each(finddups);
        var seen = [];
        mw.util.$content.find('#lead p a').not('.infobox *, .navbox *').each(finddups);
    });
});

/* Edit Buttons */
if(typeof XEBPopups== 'undefined')XEBPopups=true;
if(typeof XEBHideDelay== 'undefined')XEBHideDelay=0.5; //Time before the popup disappears after the mouse moves out
if(typeof XEBExtendEditSummary == 'undefined')XEBExtendEditSummary=true; // Is the edit summary extended after a popup

function addCustomButton(imageFile, speedTip, tagOpen, tagClose, sampleText){
mwCustomEditButtons.push({
  "imageFile": imageFile,
  "speedTip": speedTip,
  "tagOpen": tagOpen,
  "tagClose": tagClose,
  "sampleText": sampleText});
}

if (typeof usersignature == 'undefined') var usersignature = '-- \~\~\~\~';

var Isrc='//upload.wikimedia.org/wikipedia/commons/';

var enExtraButtons=mwCustomEditButtons.length;


var BDict={
'A':['e/e9/Button_headline2.png','Secondary headline','\n===','===','Secondary headline'],
'B':['1/13/Button_enter.png','Line break','<br />','',''],
'C':['5/5f/Button_center.png','Center','<div style="text-align: center;">\n','\n<\/div>','Centred text'],
'D':['e/ea/Button_align_left.png','Left-Align','<div style="text-align: left; direction: ltr; margin-left: 1em;">\n','\n<\/div>','Left-aligned text'],
'D1':['a/a5/Button_align_right.png','Right-Align','<div style="text-align: right; direction: ltr; margin-left: 1em;">\n','\n<\/div>','Right-aligned text'],
'E':['0/04/Button_array.png','Table','\n{| class="wikitable" \n|- \n| 1 || 2\n|- \n| 3 || 4','\n|}\n',''],
'F':['1/1e/Button_font_color.png','Insert coloured text','<span style="color: ','">Coloured text<\/span>','ColourName'],
'FS':['1/1b/Button_miss_signature.png','Unsigned post','<small><span class="autosigned">—&nbsp;Preceding [[Wikipedia:Signatures|unsigned]] comment added by [[User:','|',']] ([[User talk:','|talk]] • [[Special:Contributions/','|contribs]]) date</span></small><!-- Template:Unsigned -->','user name or IP'],
'G':['9/9e/Btn_toolbar_gallery.png','Picture gallery',"\n<gallery>\nImage:","|[[M63]]\nImage:Mona Lisa.jpg|[[Mona Lisa]]\nImage:Truite arc-en-ciel.jpg|Eine [[Forelle ]]\n<\/gallery>",'M63.jpg'],
'H':['7/74/Button_comment.png','Comment',"<!--","-->",'Comment'],
'I1':['6/6a/Button_sup_letter.png','Superscript','<sup>','<\/sup>','Superscript text'],
'I2':['a/aa/Button_sub_letter.png','Subscript','<sub>','<\/sub>','Subscript text'],
'J1':['5/58/Button_small.png','Small','<small>','<\/small>','Small Text'],
'J2':['5/56/Button_big.png','Big text','<big>','<\/big>','Big text'],
'K':['b/b4/Button_category03.png','Category',"[[Category:","]]",'Category name'],
'L':['8/8e/Button_shifting.png','Insert tab(s)',':','',':'],
'M':['f/fd/Button_blockquote.png','Insert block of quoted text','<blockquote style="border: 1px solid blue; padding: 2em;">\n','\n<\/blockquote>','Block quote'],
'N':['4/4b/Button_nbsp.png','nonbreaking space','&nbsp;','',''],
'O':['2/23/Button_code.png','Insert code','<code>','<\/code>','Code'],
'P':['3/3c/Button_pre.png','Pre formatted Text','<pre>','<\/pre>','Pre formatted text'],
'P1':['9/93/Button_sub_link.png','Insert link to sub-page','[[','/Sub_Page]]','Page'],
'Q':['d/d3/Button_definition_list.png','Insert definition list','\n; ','\n: Item 1\n: Item 2','Definition'],
'R':['7/79/Button_reflink.png','Insert a reference','<ref>','<\/ref>','Insert reference material'],
'R1':['7/79/Button_reflink.png','Start a reference','<ref name="','','Reference name'],
'R2':['9/99/Button_reflink_advanced_2.png','Insert reference material','">','</ref>','Reference material'],
'R3':['1/1a/Button_reflink_advanced_3.png','No reference material','','"/>',''],
'R4':['9/9a/Button_references.png','Reference footer',"\n==Notes==\n<!--See http://en.wikipedia.org/wiki/Wikipedia:Footnotes for an explanation of how to generate footnotes using the <ref(erences/)> tags-->\n<div class=\'references-small\'>\n<references/>\n</div>",'',''],
'S':['c/c9/Button_strike.png','Strikeout','<s>','<\/s>','Struck out text'],
'T':['e/eb/Button_plantilla.png','Template','{{','}}','Template name'],
'TS':['a/a4/TableStart.png','Start a table','{|','',''],
'TC':['7/71/TableCell.png','Table cell','|','',''],
'TE':['0/06/TableEnd.png','End a table','','|}',''],
'TR':['4/4c/TableRow.png','Start a table row','|-','',''],
'T1':['3/30/Tt_icon.png','Teletype text','<tt>','<\/tt>','Teletype Text'],
'TL':['3/37/Button_tl_template.png','Template link',"{{subst:"+"tl|",'}}','Template name'],
'U':['f/fd/Button_underline.png','Underlined',"<u>","<\/u>",'Underlined text'],
'V':['c/c8/Button_redirect.png','Redirect',"#REDIRECT [[","]]",'Article Name'],
'W':['8/88/Btn_toolbar_enum.png','Numbering',"\n# ","\n# Element 2\n# Element 3",'Element 1'],
'X':['1/11/Btn_toolbar_liste.png','List',"\n* ","\n* Element B\n* Element C",'Element A'],
'Y1':['c/ce/Button_no_include.png','No Include',"<noinclude>","<\/noinclude>",'Text'],
'Y2':['7/79/Button_include.png','Include only',"<includeonly>","<\/includeonly>",'Text'],
'Z':['3/35/Button_substitute.png','Substitute',"{{subst:","}}",'Template'],
'AI':['1/1c/Button_advanced_image.png','Advanaced Image',"[[Image:","|thumb|right|px|Caption]]",'FileName.jpg'],
'GEO':['b/b8/Button_Globe.png','Geo location',"","",""],
'TALK':['4/49/Button_talk.png','Add talk template',"","",""]
};

var XEBOrder2=[];




addOnloadHook(initButtons);
if(!mw.config.get('wgIsArticle'))// only if edit
{ 

	if(XEBPopups)hookEvent("load", extendButtons);
}

function initButtons(){

	var bc,d;

	if (typeof XEBOrder!='string') // can be modified
		XEBOrder2="A,D,C,D1,F,U,J1,E,G,Q,W,X,K,L,H,O,R,T".split(",");
	else if (XEBOrder.toLowerCase()=='all') 
		for (b in BDict) XEBOrder2.push(b);
	else XEBOrder2=XEBOrder.toUpperCase().split(",");

	for (b in BDict) BDict[b][0] = Isrc+BDict[b][0]; // // Add the start of the URL (Isrc) to the XEB buttons
	// If the user has defined any buttons then add them into the available button lists 

	if (typeof myButtons=='object')
	  for (b in myButtons) BDict[b] = myButtons[b];	// custom user buttons
	// Add the media wiki standard buttons into the available buttons 

	for (b in mwEditButtons) { // add standard buttons for full XEB order changing

BDict[b]=[mwEditButtons[b].imageFile,mwEditButtons[b].speedTip,mwEditButtons[b].tagOpen,mwEditButtons[b].tagClose,mwEditButtons[b].sampleText];

	}


	for (i=0;i<XEBOrder2.length;i++) {
		bc = BDict[XEBOrder2[i]];
		if(typeof bc=='object')
		{

			//Call addCustomButton in wikibits
			addCustomButton(bc[0],bc[1],bc[2],bc[3],bc[4]);
		}
	}

	// Remove the default buttons (if requested by the user)
	eraseButtons();
}

function eraseButtons(){

	if (typeof rmEditButtons!='object') return;

	if (typeof rmEditButtons[0] == 'string' && rmEditButtons[0].toLowerCase() == 'all') 
	{
		mwEditButtons=[];
		for(i=0;i<enExtraButtons;i++){mwCustomEditButtons.shift();}
	}
	//Sort the user's requests so we remove the button with the highest index first
	//- This ensures we remove the buttons the user expects whatever order he requested the buttons in
	rmEditButtons.sort(sortit);

	//Remove individual buttons the user doesn't want 

	for(i=0;i<rmEditButtons.length;i++){
		var n=rmEditButtons[i];
		//Standard Wikimedia buttons
		if(n>=0 && n<mwEditButtons.length){
			if(n<mwEditButtons.length){
				var x = -1;
				while((++x)<mwEditButtons.length)
					if(x>=n)
						mwEditButtons[x] = mwEditButtons[x+1];
			}
		mwEditButtons.pop();
		}
		//Extra buttons in English Wikipedia
		n=n-mwEditButtons.length;
		if(n>0 && n<mwCustomEditButtons.length){
		if(n<mwCustomEditButtons.length){
				var x = -1;
				while((++x)<mwCustomEditButtons.length)
					if(x>=n)
						mwCustomEditButtons[x] = mwCustomEditButtons[x+1];
			}
		mwCustomEditButtons.pop();
		}
	}
};

//Function:
//	sortit
//Purpose:
//	Used to sort the rmEditButtons array into descending order
function sortit(a,b){
	return(b-a)
}


//Function:
//Purpose:
//	Adds extended onclick-function to some buttons 
function extendButtons(){

	if(!(allEditButtons = document.getElementById('toolbar'))) return false;
	if(typeof editform != 'undefined')
		if(!(window.editform = document.forms['editform'])) return false;

	//  table
	extendAButton(Isrc+"0/04/Button_array.png",XEBPopupTable)
	extendAButton(Isrc+"7/79/Button_reflink.png",XEBPopupRef)
	extendAButton(Isrc+"b/b8/Button_Globe.png",XEBPopupGeoLink)
	extendAButton(Isrc+"4/49/Button_talk.png",XEBPopupTalk)
	extendAButton(Isrc+"1/1c/Button_advanced_image.png",XEBPopupImage)
	//extendAButton(Isrc+"6/6a/Button_sup_letter.png",XEBPopupFormattedText)

	// redirect -##IE doesn't like this line. Object doesn't support this property or method
	//c=XEBOrder2.getIndex('V');

//	if(c != -1)
//		allEditButtons[bu_len+c].onclick=function(){
//		var a='#REDIRECT \[\['+prompt("Which page do you want to redirect to\?")+'\]\]';
//		document.editform.elements['wpTextbox1'].value=a;
//		document.editform.elements['wpSummary'].value=a;
//		document.editform.elements['wpWatchthis'].checked=false
//  };
};

function extendAButton(url,newfunc)
{
	if(!(allEditButtons = document.getElementById('toolbar'))) return false;
	if(typeof editform != 'undefined')
		if(!(window.editform = document.forms['editform'])) return false;
	allEditButtons = allEditButtons.getElementsByTagName('img');
	for(i=0;i<allEditButtons.length;i++)
	{
		if(allEditButtons[i].src==url)
		{
			allEditButtons[i].onclick=newfunc;
		}
	}
}

function getXEBPopupDiv(name)
{
	XEBMainDiv= document.getElementById("XEB");
	if(XEBMainDiv==null){
		XEBMainDiv=document.createElement("div");
		document.body.appendChild(XEBMainDiv);
		XEBMainDiv.id="XEB";
	}

	me= document.getElementById("XEBPopup" & name);
	if(!(me==null))return me;
	me=document.createElement("div");
	XEBMainDiv.appendChild(me);

	me.id="XEBPopup";
	me.style.position='absolute';
	me.display='none';
	me.visibility='hidden';
	me.onmouseout=CheckHideXEBPopup;
	me.onmouseover=cancelHidePopup;
	return me;
}

function CheckHideXEBPopup(e){
	m= document.getElementById("XEBmnu");
	if(is_gecko)
	{
		ph=m.offsetHeight;
		var x=e.clientX + window.scrollX;
		var y=e.clientY + window.scrollY;;
		s=window.getComputedStyle(m,"");
		ph=s.height;
		ph=Number(ph.substring(0,ph.length-2));
	}
	else
	{
		var x=event.clientX+ document.documentElement.scrollLeft + document.body.scrollLeft;
		var y=event.clientY+ document.documentElement.scrollTop + document.body.scrollTop;
		ph=m.offsetHeight;
	}
	pl=curPopup.x;
	pt=curPopup.y;
	pw=m.style.width;
	pw=Number(pw.substring(0,pw.length-2));

	if(x>(pl+2)&&x<(pl+pw-5)&&y>(pt+2)&&y<(pt+ph-5))return;
	curPopup.hideTimeout=setTimeout('hideXEBPopup()',XEBHideDelay*1000);
}

function cancelHidePopup()
{
	clearTimeout(curPopup.hideTimeout)
}

function hideXEBPopup(){
	XEBMainDiv= document.getElementById("XEB");
	m= document.getElementById("XEBPopup");
	XEBMainDiv.removeChild(m);
}

function XEBstartDrag(e)
{
	m=new GetPos(e||event);
	curPopup.startDrag.mouse=m;
	curPopup.startDrag.floatpopup.y=parseInt(curPopup.div.style.top);
	curPopup.startDrag.floatpopup.x=parseInt(curPopup.div.style.left);
	curPopup.dragging=true;
}

function XEBstopDrag(e)
{
	if(curPopup.dragging==false)return;
	curPopup.dragging=false;
}

function XEBDrag(e)
{
	if(curPopup.dragging==false)return;

	m=new GetPos(e||event);
	x=parseInt(curPopup.startDrag.floatpopup.x+(m.x-curPopup.startDrag.mouse.x));
	y=parseInt(curPopup.startDrag.floatpopup.y+(m.y-curPopup.startDrag.mouse.y));

	curPopup.div.style.top=y+"px";
	curPopup.div.style.left=x+"px";

	curPopup.x=x;
	curPopup.y=y;
}

function XEBPopup(name,x,y)
{

	this.IESelectedRange=XEBgetIESelectedRange();

	winW=(is_gecko)?window.innerWidth:document.body.offsetWidth;
	if((winW-this.width)<x)x=(winW-this.width);

	this.div=getXEBPopupDiv(name);
	this.div.style.zIndex=2000;
	this.div.display="inline";
	this.div.visibility="visible";
	this.div.style.top=y + "px";
	this.x=x;
	this.y=y;
	this.name=name;

	this.startDrag=new Object;
	this.startDrag.floatpopup=new Object;
}

function setInnerHTML(text)
{
	winW=(is_gecko)?window.innerWidth:document.body.offsetWidth;
	if((winW-this.width)<this.x)this.x=(winW-this.width);
	this.div.style.left=this.x+ "px";

	mt="<div id='XEBmnu' style='width:" + this.width + "px' >";
	mt+='<div id="XEBmnuTitle" class="XEBPopupTitle" onmousedown="XEBstartDrag(event)" onmouseup="XEBstopDrag(event)" onmousemove="XEBDrag(event)">Title</div>'
	mt+=text;
	mt+="</div>";
	this.div.innerHTML=mt;
	var InTexts = this.div.getElementsByTagName('input');
	for (var i = 0; i < InTexts.length; i++) {
        	var theInput = InTexts[i];
		if (theInput.type == 'text'){theInput.setAttribute('autocomplete','off');}
	}
	x=XEBgetElementsByClassName(this.div,'XEBMnuItm','span');
	for (var i = 0; i < x.length; i++) {
        	var theItm = x[i];
		theItm.onmouseout=XEBMenuMouseOut;
		theItm.onmouseover=XEBMenuMouseOver;
	}

	this.div.style.borderWidth='thin';
	this.div.style.borderStyle='solid';
	this.div.style.backgroundColor='#D0D0D0';
}
XEBPopup.prototype.width=250;
XEBPopup.prototype.dragging=false;
XEBPopup.prototype.setInnerHTML=setInnerHTML;

var curPopup;

function GetPos(e)
{
	this.x=e.clientX-10+ document.documentElement.scrollLeft + document.body.scrollLeft;
	this.y=e.clientY-10+ document.documentElement.scrollTop + document.body.scrollTop;
}

function XEBPopupTable(e){
	m=new GetPos(e||event);

	curPopup=new XEBPopup("table",m.x,m.y);

	mt='<p>Enter the table parameters below: <\/p>'
		+'<form name="XEBPopupTableForm">'
		+'Table caption: <input type="checkbox" name="inputCaption"><p\/>'
		+'Table alignment: center<input type="checkbox" name="inputAlign"><p\/>'
		+'Table headline: colored<input type="checkbox" name="inputHead"><p\/>'
		+'Number of rows: <input type="text" name="inputRow" value="3" size="2"><p\/>'
		+'Number of columns: <input type="text" name="inputCol" value="3" size="2"><p\/>'
		//+'Alternating grey lines: <input type="checkbox" name="inputLine" checked="1" ><p\/>'
		+'Item column: <input type="checkbox" name="inputItems" ><p\/>'
		+'Sortable: <input type="checkbox" name="inputSort" ><p\/>'
		+'<\/form>'
		+'<i>The default table allows for fields and values only.<\/i><p\/>'
		+'Check "Item column" to allow for the table to have fields, items, and values.<\/i><p\/>'
		+'<p><button onClick="javascript:insertTableCode()">Insert</button>'
		+'<button onClick="hideXEBPopup()">Cancel</button>'

	curPopup.setInnerHTML(mt);

	return true;
}

function insertTableCode(){
	f=document.XEBPopupTableForm;
	var caption = (f.inputCaption.checked)?"|+ TABLE CAPTION \n":""; 
	var exhead = (f.inputHead.checked)?'|- style="background: #DDFFDD;"\n':""; 
	var nbRow = parseInt(f.inputRow.value); 
	var nbCol = parseInt(f.inputCol.value); 
	var exfield = f.inputItems.checked; 
	var align = (f.inputAlign.checked)?'align="center"':""; 

	//generateTable(caption, exhead, nbCol, nbRow, exfield, align);

	var code = "\n";
	code += '{| {{prettytable}} ' + align + ' '; // en: class="wikitable"
	code+=(f.inputSort.checked)?'class="sortable" \n':'\n';
	code += caption + exhead;
	if (exfield) code += '!\n';
	for (i=1;i<nbCol+1;i++) code += '! FELD ' + i + '\n';
	var items = 0;
	for (var j=0;j<nbRow;j++){
		if (exfield) { 
			items++;
			code += '|-\n! style="background: #FFDDDD;"|ITEM ' + items + '\n';
		}	else code += '|-\n';
		for (i=0;i<nbCol;i++) code += '| Element\n';
	}
	code += '|}\n';
	hideXEBPopup();
	insertTags('','', code);
	extendSummary('table');

	return false;
}  

function XEBGetSelectedText()
{
	var txtarea;
	if (document.editform) {
		txtarea = document.editform.wpTextbox1;
	} else {
		// some alternate form? take the first one we can find
		var areas = document.getElementsByTagName('textarea');

		txtarea = areas[0];
	}
	// IE & Opera
	if (document.selection  && !is_gecko)
	{
		var theSelection = document.selection.createRange().text;
		if (!theSelection) theSelection='';
	}
	// Mozilla
	else if(txtarea.selectionStart || txtarea.selectionStart == '0') {
		var replaced = false;
		var startPos = txtarea.selectionStart;
		var endPos = txtarea.selectionEnd;
		var theSelection = (txtarea.value).substring(startPos, endPos);
		if (!theSelection) theSelection='';
	}
	return theSelection;
}

function XEBgetIESelectedRange(){
	var IESel=new Object;
	var txtarea;
	if (document.editform) {
		txtarea = document.editform.wpTextbox1;
	} else {
		var areas = document.getElementsByTagName('textarea');

		txtarea = areas[0];
	}
	// IE & Opera

	if (document.selection  && !is_gecko)
	{
		txtarea.focus();
		IESel.Rng=document.selection.createRange();
		return IESel;
	}
}

function XEBinsertText(beforeText,selText,afterText,IESelectedRange) {
	var newText=beforeText + selText + afterText;
	var txtarea;
	if (document.editform) {
		txtarea = document.editform.wpTextbox1;
	} else {
		// some alternate form? take the first one we can find
		var areas = document.getElementsByTagName('textarea');
		txtarea = areas[0];
	}

	// IE
	if (document.selection  && !is_gecko) {

		tr=IESelectedRange.Rng;
		tr.text=newText;
		txtarea.focus();
		//txtarea.caretpos=tr.duplicate();
		tr.select();

		return;

	// Mozilla
	} else if(txtarea.selectionStart || txtarea.selectionStart == '0') {
		var replaced = false;
		var startPos = txtarea.selectionStart;
		var endPos = txtarea.selectionEnd;

		if (endPos-startPos) {
			replaced = true;
		}
		var scrollTop = txtarea.scrollTop;
//		var myText = (txtarea.value).substring(startPos, endPos);
//		if (!myText) {
//			myText=sampleText;
//		}
//		if (myText.charAt(myText.length - 1) == " ") { // exclude ending space char, if any
//			subst = tagOpen + myText.substring(0, (myText.length - 1)) + tagClose + " ";
//		} else {
//			subst = tagOpen + myText + tagClose;
//		}
		txtarea.value = txtarea.value.substring(0, startPos) + newText +
			txtarea.value.substring(endPos, txtarea.value.length);
		txtarea.focus();
		//set new selection
		if (!replaced) {
			var cPos = startPos+(newText.length);
			txtarea.selectionStart = cPos;
			txtarea.selectionEnd = cPos;
		} else {
			txtarea.selectionStart = startPos+beforeText.length;
			txtarea.selectionEnd = startPos+beforeText.length+selText.length;
		}
		txtarea.scrollTop = scrollTop;

	}
	if (txtarea.createTextRange) {

		txtarea.caretPos = document.selection.createRange().duplicate();
	}
txtarea.focus();
}

function generateTable(caption, exhead, nbCol, nbRow, exfield, align){};

function XEBPopupRef(e){

	m=new GetPos(e||event);

	curPopup=new XEBPopup("ref",m.x,m.y);
	curPopup.width=500;
	mt='<p>Enter the reference parameters below: <\/p>'
		+'<form name="XEBPopupRefForm">'
		+'Name:<input type="text" name="refName" value="" size="10"><p\/>'
		+'Material:<input type="text" name="refMaterial" value="' + XEBGetSelectedText() + '" size="20">'
		+'<\/form>'
		+'<p><button onClick="javascript:insertRef()">Insert</button>'
		+'<button onClick="hideXEBPopup()">Cancel</button>';

	curPopup.setInnerHTML(mt);
//	document.XEBPopupRefForm.refName.focus();
	return true;
}

function insertRef(){
	f=document.XEBPopupRefForm;
	var refName = f.refName.value;
	var refMaterial=f.refMaterial.value;
	
	hideXEBPopup();
	var code1='<ref';
	code1+=(refName)?' name="'+refName+'">':'>'; 
	code2=refMaterial;
	code3='<\/ref>'
	XEBinsertText(code1,code2,code3,curPopup.IESelectedRange);

	extendSummary('ref');
	return false;
} 

//===GEO LINK Function==================================================

function XEBPopupGeoLink(e)
{
	m=new GetPos(e||event);

	curPopup=new XEBPopup("geo",m.x,m.y);
	curPopup.width=300;
	mt='<p>Enter the location parameters below: <\/p>'
		+'<form name="XEBPopupGeoLinkForm">'
		+'Loction:<p\/>'
		+'<table style="background: transparent;">'
		+'<tr><td>Latitude:<\/td><td><input type="text" autocomplete="off" name="geoLatDeg" value="" size="4"><\/td>'
		+'<td><input type="text" name="geoLatMin" size="4"><\/td>'
		+'<td><input type="text" name="geoLatSec" size="4"><\/td>'
		+'<td><select name="geoLatNS"><option value="N">N<option value="S">S</select><\/td><\/tr>'
		+'<tr><td>Longitude:<\/td><td><input type="text" name="geoLonDeg" value="" size="4"><\/td>'
		+'<td><input type="text" name="geoLonMin" value="" size="4"><\/td>'
		+'<td><input type="text" name="geoLonSec" value="" size="4"><\/td>'
		+'<td><select name="geoLonEW"><option value="E">E<option value="W">W</select><\/td><\/tr>'
		+'<\/table>'
		+'Region:<input type="text" name="geoRegion" value="" size="4"><p\/>'
		+'Type:'
		+'<SELECT NAME="geoType" size="5">'
		+'<OPTION VALUE="country">Country<OPTION VALUE="state">State'
		+'<OPTION VALUE="adm1st">Admin unit, 1st level<OPTION VALUE="adm2st">Admin unit, 2nd level'
		+'<OPTION VALUE="city">City<OPTION VALUE="airport">Airport'
		+'<OPTION VALUE="mountain">Mountain<OPTION VALUE="isle">Isle'
		+'<OPTION VALUE="waterbody">Waterbody<OPTION VALUE="landmark" SELECTED>Landmark'
		+'<OPTION VALUE="forest">forest</SELECT><br>'
		+'Title: <input type="checkbox" name="geoTitle" ><p\/>'
		+'<\/form>'
		+'<p><button onClick="javascript:insertGeoLink()">Insert</button>'
		+'<button onClick="hideXEBPopup()">Cancel</button>';

	curPopup.setInnerHTML(mt);
	document.paramForm.refName.focus();
	return true;

}
function insertGeoLink()
{
	f=document.XEBPopupGeoLinkForm;

	var code='{{Coor ';
	if(f.geoTitle.checked)code+='title ';
	ft='dms';
	if(f.geoLatSec.value==''&&f.geoLonSec.value=='')ft='dm';
	if(ft=='dm'&&f.geoLatMin.value==''&&f.geoLonMin.value=='')ft='d';
	code+=ft;
	code+='|'+f.geoLatDeg.value;
	code+=(ft=='dm'||ft=='dms')?'|'+f.geoLatMin.value:'';
	code+=(ft=='dms')?'|'+f.geoLatSec.value:'';
	code+='|'+f.geoLatNS.value;
	code+='|'+f.geoLonDeg.value;
	code+=(ft=='dm'||ft=='dms')?'|'+f.geoLonMin.value:'';
	code+=(ft=='dms')?'|'+f.geoLonSec.value:'';
	code+='|'+f.geoLonEW.value;
	code+='|type:'+f.geoType.value+'_region:'+f.geoRegion.value
	code+='}}';
	insertTags('','', code);
	extendSummary('geo-location');
	hideXEBPopup();
	return false;
}

function XEBPopupTalk(e)
{
	m=new GetPos(e||event);

	curPopup=new XEBPopup("talk",m.x,m.y);
	curPopup.width=200;
	mt='<div style="font-size:medium"><p>Please choose:<\/p>'
	mt+='<span class="XEBMnuItm" onclick="XEBInsertTalk(1)">Test1<\/span><br>'
	mt+='<span class="XEBMnuItm" onclick="XEBInsertTalk(2)">Self Test<\/span><br>'
	mt+='<span class="XEBMnuItm" onclick="XEBInsertTalk(3)">Nonsense<\/span><br>'
	mt+='<span class="XEBMnuItm" onclick="XEBInsertTalk(4)">Please stop<\/span><br>'
	mt+='<span class="XEBMnuItm" onclick="XEBInsertTalk(5)">Last chance<\/span><br>'
	mt+='<span class="XEBMnuItm" onclick="XEBInsertTalk(6)">Blanking<\/span><br>'
	mt+='<span class="XEBMnuItm" onclick="XEBInsertTalk(7)">Blatant<\/span><br>'
	mt+='<span class="XEBMnuItm" onclick="XEBInsertTalk(8)">*BLOCKED*<\/span><br>'
	mt+='<span class="XEBMnuItm" onclick="XEBInsertTalk(9)">Spam<\/span><br>'
	mt+='<span class="XEBMnuItm" onclick="XEBInsertTalk(10)">Npov<\/span></div>'

	curPopup.setInnerHTML(mt);

	return true;

}
function XEBInsertTalk(itm)
{
	hideXEBPopup();
	if(itm==1)code='{{subst:test1-n|}}';
	if(itm==2)code='{{subst:selftest-n|}}';
	if(itm==3)code='{{subst:test2-n|}}';
	if(itm==4)code='{{subst:test3-n|}}';
	if(itm==5)code='{{subst:test4-n|}}';
	if(itm==6)code='{{subst:test2a-n|}}';
	if(itm==7)code='{{subst:bv-n|}}';
	if(itm==8)code='{{subst:blantant|}}';
	if(itm==9)code='{{subst:spam-n|}}';
	if(itm==10)code='{{subst:NPOV user}}';

	insertTags('','', code);
	return false;
}
function XEBPopupImage(e)
{
	m=new GetPos(e||event);

	curPopup=new XEBPopup("image",m.x,m.y);
	curPopup.width=300;

	mt='<p>Enter the image parameters below: <\/p>'
		+'<form name="XEBPopupImageForm">'
		+'File:<input type="text" name="imgFile" value="' + XEBGetSelectedText() + '" size="30"><br>'
		+'Type:<SELECT NAME="imgType">'
		+'<OPTION VALUE="thumb">Thumbnail'
		+'<OPTION VALUE="frame">Frame'
		+'<OPTION VALUE="none">[not specified]'
		+'</SELECT><br>'
		+'Location:<SELECT NAME="imgLocation">'
		+'<OPTION VALUE="left">Left'
		+'<OPTION VALUE="center">Centre'
		+'<OPTION VALUE="right">Right'
		+'<OPTION VALUE="none">None'
		+'</SELECT><br>'
		+'Size:<input type="text" name="imgSize" value="100" size="3">px<br>'
		+'Caption:<input type="text" name="imgCaption" value="" size="30"><\/p>'
		+'<\/form>'
		+'<p><button onClick="javascript:XEBInsertImage()">Insert</button>'
		+'<button onClick="hideXEBPopup()">Cancel</button>';

	curPopup.setInnerHTML(mt);

	return true;
}
function XEBInsertImage()
{
	f=document.XEBPopupImageForm;
	hideXEBPopup();
	var code='[[Image:';
	code+=f.imgFile.value;
	code+='|'+f.imgType.value;
	code+='|'+f.imgLocation.value;
	code+='|'+f.imgSize.value;
	code+='|'+f.imgCaption.value;
	code+=']]';
	insertTags('','', code);
	extendSummary('image');

	return false;
}

function XEBPopupFormattedText(e)
{
	m=new GetPos(e||event);

	curPopup=new XEBPopup("image",m.x,m.y);
	curPopup.width=300;
	
	mt='<form name="XEBPopupImageForm">'
		+'<table  style="background: transparent;">'
		+'<tr><td>Bold:<\/td><td><input type="checkbox" name="textBold"><\/td>'
		+'<td>Superscript:<\/td><td><input type="checkbox" name="textSuperscript"><\/td><\/tr>'
		+'<tr><td>Italic:<\/td><td><input type="checkbox" name="textItalic"><\/td>'
		+'<td>Subscript:<\/td><td><input type="checkbox" name="textSubscript"><\/td><\/tr>'
		+'<tr><td>Strike:<\/td><td><input type="checkbox" name="textStrike"><\/td>'
		+'<td>&nbsp;<\/td><\/tr>'
		+'</table>'
		+'Size:<SELECT NAME="textSize">'
		+'<OPTION VALUE="small">small'
		+'<OPTION VALUE="normal">[Normal]'
		+'<OPTION VALUE="big">big'
		+'</SELECT><br><table style="background:transparent;"><tr><td>Colour:<\/td><td>'
		+'<table width="100px">'
		+'<tr><td colspan="4">None<\/td></tr>'
		+'<tr><td bgcolor="aqua">&nbsp;<\/td><td bgcolor="gray"> &nbsp;<\/td>'
		+'<td bgcolor="olive">&nbsp;<\/td><td bgcolor="navy">&nbsp;<\/td><\/tr>'
		+'<tr><td bgcolor="black">&nbsp;<\/td><td bgcolor="green"> &nbsp;<\/td>'
		+'<td bgcolor="purple">&nbsp;<\/td><td bgcolor="teal">&nbsp;<\/td><\/tr>'
		+'<tr><td bgcolor="blue">&nbsp;<\/td><td bgcolor="lime">&nbsp;<\/td>'
		+'<td bgcolor="red">&nbsp;<\/td><td bgcolor="white">&nbsp;<\/td><\/tr>'
		+'<tr><td bgcolor="fuchsia">&nbsp;<\/td><td bgcolor="maroon">&nbsp;<\/td>'
		+'<td bgcolor="silver">&nbsp;<\/td><td bgcolor="yellow">&nbsp;<\/td><\/tr>'
		+'</table><\/td><\/tr>'
		+'<\/form>'
		+'Sample:'
		+'<span id="sampleText">Text</span>"'
		+'<p><button onClick="javascript:XEBInsertFormattedText()">Insert</button>'
		+'<button onClick="hideXEBPopup()">Cancel</button>';

	curPopup.setInnerHTML(mt);

	return true;
}

function XEBUpdateSampleText()
{
	f=document.XEBPopupImageForm;
}

function XEBMenuMouseOut(e)
{
	var targ;
	if (!e) var e = window.event;
	if (e.target) targ = e.target;
	else if (e.srcElement) targ = e.srcElement;

	targ.style.color='black';
}

function XEBMenuMouseOver(e)
{	var targ;
	if (!e) var e = window.event;
	if (e.target) targ = e.target;
	else if (e.srcElement) targ = e.srcElement;

	targ.style.color='red';
}

function XEBgetElementsByClassName(parent,clsName,htmltag){ 
	var arr = new Array(); 
	var elems = parent.getElementsByTagName(htmltag);
	for ( var cls, i = 0; ( elem = elems[i] ); i++ ){
		if ( elem.className == clsName ){
			arr[arr.length] = elem;
		}
	}
	return arr;
}

function extendSummary(newText) {
	if(!XEBExtendEditSummary)return;
	s=document.editform.elements['wpSummary'].value;
	s+=(s=='')?newText:' +'+newText;
	document.editform.elements['wpSummary'].value=s;
}


var ct = ct || {};

ct.setSelectionRange = function (ta, start, end) {
	var _static = arguments.callee;
	if (ta.setSelectionRange) {
		
		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);
		var yOffset = hta.scrollHeight;
		hta.style.display = 'none';
		ta.focus();
		ta.setSelectionRange(start, end);
		if (yOffset > ta.clientHeight) {
			yOffset -= Math.floor(ta.clientHeight / 2);
			ta.scrollTop = yOffset;
			// 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();
	}
};

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.hlink = function (toWhat, text) {
	var wgServer = window.wgServer || 'http://en.wikipedia.org';
	var wgArticlePath = window.wgArticlePath || '/wiki/$1';
	var url = (wgServer + wgArticlePath).replace('$1', toWhat);
	return '<a target="_blank">' + (text || toWhat) + '</a>';
};

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));
	};
};

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;
	}
};

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<String, int>

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
});

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;
};

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);
};

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
	if ((window.wgCanonicalNamespace != null)
				&& /(\b|_)talk$/i.test(window.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')
		));
	}
};

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;
};

ct.delayScan = function () {
	if (ct.scanTimeoutId != null) {
		clearTimeout(ct.scanTimeoutId);
		ct.scanTimeoutId = null;
	}
	ct.scanTimeoutId = setTimeout(ct.scan, 500);
};

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 < 1000)) {
		// 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 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
	);
	// Propose an edit summary unless it's a new section
	var editform = document.getElementById('editform');
	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 [[User:Cameltrader#Advisor.js|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();
};

ct.addToSummary = function (summary) {
	var wpSummary = document.getElementById('wpSummary');
	if (wpSummary.value != '') {
		summary = wpSummary.value + '; ' + 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';
};

if (!window.wgContentLanguage || (window.wgContentLanguage === 'en')) { // from this line on, a level of indent is spared

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

// === 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 <tt>[[A|A]]</tt> to be abbreviated as <tt>[[A]].</tt>  '
		};
	}
	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 [[A]]B.',
				help: ct.hlink('WP:Syntax#Wiki_markup', 'MediaWiki syntax')
					+ ' allows links of the form <tt>[[A|AB]]</tt> to be abbreviated as <tt>[[A]]B.</tt>'
		};
	}
	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'
		};
	}

	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.'
							+ '<br/>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:<br/>'
								+ '<tt>[[1 January]] [[1970]]</tt><br>'
								+ '<tt>[[January 1]], [[1970]]</tt><br>'
								+ 'and month names should not be abbreviated.'
				});
			}
		}
	}
	return b;
});

ct.rules.push(function (s) {
	// Matches decades in the range 1000s ... 2990s,
	// linked either as [[xxx0]]s 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 = /[{letter}]( +- +)[{letter}]/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];
		// Be careful not to break wikilinks.  If we find a ']' before we find an '['---drop the suggestion.
		var rightContext = s.substring(m.end);
		var indexOfOpening = rightContext.indexOf('[');
		var indexOfClosing = rightContext.indexOf(']');
		if ((indexOfClosing != -1)
				&& ((indexOfOpening == -1) || (indexOfOpening > indexOfClosing))) {
			continue;
		}
		b.push({
				start: m.start + 1,
				end: m.end - 1,
				replacement: '&nbsp;\u2014 ', // U+2014 is an mdash
				name: 'mdash',
				description: 'In a sentence, a hyphen surrounded by spaces means almost certainly an mdash.'
		});
	}
	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 re = / (\u2014|\u2013|&mdash;|&ndash;)/g; // an m/ndash surrounded by normal spaces
	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: '&nbsp;' + m[1], // a non-breaking space and the dash
				name: 'nbsp-dash',
				description: 'Put a non-breaking space before the dash',
				help: 'Putting a ' + ct.hlink('non-breaking space') + ' (<tt>&amp;nbsp;</tt>) before a dash would '
					+ 'prevent the user agent from wrapping it at the beginning of the next line.'
		};
	}
	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: "<p>In this case the triangular colon (``\u02d0'', <tt>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: "<p>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''."
							+ '<p>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;
});

ct.rules.push(function (s) {
	var a = ct.getAllMatches(/\b(NOT)\b/g, s);
	var b = [];
	for (var i = 0; i < a.length; i++) {
		var m = a[i];
		if ((s.substring(m.start - 2, m.start) == "''")
				&& (s.substring(m.end, m.end + 2) == "''")) {
			continue;
		}
		var noMoreLinksRemainder = ' A COLLECTION OF LINKS NOR SHOULD IT BE USED FOR';
		if (s.substring(m.end, m.end + noMoreLinksRemainder.length) === noMoreLinksRemainder) {
			// Tolerate subst'ed Template:NoMoreLinks
			continue;
		}
		b.push({
				start: m.start,
				end: m.end,
				replacement: "''not''",
				name: 'all-caps',
				description: 'Change to lowercase',
				help: 'According to the ' + ct.hlink('WP:MOS#Capital_letters', 'Manual of Style')
					+ ', the word <i>' + m[1].toLowerCase() + '</i> should be italicised instead '
					+ 'of being written in all caps.'
		});
	}
	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: '{{lang-' + code + '|' + m[3] + '}}',
				name: 'lang-' + code,
				description: 'Apply the {{lang-' + code + '}} template',
				help: 'The <tt>' + ct.hlink('Template:lang-' + code, '{{lang-' + code + '}}')
					+ '</tt> template can be applied for this text.'
					+ '<br/>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: '{{main|' + m[1] + '}}',
					name: 'template-main',
					description: 'Use the {{main|...}} template',
					help: 'Template <tt>' + ct.hlink('Template:Main', '{{main|...}}')
						+ '</tt> can be used in this place.'
			});
		}
	}
	return b;
});

ct.rules.push(function (s) {
	var re = /^[ ':]*(?:(?:Further|More) +info(?:rmation)?)[ ']*:[ ']*\[\[([^\]]+)\]\][ ']*$/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: '{{futher|' + m[1] + '}}',
					name: 'template-further',
					description: 'Use the {{futher|...}} template',
					help: 'Template <tt>' + ct.hlink('Template:Further', '{{further|...}}')
						+ '</tt> can be used in this place.'
			});
		}
	}
	return b;
});

ct.rules.push(function (s) {
	var exceptions = {};
	var wgTitle = window.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 = '{{' + 'DEFAULTSORT:' + lastName + ', ' + firstNames + '}}\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 = window.wgTitle || '';
	var reTitle = /^(a|the) (.*)$/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: '{{' + 'DEFAULTSORT:' + nounPhrase + ', ' + article + '}}\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>{{' + ct.hlink('Template:DEFAULTSORT', 'DEFAULTSORT')
						+ '}}</tt> template is discouraged.  The magic word with the same name should be used instead.'
		});
	}
	return b;
});

ct.rules.push(function (s) {
	var _static = arguments.callee;
	if (_static.DEPRECATED_TEMPLATES_ARRAY == null) {
		_static.DEPRECATED_TEMPLATES_ARRAY = [
				'ArB', 'ArTranslit', 'ArabDIN', 'BridgeType', 'CFB Coaching Record End', 'CFB Coaching Record Entry',
				'CFB Coaching Record Start', 'CFB Coaching Record Team', 'CFB Coaching Record Team End', 'CURRENTWEEKDAY', 'Canada CP 2001',
				'CelsiusToKelvin', 'Chembox', 'Chembox simple inorganic', 'Chembox simple organic', 'Chinesename', 'ConvertVolume',
				'ConvertWeight', 'Country', 'Cultivar hybrid', 'Dated episode notability', 'Doctl', 'Dynamic navigation box',
				'Dynamic navigation box with image', 'Dynamic navigation small', 'Episode-unreferenced', 'Extra album cover', 'Extra chronology',
				'Fa', 'Factor', 'Fn', 'Fnb', 'Football stadium', 'Footnote', 'GUE', 'Geolinks-US-loc', 'Getamap', 'Harvard reference',
				'Hiddenkey', 'IAST-hi', 'IAST1', 'ISOtranslit', 'Iftrue', 'Illinois Area Codes', 'Infobox Minor Planet', 'Infobox Ship',
				'Infobox music venue', 'Ivrit', 'JER', 'Lang-yi2', 'Lang2iso', 'LangWithNameNoItals', 'Latinx',
				'Military-Insignia', 'Mmuk mapdet', 'Mmuk mapho25', 'Mmuk maphot', 'Mmuknr map', 'Mmuknr photo', 'Mmukpc prim', 'Navbox generic',
				'Navigation', 'Navigation box with image', 'Navigation no hide', 'Navigation with columns', 'Navigation with image', 'NavigationBox',
				'Novelinfoboxincomp', 'Novelinfoboxneeded', 'OldVGpeerreview', 'Ordinal date', 'PD-LOC', 'PIqaD',
				'Pekinensis tail familia Amaranthaceae', 'Pekinensis tail genus Chenopodium', 'Pekinensis tail regnum Plantae', 'PerB',
				'PerTranslit', 'Pound avoirdupois', 'Prettyinfobox', 'Prettytable', 'Qif', 'Rating-10', 'Rating-3', 'Rating-4', 'Rating-5',
				'Rating-6', 'Ref num', 'Reqimage', 'Rewrite-section', 'Ruby', 'Sectionrewrite', 'Semxlit', 'Skyscraper', 'Sortdate', 'Source',
				'Storm pics', 'Supertribus', 'Switch', 'Tablabonita', 'Taxobox superregnum entry', 'Taxobox supertribus entry', 'IPA fonts',
				'Unicode fonts', 'User R-proglang', 'User asm', 'User cobol', 'User css', 'User haskell', 'User html', 'User java', 'User mobile',
				'User programming', 'User unicode', 'User xhtml', 'User xml', 'Tfd-kept', 'Timeline infobox finish', 'Timeline infobox start',
				'Translit-yi2', 'WAFerry', 'Weight'
		];
		_static.DEPRECATED_TEMPLATES_SET = {};
		for (var i = _static.DEPRECATED_TEMPLATES_ARRAY.length - 1; i >= 0; i--) {
			_static.DEPRECATED_TEMPLATES_SET[_static.DEPRECATED_TEMPLATES_ARRAY[i]] = true;
		}
	}
	var a = ct.getAllMatches(ct.fixRegExp(/(\{\{\s*)([{letter}0-9\s\-]+)(\s*(\||\}\}))/g), s);
	var b = [];
	for (var i = 0; i < a.length; i++) {
		var m = a[i];
		var name = m[2].replace(/ /g, '_');
		name = name.charAt(0).toUpperCase() + name.substring(1);
		if (_static.DEPRECATED_TEMPLATES_SET[name]) {
			b.push({
					start: m.start,
					end: m.end,
					name: 'deprecated-template',
					description: 'Template {{' + name + '}} has been deprecated',
					help: 'Template <tt>' + ct.hlink('Template:' + name, '{{' + name + '}}')
						+ ' is ' + ct.hlink('Category:Deprecated templates', 'deprecated')
						+ '.  Consider using another one as recommended on the template page.'
			});
		}
	}
	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>==&nbsp;Heading&nbsp;==</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>==&nbsp;Heading&nbsp;==</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]|&ndash;|&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 <i>born</i> should be fully written.'
		};
	}
	return a;
});

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',
					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:<br><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',
					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:<br><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',
				description: 'Bad ISBN checksum',
				help: 'Bad ISBN checksum for<br/><tt>' + m[1] + '</tt><br/>'
		});
	}
	return b;
});

} // end if (window.wgContentLanguage === 'en')

//<source lang="javascript">
 
/*
 * DisamAssist: a tool for repairing links from articles to disambiguation pages.
 */
 
( function( mw, $, undefined ) {
	var cfg = {};
	var txt = {};
	var startLink, ui;
	var links, pageChanges;
	var currentPageTitle, currentPageParameters, currentLink;
	var possibleBacklinkDestinations;
	var forceSamePage = false;
	var running = false;
	var choosing = false;
	var canMarkIntentionalLinks = false;
	var displayedPages = {};
	var editCount = 0;
	var editLimit;
	var pendingSaves = [];
	var pendingEditBox = null;
	var pendingEditBoxText;
	var lastEditMillis = 0;
	var runningSaves = false;
 
	/*
	 * Entry point. Check whether we are in a disambiguation page. If so, add a link to start the tool
	 */
	var install = function() {
		cfg = window.DisamAssist.cfg;
		txt = window.DisamAssist.txt;
		if ( mw.config.get( 'wgAction' ) === 'view' && isDisam() ) {
			mw.loader.using( ['mediawiki.Title', 'mediawiki.api'], function() {
				$( document ).ready( function() {
					// This is a " (disambiguation)" page
					if ( new RegExp( cfg.disamRegExp ).exec( mw.config.get( 'wgTitle' ) ) ) {
						var startMainLink = $( mw.util.addPortletLink( 'p-cactions', '#', txt.startMain ) )
							.click( startMain );
						var startSameLink = $( mw.util.addPortletLink( 'p-cactions', '#', txt.startSame ) )
							.click( startSame );
						startLink = startMainLink.add( startSameLink );
					} else {
						startLink = $( mw.util.addPortletLink( 'p-cactions', '#', txt.start ) ).click( start );
					}
				} );
			} );
		}
	};
 
	/*
	 * Start the tool. Display the UI and begin looking for links to fix
	 */
	var start = function() {
		if ( !running ) {
			running = true;
			links = [];
			pageChanges = [];
			displayedPages = {};
			ensureDABExists().then( function( canMark ) {
				canMarkIntentionalLinks = canMark;
				createUI();
				addUnloadConfirm();
				markDisamOptions();
				checkEditLimit().then( function() {
					togglePendingEditBox( false );
					doPage();
				} );
			} );
		}
	};
 
	/*
	 * Start DisamAssist. Disambiguate incoming links to the current page, regardless
	 * of the title.
	 */
	var startSame = function() {
		forceSamePage = true;
		start();
	};
 
	/*
	 * Start DisamAssist. If the page title ends with " (disambiguation)", disambiguate
	 * links to the primary topic article. Otherwise, disambiguate links to the current
	 * page.
	 */
	var startMain = function() {
		forceSamePage = false;
		start();
	};
 
	/*
	 * Create and show the user interface.
	 */
	var createUI = function() {
		ui = {
			display: $( '<div></div>' ).addClass( 'disamassist-box disamassist-mainbox' ),
			finishedMessage: $( '<div></div>' ).text( txt.noMoreLinks ).hide(),
			pageTitleLine: $( '<span></span>' ).addClass( 'disamassist-pagetitleline' ),
			pendingEditCounter: $( '<div></div>').addClass( 'disamassist-editcounter' ),
			context: $( '<span></span>' ).addClass( 'disamassist-context' ),
			undoButton: createButton( txt.undo, undo ),
			omitButton: createButton( txt.omit, omit ),
			endButton: createButton( txt.close, saveAndEnd ),
			refreshButton: createButton( txt.refresh, refresh ),
			titleAsTextButton: createButton( txt.titleAsText, chooseTitleFromPrompt ),
			intentionalLinkButton: canMarkIntentionalLinks ? createButton( txt.intentionalLink, chooseIntentionalLink ) : $( '<span></span>' ),
			disamNeededButton: cfg.disamNeededText ? createButton( txt.disamNeeded, chooseDisamNeeded ) : $( '<span></span>' ),
			removeLinkButton: createButton( txt.removeLink, chooseLinkRemoval )
		};
		var top = $( '<div></div>' ).addClass( 'disamassist-top' )
			.append( [ui.pendingEditCounter, ui.finishedMessage, ui.pageTitleLine] );
		var leftButtons = $( '<div></div>' ).addClass( 'disamassist-leftbuttons' )
			.append( [ui.titleAsTextButton, ui.removeLinkButton, ui.intentionalLinkButton, ui.disamNeededButton, ui.omitButton] );
		var rightButtons = $( '<div></div>' ).addClass( 'disamassist-rightbuttons' )
			.append( [ui.undoButton, ui.refreshButton, ui.endButton] );
		var allButtons = $( '<div></div>' ).addClass( 'disamassist-allbuttons' )
			.append( [leftButtons, rightButtons] );
		ui.display.append( [top, ui.context, allButtons] );
		updateEditCounter();
		toggleActionButtons( false );
		// Insert the UI in the page
		$( '#mw-content-text' ).before( ui.display );
		ui.display.hide().fadeIn();
	};
 
	/*
	 * If there are pending changes, show a confirm dialog before closing
	 */
	var addUnloadConfirm = function() {
		$( window ).bind( 'beforeunload', function( ev ) {
			if ( running && checkActualChanges() ) {
				return txt.pending;
			} else if ( editCount !== 0 ) {
				return txt.editInProgress;
			}
		});
	};
 
	/*
	 * Mark the disambiguation options as such
	 */
	var markDisamOptions = function() {
		var optionPageTitles = [];
		var optionMarkers = [];
		getDisamOptions().each( function() {
			var link = $( this );
			var title = extractPageName( link );
			var optionMarker = $( '<a></a>' ).attr( 'href', '#' ).addClass( 'disamassist-optionmarker' )
				.text( txt.optionMarker ).click( function( ev ) {
					ev.preventDefault();
					chooseReplacement( title );
				} );
			link.after( optionMarker );
			optionMarkers.push( optionMarker );
			optionPageTitles.push( title );
		} );
		// Now check the disambiguation options and display a different message for those that are
		// actually the same as the target page where the links go, as choosing those options doesn't really
		// accomplish anything (except bypassing redirects, which might be useful in some cases)
		var targetPage = getTargetPage();
		fetchRedirects( optionPageTitles.concat( targetPage ) ).done( function( redirects ) {
			var endTargetPage = resolveRedirect( targetPage, redirects );
			for ( var ii = 0; ii < optionPageTitles.length; ii++ ) {
				var endOptionTitle = resolveRedirect( optionPageTitles[ii], redirects );
				if ( isSamePage( endOptionTitle, targetPage ) || isSamePage( optionPageTitles[ii], targetPage ) ) {
					optionMarkers[ii].text( txt.targetOptionMarker ).addClass( 'disamassist-curroptionmarker');
				} else if ( isSamePage( endOptionTitle, endTargetPage ) ) {
					optionMarkers[ii].text( txt.redirectOptionMarker ).addClass( 'disamassist-curroptionmarker');
				}
			}
		} ).fail( error );	
	};
 
	/*
	 * Check whether intentional links to disambiguation pages can be explicitly marked
	 * as such in this wiki. If so, ensure that a "Foo (disambiguation)" page exists.
	 * Returns a jQuery promise
	 */
	var ensureDABExists = function() {
		var dfd = new $.Deferred();
		var title = mw.config.get( 'wgTitle' );
		// That concept doesn't exist in this wiki.
		if ( !cfg.intentionalLinkOption ) {
			dfd.resolve( false );
		// "Foo (disambiguation)" exists: it's the current page.
		} else if ( new RegExp( cfg.disamRegExp ).exec( title ) ) {
			dfd.resolve( true );
		} else {
			var disamTitle = cfg.disamFormat.replace( '$1', title );
			loadPage( disamTitle ).done( function( page ) {
				// "Foo (disambiguation)" doesn't exist.
				if ( page.missing ) {
					// We try to create it
					page.content = cfg.redirectToDisam.replace( '$1', title );
					var summary = txt.redirectSummary.replace( '$1', title );
					savePage( disamTitle, page, summary, false, true ).done( function() {
						dfd.resolve( true );
					} ).fail( function( description ) {
						error( description );
						dfd.resolve( false );
					} );
				// It does exist
				} else {
					dfd.resolve( true );
				}
			} ).fail( function( description ) {
				error( description );
				dfd.resolve( false );
			} );
		}
		return dfd.promise();
	};
 
	/*
	 * Check whether the edit cooldown applies and sets editLimit accordingly.
	 * Returns a jQuery promise
	 */
	var checkEditLimit = function() {
		var dfd = new $.Deferred();
		if ( cfg.editCooldown <= 0 ) {
			editLimit = false;
			dfd.resolve();
		} else {
			fetchRights().done( function( rights ) {
				editLimit = $.inArray( 'bot', rights ) === -1;
			} ).fail( function( description ) {
				error( description );
				editLimit = true;
			} ).always( function() {
				dfd.resolve();
			} );
		}
		return dfd.promise();
	};
 
	/*
	 * Find and ask the user to fix all the incoming links to the disambiguation ("target")
	 * page from a single "origin" page
	 */
	var doPage = function() {
		if ( pageChanges.length > cfg.historySize ) {
			applyChange( pageChanges.shift() );
		}
		if ( links.length === 0 ) {
			var targetPage = getTargetPage();
			getBacklinks( targetPage ).done( function( backlinks, pageTitles ) {
				var pending = {};
				$.each( pendingSaves, function() {
					pending[this[0]] = true;
				} );
				possibleBacklinkDestinations = $.grep( pageTitles, function( t, ii) {
					if ( t == targetPage ) {
						return true;
					}
					return removeDisam(t) != targetPage;
				} );
				// Only incoming links from pages we haven't seen yet and we aren't currently
				// saving (displayedPages is reset when the tool is closed and opened again,
				// while the list of pending changes isn't; if the edit cooldown is disabled,
				// it will be empty)
				links = $.grep( backlinks, function( el, ii ) {
					return !displayedPages[el] && !pending[el];
				} );
				if ( links.length === 0 ) {
					updateContext();
				} else {
					doPage();
				}
			} ).fail( error );
		} else {
			currentPageTitle = links.shift();
			displayedPages[currentPageTitle] = true;
			toggleActionButtons( false );
			loadPage( currentPageTitle ).done( function( data ) {
				currentPageParameters = data;
				currentLink = null;
				doLink();
			} ).fail( error );
		}
	};
 
	/*
	 * Find and ask the user to fix a single incoming link to the disambiguation ("target")
	 * page
	 */
	var doLink = function() {
		currentLink = extractLinkToPage( currentPageParameters.content,
			possibleBacklinkDestinations, currentLink ? currentLink.end : 0 );
		if ( currentLink ) {
			updateContext();
		} else {
			doPage();
		}
	};
 
	/*
	 * Replace the target of a link with a different one
	 * pageTitle: New link target
	 * extra: Additional text after the link (optional)
	 * summary: Change summary (optional)
	 */
	var chooseReplacement = function( pageTitle, extra, summary ) {
		if ( choosing ) {
			choosing = false;
			if ( !summary ) {
				if ( pageTitle ) {
					summary = txt.summaryChanged.replace( '$1', pageTitle );		
				} else {
					summary = txt.summaryOmitted;
				}
			}
			addChange( currentPageTitle, currentPageParameters, currentPageParameters.content, currentLink, summary );
			if ( pageTitle && ( pageTitle !== getTargetPage() || extra ) ) {
				currentPageParameters.content =
					replaceLink( currentPageParameters.content, pageTitle, currentLink, extra || '' );
			}
			doLink();
		}
	};
 
	/*
	 * Replace the link with an explicit link to the disambiguation page
	 */
	var chooseIntentionalLink = function() {
		var disamTitle = cfg.disamFormat.replace( '$1', getTargetPage() );
		chooseReplacement( disamTitle, '', txt.summaryIntentional );
	};
 
	/*
	 * Prompt for an alternative link target and use it as a replacement
	 */
	var chooseTitleFromPrompt = function() {
		var title = prompt( txt.titleAsTextPrompt );
		if ( title !== null ) {
			chooseReplacement( title );
		}
	};
 
	/*
	 * Remove the current link, leaving the text unchanged
	 */
	var chooseLinkRemoval = function() {
		if ( choosing ) {
			var summary = txt.summaryRemoved;
			addChange( currentPageTitle, currentPageParameters, currentPageParameters.content, currentLink, summary );
			currentPageParameters.content = removeLink( currentPageParameters.content, currentLink );
			doLink();
		}
	};
 
	/*
	 * Add a "disambiguation needed" template after the link
	 */
	var chooseDisamNeeded = function() {
		chooseReplacement( currentLink.title, cfg.disamNeededText, txt.summaryHelpNeeded );
	};
 
	/*
	 * Undo the last change
	 */
	var undo = function() {
		if ( pageChanges.length !== 0 ) {
			var lastPage = pageChanges[pageChanges.length - 1];
			if ( currentPageTitle !== lastPage.title ) {
				links.unshift( currentPageTitle );
				currentPageTitle = lastPage.title;
			}
			currentPageParameters = lastPage.page;
			currentPageParameters.content = lastPage.contentBefore.pop();
			currentLink = lastPage.links.pop();
			lastPage.summary.pop();
			if ( lastPage.contentBefore.length === 0 ) {
				pageChanges.pop();
			}
			updateContext();
		}
	};
 
	/*
	 * Omit the current link without making a change
	 */
	var omit = function() {
		chooseReplacement( null );
	};
 
	/*
	 * Save all the pending changes and restart the tool.
	 */
	var refresh = function() {
		saveAndEnd();
		start();
	};
 
	/*
	 * Enable or disable the buttons that can perform actions on a page or change the current link.
	 * enabled: Whether to enable or disable the buttons
	 */
	var toggleActionButtons = function( enabled ) {
		var affectedButtons = [ui.omitButton, ui.titleAsTextButton, ui.removeLinkButton,
			ui.intentionalLinkButton, ui.disamNeededButton, ui.undoButton];
		$.each( affectedButtons, function( ii, button ) {
			button.prop( 'disabled', !enabled );
		} );	
	};
 
	/*
	 * Show or hide the 'no more links' message
	 * show: Whether to show or hide the message
	 */
	var toggleFinishedMessage = function( show ) {
		toggleActionButtons( !show );
		ui.undoButton.prop( 'disabled', pageChanges.length === 0 );
		ui.finishedMessage.toggle( show );
		ui.pageTitleLine.toggle( !show );
		ui.context.toggle( !show );
	};
 
	var togglePendingEditBox = function( show ) {
		if ( pendingEditBox === null ) {
			pendingEditBox = $( '<div></div>' ).addClass( 'disamassist-box disamassist-pendingeditbox' );
			pendingEditBoxText = $( '<div></div>' );
			pendingEditBox.append( pendingEditBoxText ).hide();
			if ( editLimit ) {
				pendingEditBox.append( $( '<div></div>' ).text( txt.pendingEditBoxLimited )
					.addClass( 'disamassist-subtitle' ) );
			}
			$( '#mw-content-text' ).before( pendingEditBox );
			updateEditCounter();
		}
		if ( show ) {
			pendingEditBox.fadeIn();
		} else {
			pendingEditBox.fadeOut();
		}
	};
 
	var notifyCompletion = function() {
		var oldTitle = document.title;
		document.title = txt.notifyCharacter + document.title;
		$( document.body ).one( 'mousemove', function() {
			document.title = oldTitle;
		} );
	};
 
	/*
	 * Update the displayed information to match the current link
	 * or lack thereof
	 */
	var updateContext = function() {
		updateEditCounter();
		if ( !currentLink ) {
			toggleFinishedMessage( true );
		} else {
			ui.pageTitleLine.html( txt.pageTitleLine.replace( '$1',
				mw.util.getUrl( currentPageTitle, {redirect: 'no'} ) ).replace( '$2', mw.html.escape( currentPageTitle ) ) );
			var context = extractContext( currentPageParameters.content, currentLink );
			ui.context.empty()
				.append( $( '<span></span>' ).text( context[0] ) )
				.append( $( '<span></span>' ).text( context[1] ).addClass( 'disamassist-inclink' ) )
				.append( $( '<span></span>' ).text( context[2] ) );
			var numLines = Math.ceil( ui.context.height() / parseFloat( ui.context.css( 'line-height' ) ) );
			if ( numLines < cfg.numContextLines ) {
				// Add cfg.numContextLines - numLines + 1 line breaks, so that the total number
				// of lines is cfg.numContextLines
				ui.context.append( new Array( cfg.numContextLines - numLines + 2 ).join( '<br>' ) );
			}
			toggleFinishedMessage( false );
			ui.undoButton.prop( 'disabled', pageChanges.length === 0 );
			ui.removeLinkButton.prop( 'disabled', currentPageParameters.redirect );
			ui.intentionalLinkButton.prop( 'disabled', currentPageParameters.redirect );
			ui.disamNeededButton.prop( 'disabled', currentPageParameters.redirect || currentLink.hasDisamTemplate );
			choosing = true;
		}
	};
 
	/*
	 * Update the count of pending changes
	 */
	var updateEditCounter = function() {
		if ( ui.pendingEditCounter ) {
			ui.pendingEditCounter.text( txt.pendingEditCounter.replace( '$1', editCount )
				.replace( '$2', countActuallyChangedFullyCheckedPages() ) );
		}
		if ( pendingEditBox ) {
			if ( editCount === 0 && !running ) {
				togglePendingEditBox( false );
				notifyCompletion();
			}
			var textContent = editCount;
			if ( editLimit ) {
				textContent = txt.pendingEditBoxTimeEstimation.replace( '$1', editCount )
					.replace( '$2', secondsToHHMMSS( cfg.editCooldown * editCount ) );
			}
			pendingEditBoxText.text( txt.pendingEditBox.replace( '$1', textContent ) );
		}
	};
 
	/*
	 * Apply the changes made to an "origin" page
	 * pageChange: Change that will be saved
	 */
	var applyChange = function( pageChange ) {
		if ( pageChange.page.content !== pageChange.contentBefore[0] ) {
			editCount++;
			var changeSummaries = pageChange.summary.join( txt.summarySeparator );
			var summary = txt.summary.replace( '$1', getTargetPage() ).replace( '$2', changeSummaries );
			var save = editLimit ? saveWithCooldown : savePage;
			save( pageChange.title, pageChange.page, summary, true, true ).always( function() {
				if ( editCount > 0 ) {
					editCount--;
				}
				updateEditCounter();
			} ).fail( error );
			updateEditCounter();
		}
	};
 
	/*
	 * Save all the pending changes
	 */
	var applyAllChanges = function() {
		for ( var ii = 0; ii < pageChanges.length; ii++ ) {
			applyChange( pageChanges[ii] );
		}
		pageChanges = [];
	};
 
	/*
	 * Record a new pending change
	 * pageTitle: Title of the page
	 * page: Content of the page
	 * oldContent: Content of the page before the change
	 * link: Link that has been changed
	 * summary: Change summary
	 */
	var addChange = function( pageTitle, page, oldContent, link, summary ) {
		if ( ( pageChanges.length === 0 ) || ( pageChanges[pageChanges.length - 1].title !== pageTitle ) ) {
			pageChanges.push( {
				title: pageTitle,
				page: page,
				contentBefore: [],
				links: [],
				summary: []
			} );
		}
		var lastPageChange = pageChanges[pageChanges.length - 1];
		lastPageChange.contentBefore.push( oldContent );
		lastPageChange.links.push( link );
		lastPageChange.summary.push( summary );
	};
 
	/*
	 * Check whether actual changes are stored in the history array
	 */
	var checkActualChanges = function() {
		return countActualChanges() !== 0;
	};
 
	/*
	 * Return the number of entries in the history array that represent actual changes
	 */
	var countActualChanges = function() {
		var changeCount = 0;
		for ( var ii = 0; ii < pageChanges.length; ii++ ) {
			if ( pageChanges[ii].page.content !== pageChanges[ii].contentBefore[0] ) {
				changeCount++;
			}
		}
		return changeCount;		
	};
 
	/*
	 * Return the number of changed pages in the history array, ignoring the last entry
	 * if we aren't done with that page yet
	 */
	var countActuallyChangedFullyCheckedPages = function() {
		var changeCount = countActualChanges();
		if ( pageChanges.length !== 0 ) {
			var lastChange = pageChanges[pageChanges.length - 1];
			if ( lastChange.title === currentPageTitle && currentLink !== null
					&& lastChange.page.content !== lastChange.contentBefore[0] ) {
				changeCount--;
			}
		}
		return changeCount;
	};
 
	/*
	 * Find the links to disambiguation options in a disambiguation page
	 */
	var getDisamOptions = function() {
		return $( '#mw-content-text a' ).filter( function() {
			return !!extractPageName( $( this ) );
		} );
	};
 
	/*
	 * Save all the pending changes and close the tool
	 */
	var saveAndEnd = function() {
		applyAllChanges();
		end();
	};
 
	/*
	 * Close the tool
	 */
	var end = function() {
		var currentToolUI = ui.display;
		choosing = false;
		running = false;
		startLink.removeClass( 'selected' );
		$( '.disamassist-optionmarker' ).remove();
		currentToolUI.fadeOut( { complete: function() {
			currentToolUI.remove();
			if ( editCount !== 0 ) {
				togglePendingEditBox( true );
			}
		} } );
	};
 
	/*
	 * Display an error message
	 */
	var error = function( errorDescription ) {
		var errorBox = $( '<div></div>' ).addClass( 'disamassist-box disamassist-errorbox' );
		errorBox.text( txt.error.replace( '$1', errorDescription ) );
		errorBox.append( createButton( txt.dismissError, function() {
			errorBox.fadeOut();
		} ).addClass( 'disamassist-errorbutton' ) );
		var uiIsInPlace = ui && $.contains( document.documentElement, ui.display[0] );
		var nextElement = uiIsInPlace ? ui.display : $( '#mw-content-text' );
		nextElement.before( errorBox );
		errorBox.hide().fadeIn();
	}
 
	/*
	 * Change a link so that it points to the title
	 * text: The wikitext of the whole page
	 * title: The new destination of the link
	 * link: The link that will be modified
	 * extra: Text that will be added after the link (optional)
	 */
	var replaceLink = function( text, title, link, extra ) {
		var newContent;
		if ( isSamePage( title, link.description ) ) {
			// [[B|A]] should be replaced with [[A]] rather than [[A|A]]
			newContent = link.description;
		} else {
			newContent = title + '|' + link.description;
		}
		var linkStart = text.substring( 0, link.start );
		var linkEnd = text.substring( link.end );
		return linkStart + '[[' + newContent + ']]' + link.afterDescription + ( extra || '' ) + linkEnd;
	};
 
	/*
	 * Remove a link from the text
	 * text: The wikitext of the whole page
	 * link: The link that will be removed
	 */
	var removeLink = function( text, link ) {
		var linkStart = text.substring( 0, link.start );
		var linkEnd = text.substring( link.end );
		return linkStart + link.description + link.afterDescription + linkEnd;
	};
 
	/*
	 * Extract a link from a string in wiki format,
	 * starting from a given index. Return a link if one can be found,
	 * otherwise return null. The "link" includes "disambiguation needed"
	 * templates inmediately following the link proper
	 * text: Text from which the link will be extracted
	 * lastIndex: Index from which the search will start
	 */
	var extractLink = function( text, lastIndex ) {
		// FIXME: Not an actual title regex (lots of false positives
		// and some false negatives), but hopefully good enough.
		var titleRegex = /\[\[(.*?)(?:\|(.*?))?]]/g;
		// Ditto for the template regex. Disambiguation link templates
		// should be simple enough for this not to matter, though.
		var templateRegex = /^(\w*[.,:;?!)}\s]*){{\s*([^|{}]+?)\s*(?:\|[^{]*?)?}}/;
		titleRegex.lastIndex = lastIndex;
		var match = titleRegex.exec( text );
		if ( match !== null && match.index !== -1 ) {
			var possiblyAmbiguous = true;
			var hasDisamTemplate = false;
			var end = match.index + 4 + match[1].length + ( match[2] ? 1 + match[2].length : 0 );
			var afterDescription = '';
			var rest = text.substring( end );
			var templateMatch = templateRegex.exec( rest );
			if ( templateMatch !== null ) {
				var templateTitle = getCanonicalTitle( templateMatch[2] );
				if ( $.inArray( templateTitle, cfg.disamLinkTemplates ) !== -1 ) {
					end += templateMatch[0].length;
					afterDescription = templateMatch[1].replace(/\s$/, '');
					hasDisamTemplate = true;
				} else if ( $.inArray( templateTitle, cfg.disamLinkIgnoreTemplates ) !== -1 ) {
					possiblyAmbiguous = false;
				}
			}
			return {
				start: match.index,
				end: end,
				possiblyAmbiguous: possiblyAmbiguous,
				hasDisamTemplate: hasDisamTemplate,
				title: match[1],
				description: match[2] ? match[2] : match[1],
				afterDescription: afterDescription
			};
		}
		return null;
	};
 
	/*
	 * Extract a link to one of a number of destination pages from a string 
	 * ("text") in wiki format, starting from a given index ("lastIndex").
	 * "Disambiguation needed" templates are included as part of the links,
	 * while "R to disambiguation page" and 
	 * text: Page in wiki format
	 * destinations: Array of page titles to look for
	 * lastIndex: Index from which the search will start
	 */
	var extractLinkToPage = function( text, destinations, lastIndex ) {
		var link, title;
		do {
			link = extractLink( text, lastIndex );
			if ( link !== null ) {
				lastIndex = link.end;
				title = getCanonicalTitle( link.title );
			}
		} while ( link !== null
			&& ( !link.possiblyAmbiguous || $.inArray( title, destinations ) === -1 ) );
		return link;
	};
 
	/*
	 * Find the "target" page: either the one we are in or the "main" one found by extracting
     * the title from ".* (disambiguation)" or whatever the appropiate local format is
	 */
	var getTargetPage = function() {
		var title = mw.config.get( 'wgTitle' );
		return forceSamePage ? title : removeDisam(title);
	};
 
	/*
	 * Extract a "main" title from ".* (disambiguation)" or whatever the appropiate local format is
	 */
	var removeDisam = function( title ) {
		var match = new RegExp( cfg.disamRegExp ).exec( title );
		return match ? match[1] : title;
	};
 
	/*
	 * Check whether two page titles are the same
	 */
	var isSamePage = function( title1, title2 ) {
		return getCanonicalTitle( title1 ) === getCanonicalTitle( title2 );
	};
 
	/*
	 * Return the 'canonical title' of a page
	 */
	var getCanonicalTitle = function( title ) {
		try {
			title = new mw.Title( title ).getPrefixedText();
		} catch ( ex ) {
			// mw.Title seems to be buggy, and some valid titles are rejected
			// FIXME: This may cause false negatives	
		}
		return title;
	};
 
	/*
	 * Extract the context around a given link in a text string
	 */
	var extractContext = function( text, link ) {
		var contextStart = link.start - cfg.radius;
		var contextEnd = link.end + cfg.radius;
		var contextPrev = text.substring( contextStart, link.start );
		if ( contextStart > 0 ) {
			contextPrev = txt.ellipsis + contextPrev;
		}
		var contextNext = text.substring( link.end, contextEnd );
		if ( contextEnd < text.length ) {
			contextNext = contextNext + txt.ellipsis;
		}
		return [contextPrev, text.substring( link.start, link.end ), contextNext];
	};
 
	/*
	 * Extract the prefixed page name from a link
	 */
	var extractPageName = function( link ) {
		var pageName = extractPageNameRaw( link );
		if ( pageName ) {
			var sectionPos = pageName.indexOf( '#' );
			var section = '';
			if ( sectionPos !== -1 ) {
				section = pageName.substring( sectionPos );
				pageName = pageName.substring( 0, sectionPos );
			}
			return getCanonicalTitle( pageName ) + section;
		} else {
			return null;
		}
	};
 
	/*
	 * Extract the page name from a link, as is
	 */
	var extractPageNameRaw = function( link ) {
		if ( !link.hasClass( 'image' ) ) {
			var href = link.attr( 'href' );
			if ( link.hasClass( 'new' ) ) { // "Red" link
				if ( href.indexOf( mw.config.get( 'wgScript' ) ) === 0 ) {
					return mw.util.getParamValue( 'title', href );
				}
			} else {
				var regex = mw.config.get( 'wgArticlePath' ).replace( '$1', '(.*)' );
				var regexResult = RegExp( '^' + regex + '$' ).exec( href );
				if ( $.isArray( regexResult ) && regexResult.length > 1 ) {
					return decodeURIComponent( regexResult[1] );
				}
			}
		}
		return null;
	};
 
	/*
	 * Check whether this is a disambiguation page
	 */
	var isDisam = function() {
		var categories = mw.config.get( 'wgCategories' );
		for ( var ii = 0; ii < categories.length; ii++ ) {
			if ( $.inArray( categories[ii], cfg.disamCategories ) !== -1 ) {
				return true;
			}
		}
		return false;
	};
 
	var secondsToHHMMSS = function( totalSeconds ) {
		var hhmmss = '';
		var hours = Math.floor( totalSeconds / 3600 );
		var minutes = Math.floor( totalSeconds % 3600 / 60 );
		var seconds = Math.floor( totalSeconds % 3600 % 60 );
		if ( hours >= 1 ) {
			hhmmss = pad( hours, '0', 2 ) + ':';
		}
		hhmmss += pad( minutes, '0', 2 ) + ':' + pad( seconds, '0', 2 );
		return hhmmss;
	};
 
	var pad = function( str, z, width ) {
		str = str.toString();
		if ( str.length >= width ) {
			return str;
		} else {
			return new Array( width - str.length + 1 ).join( z ) + str;
		}
	}
 
	/*
	 * Create a new button
	 * text: Text that will be displayed on the button
	 * onClick: Function that will be called when the button is clicked
	 */
	var createButton = function( text, onClick ) {
		var button = $( '<input></input>', {'type': 'button', 'value': text } );
		button.addClass( 'disamassist-button' ).click( onClick );
		return button;
	};
 
	/*
	 * Given a page title and an array of possible redirects {from, to} ("canonical titles"), find the page
	 * at the end of the redirect chain, if there is one. Otherwise, return the page title that was passed
	 */
	var resolveRedirect = function( pageTitle, possibleRedirects ) {
		var appliedRedirect = true;
		var visitedPages = {};
		var currentPage = getCanonicalTitle( pageTitle );
		while ( appliedRedirect ) {
			appliedRedirect = false;
			for ( var ii = 0; ii < possibleRedirects.length; ii++ ) {
				if ( possibleRedirects[ii].from === currentPage ) {
					if ( visitedPages[possibleRedirects[ii].to] ) {
						// Redirect chain detected
						return pageTitle;
					}
					visitedPages[currentPage] = true;
					appliedRedirect = true;
					currentPage = possibleRedirects[ii].to;
				}
			}
		}
		// No redirect rules applied for an iteration of the outer loop:
		// no more redirects. We are done
		return currentPage;
	};
 
	/*
	 * Fetch the incoming links to a page. Returns a jQuery promise
	 * (success - array of titles of pages that contain links to the target page and
	 * array of "canonical titles" of possible destinations of the backlinks (either
	 * the target page or redirects to the target page), failure - error description)
	 * page: Target page
	 */
	var getBacklinks = function( page ) {
		var dfd = new $.Deferred();
		var api = new mw.Api();
		api.get( {
			'action': 'query',
			'list': 'backlinks',
			'bltitle': page,
			'blredirect': true,
			'bllimit': cfg.backlinkLimit,
			'blnamespace': cfg.targetNamespaces.join( '|' )
		} ).done( function( data ) {
			// There might be duplicate entries in some corner cases. We don't care,
			// since we are going to check later, anyway
			var backlinks = [];
			var linkTitles = [getCanonicalTitle( page )];
			$.each( data.query.backlinks, function() {
				backlinks.push( this.title );
				if ( this.redirlinks ) {
					linkTitles.push( this.title );
					$.each( this.redirlinks, function() {
						backlinks.push( this.title );
					} );
				}
			} );
			dfd.resolve( backlinks, linkTitles );
		} ).fail( function( code, data ) {
			dfd.reject( txt.getBacklinksError.replace( '$1', code ) );
		} );
		return dfd.promise();
	};
 
	/*
	 * Download a list of redirects for some pages. Returns a jQuery callback (success -
	 * array of redirects ({from, to}), failure - error description )
	 * pageTitles: Array of page titles
	 */
	var fetchRedirects = function( pageTitles ) {
		var dfd = new $.Deferred();
		var api = new mw.Api();
		var currentTitles = pageTitles.slice( 0, cfg.queryTitleLimit );
		var restTitles = pageTitles.slice( cfg.queryTitleLimit );
		api.get( {
			action: 'query',
			titles: currentTitles.join( '|' ),
			redirects: true
		} ).done( function( data ) {
			var theseRedirects = data.query.redirects ? data.query.redirects : [];
			if ( restTitles.length !== 0 ) {
				fetchRedirects( restTitles ).done( function( redirects ) {
					dfd.resolve( theseRedirects.concat( redirects ) );
				} ).fail( function( description ) {
					dfd.reject( description );
				} );
			} else {
				dfd.resolve( theseRedirects );
			}
		} ).fail( function( code, data ) {
			dfd.reject( txt.fetchRedirectsError.replace( '$1', code ) );
		} );
		return dfd.promise();
	};
 
	/*
	 * Download the list of user rights for the current user. Returns a
	 * jQuery promise (success - array of right names, error - error description)
	 */
	var fetchRights = function() {
		var dfd = $.Deferred();
		var api = new mw.Api();
		api.get( {
			action: 'query',
			meta: 'userinfo',
			uiprop: 'rights'
		} ).done( function( data ) {
			dfd.resolve( data.query.userinfo.rights );
		} ).fail( function( code, data ) {
			dfd.reject( txt.fetchRightsError.replace( '$1', code ) );
		} );
		return dfd.promise();
	};
 
	/*
	 * Load the raw page text for a given title. Returns a jQuery promise (success - page
	 * content, failure - error description)
	 * pageTitle: Title of the page
	 */
	var loadPage = function( pageTitle ) {
		var dfd = new $.Deferred();
		var api = new mw.Api();
		api.get( {
			action: 'query',
			titles: pageTitle,
			intoken: 'edit',
			prop: 'info|revisions',
			rvprop: 'timestamp|content'
		} ).done( function( data ) {
			var pages = data.query.pages;
			for ( var key in pages ) {
				if ( pages.hasOwnProperty( key ) ) {
					break;
				}
			}
			var rawPage = data.query.pages[key];
			var page = {};
			page.redirect = rawPage.redirect !== undefined;
			page.missing = rawPage.missing !== undefined;
			if ( rawPage.revisions ) {
				page.content = rawPage.revisions[0]['*'];
				page.baseTimeStamp = rawPage.revisions[0].timestamp;
			} else {
				page.content = '';
				page.baseTimeStamp = null;
			}
			page.startTimeStamp = rawPage.starttimestamp;
			page.editToken = rawPage.edittoken;
			dfd.resolve( page );
		} ).fail( function( code, data ) {
			dfd.reject( txt.loadPageError.replace( '$1', pageTitle ).replace( '$2', code ) );
		} );
		return dfd.promise();
	};
 
	/*
	 * Register changes to a page, to be saved later. Returns a jQuery promise
	 * (success - no params, failure - error description). Takes the same parameters
	 * as savePage 
	 */
	var saveWithCooldown = function() {
		var deferred = new $.Deferred();
		pendingSaves.push( {args: arguments, dfd: deferred} );
		if ( !runningSaves ) {
			checkAndSave();
		}
		return deferred.promise();
	};
 
	/*
	 * Save the first set of changes in the list of pending changes, providing that
	 * enough time has passed since the last edit
	 */
	var checkAndSave = function() {
		if ( pendingSaves.length === 0 ) {
			runningSaves = false;
			return;
		}
		runningSaves = true;
		var millisSinceLast = new Date().getTime() - lastEditMillis;
		if ( millisSinceLast < cfg.editCooldown * 1000 ) {
			setTimeout( checkAndSave, cfg.editCooldown * 1000 - millisSinceLast );
		} else {
			// The last edit started at least cfg.editCooldown seconds ago
			var save = pendingSaves.shift();
			savePage.apply( this, save.args ).done( function() {
				checkAndSave();
				save.dfd.resolve();
			} ).fail( function( description ) {
				checkAndSave();
				save.dfd.reject( description );
			} );
			// We'll use the time since the last edit started
			lastEditMillis = new Date().getTime();
		}
	};
 
	/*
	 * Save the changes made to a page. Returns a jQuery promise (success - no params,
	 * failure - error description)
	 * pageTitle: Title of the page
	 * page: Page data
	 * summary: Summary of the changes made to the page
	 * minorEdit: Whether to mark the edit as 'minor'
	 * botEdit: Whether to mark the edit as 'bot'
	 */
	var savePage = function( pageTitle, page, summary, minorEdit, botEdit ) {
		var dfd = new $.Deferred();
		var api = new mw.Api();
		api.post( {
			action: 'edit',
			title: pageTitle,
			token: page.editToken,
			text: page.content,
			basetimestamp: page.baseTimeStamp,
			starttimestamp: page.startTimeStamp,
			summary: summary,
			watchlist: cfg.watch,
			minor: minorEdit,
			bot: botEdit
		} ).done( function() {
			dfd.resolve();
		} ).fail( function( code, data ) {
			dfd.reject( txt.savePageError.replace( '$1', pageTitle ).replace( '$2', code ) );
		} );
		return dfd.promise();
	};
 
	install();
} )( mediaWiki, jQuery );

var xpagehistory = {
  loadinganimation : 0,
  execute : function () {
  	if (mw.config.get('wgArticleId') === 0) return; // no deleted articles, no special pages
  	if (mw.config.get('wgCurRevisionId') != mw.config.get('wgRevisionId')) return; // only current revision
  	$("<div id='xtools' style='font-size:84%; line-height:1.2em; margin:0 0 0.4em 0.2em; width:auto;'><span id='xtoolsresult'></span><span id='xtoolsloading'>.</span></div>").insertBefore("#contentSub");
  	loadinganimation = window.setInterval( function() { if ($("#xtoolsloading").html() == ".&nbsp;&nbsp;") $("#xtoolsloading").html("&nbsp;.&nbsp;"); else if ($("#xtoolsloading").html() == "&nbsp;.&nbsp;") $("#xtoolsloading").html("&nbsp;&nbsp;."); else $("#xtoolsloading").html(".&nbsp;&nbsp;"); }, 300);
  	mw.loader.load("//tools.wmflabs.org/xtools/api.php?pageid=" +  mw.config.get('wgArticleId') + "&db=" + mw.config.get('wgDBname') + "&nsid=" + mw.config.get('wgNamespaceNumber') + "&pagetitle=" + mw.config.get('wgPageName') + "&wditemid=" + mw.config.get('wgWikibaseItemId') + "&uselang=" + mw.config.get('wgContentLanguage') );
  },
  resultloaded : function( res ) {
  	$("#xtoolsresult").html(res);
  	this.stoploading();
  },
  stoploading : function() {
  	clearInterval(loadinganimation);
  	$('#xtoolsloading').remove();
  }
}
if ( (mw.config.get('wgAction') == "view") ) $( xpagehistory.execute );
/*</nowiki>*/