User:Ringkjøbing/monobook.js

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
// include comfortable javascript editor by cacycle
 
// levels of undo (each level holds the whole text)
var undoBufferMax = 20;
 
// text and popup title of summary buttons
var summaryButtons = [
  ['Copyedit',  'Copyedit'],
  ['Linkfix',   'Linkfix'],
  ['Vandal',    'Reverting vandalism'],
  ['Format',    'Formatting source text'],
  ['Cap lists', 'Capitalizing (link) lists as per [[Wikipedia:List]] and [[Wikipedia:Manual_of_Style]]'] 
];
 
// background color of preview box
var previewBackground = '#ffffdd';
 
// loads the editor
document.write('<script type="text/javascript" src=" http://en.wikipedia.org/w/index.php?title=User:Cacycle/editor.js&action=raw&ctype=text/javascript&dontcountme=s"></script>');
 
// Live Preview ([[User:Pilaf/Live Preview]], )
wpUserName   = 'Cacycle'; // User name to display in signatures 
wpShowImages = true; // Enable downloading and displaying of images
document.write('<script type="text/javascript" src=" http://en.wikipedia.org/w/index.php?title=User:Pilaf/livepreview.js&action=raw&ctype=text/javascript&dontcountme=s"></script>');
 
// installs the editor and other tools after loading the page 
window.onload = Main;
function Main() {
  SetupEditor();
//  othertools();
}
// NON-NAMESPACED FUNCTION NAMES ARE DEPRECATED

// <pre><nowiki>

/////////////////////////////////////////////////////////////
// STRING UTILITY FUNCTIONS

util = new Object();

util.trimSpaces = function(s) {
    if (!s) return s;
    s = s.replace(/^\s+/,'');
    s = s.replace(/\s+$/,'');
    return s;
}
trimspaces = util.trimSpaces;

util.trimLines = function(s) {
    return s.replace(/^\n+/, '').replace(/\n+$/, '');
}
trim_lines = util.trimLines;

util.stringQuoteEscape = function(str) {
    if (!str) return str;
    return "'" + str.replace(/\'/g, '\\\'').replace(/\%27/g, '\\\'') + "'";
}
string_quote_escape = util.stringQuoteEscape;

// wiki article name escaping
util.wpaEscape = function(s) {
    // encodeURIComponent is better than 'escape' for unicode chars;
    // it also escapes '+'.
    // Don't escape ':'
    return encodeURIComponent(s.replace(/ /g,'_')).replace(/%3A/g,':').replace(/%2F/g,'/');
}
wpaescape = wpaencode = util.wpaEscape;

util.wpaDecode = function(s) {
    return decodeURIComponent(s).replace(/_/g,' ');
}
wpaunescape = wpadecode = util.wpaDecode;

util.urlGetPath = function(s) {
    return s.replace(/^http:\/\/[^\/]+/, '');
}
url_getpath = util.urlGetPath;

// Return an authentication token useful to pass through URL for automatic
// edits, to prevent XSS attacks.
//
// requires md5.js
util.makeAuthToken = function() {
    // I'd like to use something like readCookie('enwikiToken') + '%' +
    // readCookie('enwiki_session')), but not sure how to get it cross-wiki
    // compatible, so just use entire cookie for now.

    return hex_md5('auth_token:'+Array.join(arguments, '%') +
                   '%%' + document.cookie);
}
makeAuthToken = util.makeAuthToken;

////////////////////////////////////////////////////////////
// DOM UTILITY FUNCTIONS
util.getElementsByClass = function(searchClass, node, tag) {
    var classElements = [];
    if (node == null)
        node = document;
    if (tag == null)
        tag = '*';
    var els = node.getElementsByTagName(tag);
    var elsLen = els.length;
    var pattern = new RegExp("(^|\\s)"+searchClass+"(\\s|$)");
    for (var i = 0; i < elsLen; i++) {
        if (pattern.test(els[i].className) ) {
            classElements.push(els[i]);
        }
    }
    return classElements;
}
getElementsByClass = util.getElementsByClass;

util.addClass = function(node, cls) {
    node.className += ' '+cls;
}
addClass = util.addClass;

util.removeClass = function(node, cls) {
    node.className = node.className.replace(
        new RegExp("(^|\\s)"+cls+"(\\s|$)",'g'), ' ');
}
removeClass = util.removeClass;

util.addNodeBefore = function(node, newnode) {
    node.parentNode.insertBefore(newnode, node);
    return newnode;
}
add_before = util.addNodeBefore;

util.addNodeBefore = function(node, newnode) {
    if (node.nextSibling) {
        node.parentNode.insertBefore(newnode, node.nextSibling);
    } else {
        node.parentNode.appendChild(newnode);
    }
    return newnode;
}
add_after = util.addNodeBefore;

// return nodes in [node_start, node_end)
util.getNodesInRange = function(node_start, node_end) {
    var nodes = [];
    while (node_start != node_end) {
        nodes.push(node_start);
        node_start = node_start.nextSibling;
        if (!node_start) return null; // didn't reach node_end!
    }
    return nodes;
}
getNodesInRange = util.getNodesInRange;

util.removeNodesInRange = function(node_start, node_end) {
    if (!node_end) {
        alert("## removeNodesInRange: node_end==null");
        return null;
    }
    if (!util.getNodesInRange(node_start, node_end)) {
        alert("## removeNodesInRange: range does not terminate");
        return null;
    }
    var parent = node_start.parentNode;
    while (node_start != node_end) {
        var n = node_start.nextSibling; // save before it gets clobbered
        parent.removeChild(node_start);
        node_start = n;
        if (!node_start) return null;
    }
}
removeNodesInRange = util.removeNodesInRange;

util.createHref = function(href, title, inner) {
    var a = document.createElement('a');
    a.href = href;
    a.title = title;
    a.innerHTML = inner;
    return a;
}
createHref = util.createHref;

util.findHref = function(href) {
    href = util.wpaEscape(util.wpaDecode(href));
    var links=document.links;
    for(i=0;i<links.length;++i) {
        // unescape and reescape to ensure canonical escaping
        if (util.wpaEscape(util.wpaDecode(links[i].href)) == href) return links[i];
    }
    return null;
}
findHref = util.findHref;

// insert a new node as parent of node
util.insertNode = function(node, newNode) {
    if (!node) return null;

    node.parentNode.replaceChild(newNode, node);
    newNode.appendChild(node);
    return newNode;
}
insertNode = util.insertNode;

util.appendChildren = function(node, newNodes) {
    for (var i in newNodes) {
        node.appendChild(newNodes[i]);
    }
}
appendChild = util.appendChild;

util.hookEventObj = function(obj, hookName, hookFunct) {
    if (!obj) return;

    if (obj.addEventListener)
        obj.addEventListener(hookName, hookFunct, false);
    else if (obj.attachEvent)
        obj.attachEvent("on" + hookName, hookFunct);
}
hookEventObj = util.hookEventObj;

util.copyArray = function(a) {
    var r = [];
    for (var i=0; i < a.length; i++)
        r[i] = a[i];
    return r;
}
copyArray = util.copyArray;

// add a span around a node if there isn't one.
util.ensureSpan = function(node) {
    if (node.parentNode.nodeName == 'SPAN') {
        return node.parentNode;
    }
    return insertNode(node, document.createElement('span'));
}
ensureSpan = util.ensureSpan;

////////////////////////////////////////////////////////////
// STYLESHEET FUNCTIONS
util.addStylesheetRule = function(tag, style) {
    var ss = document.styleSheets[0];
    if (ss.insertRule) {
        ss.insertRule(tag + '{' + style + '}', ss.cssRules.length);
    } else if (ss.addRule) {
        ss.addRule(tag, style);
    }
}
addStylesheetRule = util.addStylesheetRule;

////////////////////////////////////////////////////////////
// AJAX FUNCTIONS

// cross-platform
util.HTTPClient = function() {
    var http;
    if(window.XMLHttpRequest) {
        http = new XMLHttpRequest();
    } else if (window.ActiveXObject) {
        try {
            http = new ActiveXObject("Msxml2.XMLHTTP");
        } catch (e) {
            try {
                http = new ActiveXObject("Microsoft.XMLHTTP");
            } catch (E) {
                http = false;
            }
        }
    }
    return http;
}
HTTPClient = util.HTTPClient;

util.asyncDownloadXML = function(url, callback, props) {
    var req = util.HTTPClient();
    if (!req) return null;
    // add optional arguments
    if (props) {
        for (var k in props) {
            req[k] = props[k];
        }
    }
    req.open("GET", url, true);
    req.overrideMimeType('text/xml');
    // using onload instead of onreadystatechange allows multiple asynchronous requests
    // TODO: since we now have access to 'req' as a variable, we could change back.
    // Is there any advantage to using onreadystatechange?
    req.onload = function(event) {
        var req = event.target;
        if (req.readyState == 4) callback(req);
    };
    req.send(null);
    return req;
}
asyncDownloadXML = util.asyncDownloadXML;

util.buildParams = function(paramArray) {
    var params = '';
    for (k in paramArray) {
        v = paramArray[k];
        // if v is a Boolean then the form was a checkbox.
        // unchecked checkboxes should not add any input fields.
        if (v == false) continue;
        if (v == true) v = 'on';
        params += '&' + k + '=' + encodeURIComponent(v);
    }
    params = params.replace(/^&/,'');
    return params;
}
buildParams = util.buildParams;

util.addFormHiddenParams = function(newform, d) {
    for (var k in d) {
        v = d[k];
        // if v is a Boolean then the form was a checkbox.
        // unchecked checkboxes should not add any input fields.
        if (v == false) continue;
        if (v == true) v = 'on';
        var t = document.createElement('input');
        t.type = 'hidden';
        t.name = k;
        t.value = d[k];
        newform.appendChild(t);
    }
    return newform;
}
addFormHiddenParams = util.addFormHiddenParams;

util.asyncPostXML = function(url, parameters, callback, props) {
    var req = util.HTTPClient();
    if (!req) return null;
    if (typeof parameters != 'string') parameters = buildParams(parameters);
    // add optional arguments
    if (props) {
        for (var k in props) {
            req[k] = props[k];
        }
    }
    req.open("POST", url, true);
    req.overrideMimeType('text/xml');
    req.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
    req.setRequestHeader("Content-length", parameters.length);
    req.setRequestHeader("Connection", "close");
    req.onload = function(event) {
        var req = event.target;
        if (req.readyState == 4) callback(req);
    };
    req.send(parameters);
    return req;
}
asyncPostXML = util.asyncPostXML;

// Temporarily replace the content of statusNode with a non-clicakble, bolded string
// that shows we're doing something.  statusNode should be the node that completely wraps
// an <a> element that was just clicked.
util.buttonShowStatus = function(statusNode, statusText) {
    if (!statusNode) return;

    if (!statusText) {
        // use <a> tag to keep padding/margin/color/etc.
        statusText = '<a><b>'+statusNode.textContent+'...</b></a>';
    }

    // Note: saving innerHTML doesn't work if we've messed with the document
    // tree.

    // statusNode.savedContent = statusNode.innerHTML;
    // statusNode.innerHTML = statusText;

    statusNode.savedContent = util.copyArray(statusNode.childNodes);
    statusNode.innerHTML = statusText;
}
buttonShowStatus = util.buttonShowStatus;

util.buttonRestoreStatus = function(statusNode) {
    if (statusNode && statusNode.savedContent) {
        // statusNode.innerHTML = statusNode.savedContent;
        statusNode.innerHTML = '';
        util.appendChildren(statusNode, statusNode.savedContent);
    }
}
buttonRestoreStatus = util.buttonRestoreStatus;

//

// </nowiki></pre>
// [[User:Quarl/wikipage.js]] - "WikiPage" class for page name, etc. functionality

// depends: util.js

// Suppose we are editing [[Template talk:Foo bar 'blah']].
//   WikiPage.editingP:             true;
//   ...

// wikiPage is a pre-initialized global variable using the current page's canonical URL.
// It uses the "Retrieved from" page hyperlink which is robust against redirections, editing, special characters, special pages.
//
//   wikiPage.url:         "http://en.wikipedia.org/wiki/Template_talk:Foo_bar_%27blah%27"
//   wikiPage.qurl:        "http://en.wikipedia.org/w/index.php?title=Template_talk:Foo_bar_%27blah%27"
//   wikiPage.page:        "Template talk:Foo bar 'blah'"
//   wikiPage.article:     "Foo bar 'blah'"
//   wikiPage.namespace:   "Template talk"
//   wikiPage.namespaceNT: "Template"
//   wikiPage.talkP:       true
//   wikiPage.nsTemplateP: true
//   wikiPage.nsMainP:     false
//   wikiPage.nsUserP:     false
//   wikiPage.nsCategoryP: false
//   wikiPage.nsSpecialP:  false
//   wikiPage.nsProjectP:  false    // (namespace "Wikipedia")

// To create new WikiPage object from a URL:
//   var wp = new WikiPage("http://en.wikipedia.org/wiki/Article_Name");
// To create a new WikiPage object from a page name:
//   var wp = new WikiPage(null, 'Article Name');

// <pre><nowiki>

/*
<div class="printfooter">
Retrieved from "<a href="http://en.wikipedia.org/wiki/Albert_Einstein">http://en.wikipedia.org/wiki/Albert_Einstein</a>"</div>
*/

// the "retrieved from" text contains the canonical article URL (even if we're looking at an edit or history page)
function getCanonPageURL0() {
    return getElementsByClass("printfooter", null, 'div')[0].getElementsByTagName('a')[0].href;
}

function getUsername0(doc) {
    // read username from pt-userpage link.
    // <li id="pt-userpage"><a href="/wiki/User:Quarl">Quarl</a></li>
    return doc.getElementById('pt-userpage').getElementsByTagName('a')[0].text;
}

var wpNamespaces = {'Media':1, 'Special':1,
                    'Talk':1,
                    'User':1, 'User talk':1,
                    'Wikipedia':1, 'Wikipedia talk':1,
                    'Image':1, 'Image talk':1,
                    'MediaWiki':1, 'MediaWiki talk':1,
                    'Template':1, 'Template talk':1,
                    'Help':1, 'Help talk':1,
                    'Category':1, 'Category talk':1,
                    'Portal':1, 'Portal talk':1,
};

function WikiPage(url, page, doc) {
    if (!(this instanceof WikiPage)) return new WikiPage(url, page, doc);
    this.doc = doc;
    if (url) {
        url = "" + url;
        if (url.match( '^(?:http://'+WikiPage.server+')?/wiki/')) {
            this.pageQuoted = RegExp.rightContext;
        } else if (url.match( '^(?:http://'+WikiPage.server+')?/w/index\\.php\\?title=([^&]+)')) {
            this.pageQuoted = RegExp.$1;
        } else {
            alert("WikiPage: Couldn't parse page name from url '"+url+"'");
            return;
        }
        this.page = wpadecode(this.pageQuoted);
    } else if (page) {
        this.page = page.replace(/_/g, ' ');
        this.pageQuoted = wpaescape(page);
    } else {
        alert("WikiPage: must specify url or page"); return;
    }
    this.url = 'http://'+WikiPage.server+'/wiki/' + this.pageQuoted;
    this.qurl = 'http://'+WikiPage.server+'/w/index.php?title=' + this.pageQuoted;

    // Get term on the left of ":".  Not any string is a namespace though, only certain hardcoded ones!
    if (this.page.match(/:/) && wpNamespaces[RegExp.leftContext]) {
        this.namespace = RegExp.leftContext;
        this.article = RegExp.rightContext;
    } else {
        this.namespace = ''; // (main)
        this.article = this.page;
    }

    if (this.namespace == 'Talk') {
        this.talkP = true;
        this.namespaceNT = '';
    } else if (this.namespace.match(/ talk$/)) {
        this.talkP = true;
        this.namespaceNT = RegExp.leftContext;
    } else {
        this.talkP = false;
        this.namespaceNT = this.namespace;
    }

    if (this.article.match(/\//)) {
        this.superarticle = RegExp.leftContext;
        this.subarticle = RegExp.rightContext;
    } else {
        this.superarticle = this.article;
        this.subarticle = '';
    }

    this.nsMainP = (this.namespaceNT == '');
    this.nsMediaP = (this.namespaceNT == 'Media');
    this.nsSpecialP = (this.namespaceNT == 'Special');
    this.nsUserP = (this.namespaceNT == 'User');
    this.nsPortalP = (this.namespaceNT == 'Portal');
    this.nsImageP = (this.namespaceNT == 'Image');
    this.nsMediaWikiP = (this.namespaceNT == 'MediaWiki');
    this.nsTemplateP = (this.namespaceNT == 'Template');
    this.nsHelpP = (this.namespaceNT == 'Help');
    this.nsCategoryP = (this.namespaceNT == 'Category');
    this.nsProjectP = (this.namespaceNT == 'Wikipedia');

    this.talkPage = function() {
        if (this.talkP) { return this; }
        else if (this.namespaceNT == '') { return new WikiPage(null, 'Talk:'+this.article); }
        else { return new WikiPage(null, this.namespaceNT+' talk:'+this.article); }
    }

    this.notalkPage = function() {
        if (!this.talkP) { return this; }
        else if (this.namespaceNT == '') { return new WikiPage(null, this.article); }
        else { return new WikiPage(null, this.namespaceNT+':'+this.article); }
    }

    this.sandboxP     = Boolean(this.page.match(/sandbox$/i));

    this.setDoc = function(doc) {
        var wd = new WikiDocument(doc, this);

        if (wd.editingP) {
            this.editDoc = wd;
        } else {
            this.viewDoc = wd;
        }

        this.wd = wd;
    }

    if (doc) {
        // Note that a WikiPage may have more than one associated WikiDocument,
        // e.g. one from viewing and one from editing
        this.setDoc(doc);
        this.relevantUser = this.wd.relevantUser;
    } else {
        // this is less powerful than WikiDocument.relevantUser if we don't
        // have access to doc.
        this.relevantUser = getRelevantUser0(this, doc);
    }
}

function WikiDocument(doc, wp) {
    this.doc = doc;
    this.wp = wp;

    this.username     = getUsername0(doc);

    // Note: can't use "doc.editform" or "doc.forms.editform", because 'doc'
    // might actually be an XMLDocument (not HTMLDocument), if this is the
    // result of an XMLHTTPRequest.
    this.editForm     = doc.getElementById('editform');
    this.editingP     = Boolean(this.editForm);
    // obsolete method: document.title.match(/^Editing /)
    this.protectedP   = Boolean(doc.getElementById("ca-viewsource"));
    this.newSectionP  = this.editForm && (this.editForm.wpSection.value == "new");
    this.movePageP    = Boolean(doc.getElementById("movepage"));
    this.previewP     = Boolean(doc.getElementById("wikiPreview"));
    this.historyP     = Boolean(doc.getElementById("pagehistory"));
    this.permalinkP   = Boolean(doc.getElementById("t-ispermalink"));
    this.oldid        = getOldid0(doc, this.permalinkP);
    this.relevantUser = getRelevantUser0(wp, doc);
}

function getUsernameFromLink(link) {
    return link && (new WikiPage(link)).relevantUser;
}

function getRelevantUser0(wp, doc) {
    if (wp.nsUserP) return wp.superarticle;
    if (doc && wp.page == 'Special:Contributions') {
        var cdiv = doc.getElementById('contentSub');
        if (cdiv.textContent == "For newbies") return null;
        return getUsernameFromLink(cdiv.getElementsByTagName('a')[0]);
    }
    return null;
}

// Get the oldid ("permalink") for the current page.
// Note that we can't get oldid for editing pages, special pages, etc.
function getOldid0(doc, perm) {
    var tagid = perm ? 'ca-edit' : 't-permalink';
    var tag = doc.getElementById(tagid);
    if (!tag) return null;
    var href = tag.getElementsByTagName("a")[0].href;
    if (!href) return null;
    href.match(/&oldid=([0-9]+)/);
    return RegExp.$1;
}

function getQueryVars0(){
     var res = new Array();
     var pairs = location.search.substring(1).split("&");
     for(var i=0; i < pairs.length; i++){
         var pair = pairs[i].split("=");
         res[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]);
     }
     return res;
}

function initWikiPage() {
    WikiPage.server = window.location.host || 'en.wikipedia.org';

    window.wikiPage = new WikiPage(getCanonPageURL0(), null, document);
    window.wikiDoc = wikiPage.wd;

    WikiPage.queryVars    = window.queryVars    = getQueryVars0();

    // these are all deprecated
    WikiPage.username     = window.username     = wikiDoc.username;
    WikiPage.relevantUser = window.relevantUser = wikiDoc.relevantUser;
    WikiPage.editingP     = window.editingP     = wikiDoc.editingP;
    WikiPage.newSectionP  = window.newSectionP  = wikiDoc.newSectionP;
    WikiPage.movePageP    = window.movepageP    = wikiDoc.movePageP;
    WikiPage.previewP     = window.previewP     = wikiDoc.previewP;
    WikiPage.historyP     = window.historyP     = wikiDoc.historyP;
    WikiPage.sandboxP     = window.sandboxP     = wikiDoc.sandboxP;
    WikiPage.permalinkP   = window.permalinkP   = wikiDoc.permalinkP;
    WikiPage.oldid        = window.pageOldid    = wikiDoc.oldid;
}

// DEPRECATED:
function getPname() { return wikiPage.page; }
function getPnameNS() { return wikiPage.namespace; }
function getPnameNoNS() { return wikiPage.article; }
function pageIsSpecial() { return wikiPage.nsSpecialP; }
function get_query_vars() { return queryVars; }

$(initWikiPage);

// obsolete method 1:
// function getPname0() {
//     var z=document.getElementById("content").childNodes;
//     for (var n=0;n<z.length;n++) {
//         if (z[n].className=="firstHeading") return z[n].textContent;
//     }
// }

// function getPname1() {
//     var t = getPname0();
//     t = t.replace(/^Editing /,'');
//     t = t.replace(/ \(section\)$/,'');

//     return t;
// }

// obsolete method 2:
//    document.title.substr(0, document.title.lastIndexOf(' - Wikipedia, the free'));

//</nowiki></pre>
// [[User:Quarl/wikiedit.js]] - functions for automatically editing pages

// depends: util.js, wikipage.js
// recommends: smartsubmit.js

// synposis:
//     function beginEdit() {
//         wikiPage.getEditorAsync(myPage_edit, data1, data2);
//     }
//     function myPage_edit(editor, data1, data2) {
//         editor.wpTextbox1 += data1;
//         editor.wpSummary += data2;
//         editor.submit();
//

// WikiEditor is a class that facilitates editing and submitting Wikipedia edit forms.
//
// use asyncWikiEditor() to use the current edit form if available, else download one.
//     - inside the callback, "this" is equivalent to "editor"
//
// available properties:
//     wpTextbox1
//     wpSummary
//     wpMinoredit
//     wpWatchthis
//
// available functions:
//     submitDirect: submit form directly via document.form.submit
//     submitHidden: create a new hidden form, attach to document, and submit
//     submit: submitDirect if possible, else submitHidden
//     submitAsync: asynchronously submit a form via XMLHTTPRequest, and callback result
//     updateForm: update what the user sees and prepare for submitting
//     refuseCreate: if wpTextbox1 is empty, alert and return 'True'
//

// WikiEditor.addSubmitHook adds a hook that's called from all form
// submissions (including asynchronous ones).  For example usage see
// autoreplace.js.
//     WikiEditor.addSubmitHook(function(editor, button) { ... });

// quarl 2006-01-23 initial version

// <pre><nowiki>

// WikiEditor class
//
// A WikiEditor doubles as an associative array of the edit properties -
// i.e. editor.wpTextbox1 contains the edit text.
function WikiEditor(wd) {
    if (!(this instanceof WikiEditor)) return new WikiEditor(wd);
    window.wikiEditor = this;

    if (!(wd instanceof WikiDocument)) { alert("WikiEditor: need a WikiDocument"); return; }
    this.wd = wd;
    this.wp = wd.wp;
    this.form = WikiEditor.getEditForm(wd.doc);
    if (!this.form) { alert("WikiEditor error: no form!"); return; }

    // The HTML default maxlength is 200, but the MediaWiki server actually
    // accepts up to 250 chars!
    this.form.wpSummary.setAttribute('maxlength', 250);

    this.refuseCreate = function() {
        if (!this.wpTextbox1) {
            alert("Error!  Page is empty; refusing to create.");
            return true;
        } else {
            return false;
        }
    }

    this.getFormParams = function(button) {
        button = WikiEditor._checkButton(button);
        d = {};
        WikiEditor.updateFields(d, this, WikiEditor.wpFormFields);
        d[button] = this.form[button];
        return d;
    }

    this.updateThis = function() {
        WikiEditor.updateFields(this, this.form, WikiEditor.wpFormFields);
    }

    this.updateForm = function() {
        WikiEditor.updateFields(this.form, this, WikiEditor.wpFormFields);
    }

    // Direct submission, should only be used when the form is part of the
    // currently-viewed HTML page.  Navigates to result page.
    this.submitDirect = function(button) {
        button = WikiEditor._checkButton(button);
        this.updateForm();
        // Click the appropriate button.
        // Note that this generates an onClick event, which in turn calls the
        // runPreSubmitHooks function.
        this.form[button].click();
    }

    // Adds a hidden form to the current page and submits it, navigating to
    // the result page.
    this.submitHidden = function(button) {
        button = WikiEditor._checkButton(button);
        this.runPreSubmitHooks(this, button);
        var newform = document.createElement('form');
        addFormHiddenParams(newform, this.getFormParams(button));
        newform.name = this.form.name;
        newform.method = this.form.method;
        newform.id = this.form.id;
        newform.action = this.form.action;
        document.getElementById('bodyContent').appendChild(newform);
        newform.submit();
    }

    // Asynchronously submit the form and call CALLBACK.
    this.submitAsync = function(button, callback) {
        button = WikiEditor._checkButton(button);
        var cb;
        if (callback) {
            var thisE = this;
            var args = copyArray(arguments);
            args.shift(); args[0] = null;
            cb = function(req) { args[0] = req; callback.apply(thisE, args); };
        } else {
            cb = function(req) { /* dummy */ };
        }
        var data = this.getFormParams(button);
        this.runPreSubmitHooks(data, button);
        asyncPostXML(this.form.action, data, cb);
    }

    // copy input fields from form into this object for easy access (we'll copy back later)
    this.updateThis();
    this.wpTextbox1_orig = this.form.wpTextbox1_orig;

    // If this form is the current document's form, we can submit directly.
    // Else we must use the hidden submit method.
    if (this.form == document.editform) {
        this.submit = this.submitDirect;
    } else {
        this.submit = this.submitHidden;
    }
}

WikiEditor.getEditForm = function(doc) {
    if (!doc) doc = document;
    // Note: can't use "doc.editform", because 'doc' might actually be an XMLDocument (not HTMLDocument), if this is the result of an XMLHTTPRequest.
    return doc.getElementById('editform');
}

WikiEditor._assocArray = function(x) {
    for (var i in x) {
        x[ x[i] ] = 1;
    }
    return x;
}

WikiEditor.wpFormFields = WikiEditor._assocArray( [
    'wpSection', 'wpStarttime', 'wpEdittime', 'wpScrolltop',
    'wpTextbox1', 'wpSummary', 'wpMinoredit', 'wpWatchthis',
    'wpEditToken' ] );
WikiEditor.wpButtons = WikiEditor._assocArray( [ 'wpSave', 'wpPreview', 'wpDiff' ] );

WikiEditor._checkButton = function(button) {
    if (!button) return 'wpSave';                   // default
    if (typeof button != 'string' || WikiEditor.wpButtons[button] != 1) {
        alert("## WikiEditor._checkButton: invalid button '"+button+"' (error 1a0655e7-ac83-4f15-8447-694b16a834ed)");
        return 'wpPreview';
    }
    return button;
}

WikiEditor.updateFields = function(target, source, fields) {
    var targetFormP = Boolean(target.nodeName);
    var sourceFormP = Boolean(source.nodeName);
    for (var i in fields) {
        var f = fields[i];
        var v;
        if (sourceFormP && source[f]) {
            if (source[f].type == "checkbox") {
                v = source[f].checked;
            } else {
                v = source[f].value;
            }
        } else {
            v = source[f];
        }
        if (targetFormP) {
            if (target[f].type == "checkbox") {
                target[f].checked = v;
            } else {
                // don't set it if unchanged, to avoid focus/selection change
                if (target[f].value != v) target[f].value = v;
            }
        } else {
            target[f] = v;
        }
    }
}

// Get an editor for this WikiPage -- it needs to have an editDoc already (as
// an editing window.wikiPage would have); else need to use getEditorAsync().
// Usually it's easier to just always use getEditorAsync.
WikiPage.prototype.getEditor = function() {
    if (!this.editor) {
        if (!this.editDoc) {
            alert("## WikiPage.getEditor: no editDoc (use getEditorAsync)");
            return;
        }
        this.editor = WikiEditor(this.editDoc);
    }
    return this.editor;
}

// If already editing the target page, return a WikiEditor now.
// Else, download the edit form first.
// Call-back with new WikiEditor instance.
WikiPage.prototype.getEditorAsync = function(callback) {
    var wp = this;
    var args = copyArray(arguments); // copy arguments because we need it in 'cb' below

    // already cached
    if (wp.editor) {
        args[0] = wp.editor;
        callback.apply(wp.editor, args); return;
    }

    // do we already have an edit document?  (window.wikiPage.editDoc would be
    // initialized to 'WikiDocument(document)' as appropriate).
    if (wp.editDoc) {
        wp.editor = WikiEditor(wp.editDoc);
        args[0] = wp.editor;
        callback.apply(wp.editor, args); return;
    }

    // need to download a new edit document.
    var cb = function(req) {
        if (req.status != 200) {
            alert("asyncWikiEditor: Error downloading edit page!");
            return;
        }

        wp.setDoc(req.responseXML);
        wp.editor = WikiEditor(wp.editDoc);
        args[0] = wp.editor;
        callback.apply(wp.editor, args); return;
    };
    asyncDownloadXML(wp.qurl + '&action=edit', cb);
}

// deprecated
function asyncWikiEditor(wp) {
    var args = copyArray(arguments);
    args.shift();

    wp.getEditorAsync.apply(wp, args);
}

WikiEditor.pre_submit_hooks = [];

// add a submit hook to all forms (including asynchronous ones).
// Submit hooks are called with arguments (editor, form, button)
//   Note that the form argument may not be the same as editor.form or
//   document.form, if the submit is via submitHidden or submitAsync!
WikiEditor.addPreSubmitHook = function(func) {
    WikiEditor.pre_submit_hooks.push(func);
}

WikiEditor.prototype.runPreSubmitHooks = function(data, button) {
    // 'data' should be a hash array and could be either a WikiEditor
    // instance, or a separate object
    for (var i in WikiEditor.pre_submit_hooks) {
        WikiEditor.pre_submit_hooks[i](this, data, button);
    }
}

WikiEditor.onClickSubmitHook = function(button) {
    var editor = wikiPage.getEditor();
    if (editor.form[button].preventPreSumitHook) return;
    editor.updateThis();
    editor.runPreSubmitHooks(editor, button);
    // submit hooks may have changed data.
    editor.updateForm();
}

WikiEditor.load = function() {
    if (document.forms.editform) {
        // save original version of edit box
        document.forms.editform.wpTextbox1_orig = document.forms.editform.wpTextbox1.value;

        // hookEventObj(document.forms.editform, 'submit', WikiEditor.onClickSubmitHook);

        // add submit hooks
        hookEventObj(document.editform.wpSave, 'click', function() { WikiEditor.onClickSubmitHook('wpSave'); });
        hookEventObj(document.editform.wpDiff, 'click', function() { WikiEditor.onClickSubmitHook('wpDiff'); });
        hookEventObj(document.editform.wpPreview, 'click', function() { WikiEditor.onClickSubmitHook('wpPreview'); });
    }
}

$(WikiEditor.load);

// </nowiki></pre>
// [[User:Quarl/diff.js]] - utility functions for doing diffs

// quarl 2006-01-29 initial version

// requires: util.js (trimspaces)

// <pre><nowiki>

// if more than this many words of changes, use overflow string
var diff_wikisummary_maxwords = 30;
var diff_wikisummary_overflow = "$1 words changed";

/*
 * diff() and diffString() are based on
 *  http://ejohn.org/projects/javascript-diff-algorithm/
 *   Copyright John Resig
 */

function diff_split(s) {
    //return trimspaces(s).split(/(?:\s|[.,;\'\"`])+/);
    return trimspaces(s).split(/\s+/);
}

function diffString( o, n ) {
    var out = diff( diff_split(o), diff_split(n) );
    var str = "";

    for ( var i = 0; i < out.n.length - 1; i++ ) {
        if ( out.n[i].text == null ) {
            if ( out.n[i].indexOf('"') == -1 && out.n[i].indexOf('<') == -1 )
                str += "<ins style='background:#E6FFE6;'> " + out.n[i] +"</ins>";
            else
                str += " " + out.n[i];
        } else {
            var pre = "";
            if ( out.n[i].text.indexOf('"') == -1 && out.n[i].text.indexOf('<') == -1 ) {

                var n = out.n[i].row + 1;
                while ( n < out.o.length && out.o[n].text == null ) {
                    if ( out.o[n].indexOf('"') == -1 && out.o[n].indexOf('<') == -1 && out.o[n].indexOf(':') == -1 && out.o[n].indexOf(';') == -1 )
                        pre += " <del style='background:#FFE6E6;'>" + out.o[n] +" </del>";
                    n++;
                }
            }
            str += " " + out.n[i].text + pre;
        }
    }

    return str;
}

function diff( o, n ) {
    var ns = {};
    var os = {};

    for ( var i = 0; i < n.length; i++ ) {
        // note we have to check that it is in fact an object with "rows", in
        // case ns[i] happens to match a javascript member function of class
        // Array, e.g. "some"!
        if ( ns[ n[i] ] == null || !ns[n[i]].rows )
            ns[ n[i] ] = { rows: new Array(), o: null };
        ns[ n[i] ].rows.push( i );
    }

    for ( var i = 0; i < o.length; i++ ) {
        if ( os[ o[i] ] == null || !os[o[i]].rows )
            os[ o[i] ] = { rows: new Array(), n: null };
        os[ o[i] ].rows.push( i );
    }

    for ( var i in ns ) {
        if ( ns[i].rows.length == 1 && typeof(os[i]) != "undefined" && os[i].rows.length == 1 ) {
            n[ ns[i].rows[0] ] = { text: n[ ns[i].rows[0] ], row: os[i].rows[0] };
            o[ os[i].rows[0] ] = { text: o[ os[i].rows[0] ], row: ns[i].rows[0] };
        }
    }

    for ( var i = 0; i < n.length - 1; i++ ) {
        if ( n[i].text != null && n[i+1].text == null &&
             0 <= n[i].row+1 && n[i].row+1 < o.length &&
             o[ n[i].row + 1 ].text == null &&
             n[i+1] == o[ n[i].row + 1 ] )
        {
            n[i+1] = { text: n[i+1], row: n[i].row + 1 };
            o[n[i].row+1] = { text: o[n[i].row+1], row: i + 1 };
        }
    }

    for ( var i = n.length - 1; i > 0; i-- ) {
        if ( n[i].text != null && n[i-1].text == null &&
             0 <= n[i].row-1 && n[i].row-1 < o.length &&
             o[ n[i].row - 1 ].text == null &&
             n[i-1] == o[ n[i].row - 1 ] )
        {
            n[i-1] = { text: n[i-1], row: n[i].row - 1 };
            o[n[i].row-1] = { text: o[n[i].row-1], row: i - 1 };
        }
    }

    return { o: o, n: n };
}

function diffAggregate(words) {
    var phrases = new Array();
    var cur = null;
    var wordcount = 0;

    // start at virtual index -1 to check for removed words at beginning of
    // text
    for ( var i = -1; i < words.n.length; i++ ) {
        if ( i!=-1 && words.n[i].text == null ) {
            if (!cur) {
                cur = { o: "", n: "" };
                phrases.push(cur);
            }
            cur.n += " " + words.n[i];
            wordcount ++;
        } else {
            var pre = "";
            var j = i==-1 ? 0 : words.n[i].row + 1;
            while ( j < words.o.length && words.o[j].text == null ) {
                pre += " " + words.o[j];
                j++;
                wordcount ++;
            }
            if (pre) {
                if (!cur) {
                    cur = { o: "", n: "" };
                    phrases.push(cur);
                }
                cur.o += pre;
            }
            if (pre && words.n[i+1] && words.n[i+1].text == null) {
                // If there's an addition following, treat this as part of the
                // same change.
            } else {
                cur = null;
            }
        }
    }

    for (var i in phrases) {
        phrases[i].n = trimspaces(phrases[i].n);
        phrases[i].o = trimspaces(phrases[i].o);
    }

    return { phrases: phrases, wordcount: wordcount };
}

function diffWikiQuote(s) {
    if (!s) return s;
    if (s.match(/^\{\{.*\}\}$/)) return s;
    s = s.replace(/\"/g, "'");
    return '"'+s+'"';
}


function reverse(s) {
    var ret = '';
    for (var i = s.length-1; i >= 0; --i) {
        ret += s[i];
    }
    return ret;
}

// trim the equal chars from the front and back of o,n at a word boundary
function diffStringTrim(o, n) {
    var r = diffStringTrim0(reverse(o), reverse(n));
    return diffStringTrim0(reverse(r.o), reverse(r.n));
}

function diffStringTrim0(o, n) {
    var i = 0;
    while (i < o.length && i < n.length && o[i] == n[i]) {
        ++i;
    }

    // find index of last non-word character
    var prefix = o.substr(0, i);

    // if prefix ends with word characters and suffix starts with non-word,
    // then erase entire prefix
    if (prefix.match(/\w$/) &&
        !o.substr(i, 1).match(/^\w/) && !n.substr(i, 1).match(/^\w/))
    {
        o = o.substr(i);
        n = n.substr(i);
    } else if (prefix.match(/.*\W/)) {
        i = RegExp.lastMatch.length;
        o = o.substr(i);
        n = n.substr(i);
    } else {
        // keep entire prefix
    }
    return { o: o, n: n };
}

function diffSummary(o, n) {
    if (o == n) return "";
    if (!o) return "new";
    if (!n) return "blank";
    var words = diff( diff_split(o), diff_split(n) );
    var r = diffAggregate(words);
    if (!r.wordcount) return "";
    if (r.wordcount > diff_wikisummary_maxwords) {
        return diff_wikisummary_overflow.replace('$1', r.wordcount);
    }

    var phrases = r.phrases;
    var str = [];
    for (var i in phrases) {
        var r = diffStringTrim(phrases[i].o, phrases[i].n);
        var o = diffWikiQuote(r.o), n = diffWikiQuote(r.n);
        if (o && n) {
            str.push(o + ' → ' + n);
        } else if (o) {
            str.push('-' + o);
        } else if (n) {
            str.push('+' + n);
        } else {
            alert("## internal error 15e1b13f-bae3-4399-86c5-721786822fa2");
        }
    }

    return str.join(", ");
}

// </nowiki></pre>
// [[User:Quarl/cookie.js]] - cookie functions

// Note: careful changing this, as popups.js uses this as well!

// <pre><nowiki>

// based on http://www.quirksmode.org/js/cookies.html

// use kooky name to avoid name clashes
var kookie = new Object();

kookie.set = function(name, value, days, path)
{
    if (days) {
        var date = new Date();
        date.setTime(date.getTime()+(days*24*60*60*1000));
        var expires = "; expires="+date.toGMTString();
    }
    else var expires = "";

    // use path="" for no path; path=null defaults to root
    if (path == null) path = "/";
    if (path) path = "; path="+path;
    else path = "";

    document.cookie = name + "=" + value + expires + path;
}

kookie.get = function(name)
{
    var nameEQ = name + "=";
    var ca = document.cookie.split(';');
    for(var i=0;i < ca.length;i++)
    {
        var c = ca[i];
        while (c.charAt(0)==' ') c = c.substring(1,c.length);
        if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
    }
    return null;
}

kookie.erase = function(name)
{
    createCookie(name,"",-1);
}

// deprecated aliases
createCookie = kookie.set;
readCookie = kookie.get;
eraseCookie = kookie.erase;

// </nowiki></pre>
// [[User:Quarl/shortcuts.js]] - shortcut utilities

// <pre><nowiki>

// Defines the Shortcuts class.
//   Input is an associative array like this:
//
/*
   s = Shortcuts({
                    'x1'   : 'str 1',
                    'x2 '  : 'str 2', // the space after x2 prevents msg() from displaying this entry
                    'x3,x4': 'str 34' // x3 and x4 are aliases; msg() only displays x3
                });
*/

function Shortcuts(input) {
    if (!(this instanceof Shortcuts)) return new Shortcuts(input);

    this.shortcuts = {};
    for (k in input) {
        var keys = k.toUpperCase().split(',');
        this.shortcuts[keys[0]] = input[k];
        for (var i=1; i < keys.length; ++i) {
            this.shortcuts[keys[i]] = input[k] + ' ';
        }
    }
}

Shortcuts.prototype.msg = function() {
    var msg = 'Shortcuts available:\n';
    for (var key in this.shortcuts) {
        if (this.shortcuts[key].match(/ $/)) continue;
        msg += key + ': ' + this.shortcuts[key] + '\n';
    }
    return msg;
}

Shortcuts.prototype.subst = function(word) {
    return trimspaces(this.shortcuts[word.toUpperCase()]) || word;
}

Shortcuts.prototype.substP = function(word) {
    return Boolean(this.shortcuts[word.toUpperCase()]);
}

// replace the first word (doesn't require uppercase)
Shortcuts.prototype.substFirstWord = function(msg) {
    if (!msg) return msg;
    if (msg.match(/^([a-zA-Z]+)(.*)$/)) {
        return this.subst(RegExp.$1) + RegExp.$2;
    }
    return msg;
}

// replace all UPPERCASE words
Shortcuts.prototype.substUppercaseWords = function(msg) {
    if (!msg) return msg;
    var ret = '';
    var m;
    while (msg && (m = msg.match(/^(.*?)\b([A-Z]+)\b(.*)$/)) ) {
        ret += m[1] + this.subst(m[2]);
        msg = m[3];
    }

    ret += msg;
    return ret;
}

// replace all words (ignoring case)
Shortcuts.prototype.substWords = function(msg) {
    if (!msg) return msg;
    var ret = '';
    var m;
    while (msg && (m = msg.match(/^(.*?)\b([A-Za-z]+)\b(.*)$/)) ) {
        ret += m[1] + this.subst(m[2]);
        msg = m[3];
    }

    ret += msg;
    return ret;
}

// </nowiki></pre>