Benutzer:Wikibähhhr/monobook.js

aus Wikipedia, der freien Enzyklopädie
Zur Navigation springen Zur Suche springen

Hinweis: Leere nach dem Veröffentlichen den Browser-Cache, um die Änderungen sehen zu können.

  • Firefox/Safari: Umschalttaste drücken und gleichzeitig Aktualisieren anklicken oder entweder Strg+F5 oder Strg+R (⌘+R auf dem Mac) drücken
  • Google Chrome: Umschalttaste+Strg+R (⌘+Umschalttaste+R auf dem Mac) drücken
  • Internet Explorer/Edge: Strg+F5 drücken oder Strg drücken und gleichzeitig Aktualisieren anklicken
  • Opera: Strg+F5
/* <pre><nowiki> */
//======================================================================
//## core/prototypes.js 

/** bind a function to an object */
Function.prototype.bind = function(object) {
    var __self  = this;
    return function() { 
        __self.apply(object, arguments);
    };
}

/** remove whitespace from both ends */
String.prototype.trim = function() {
    return this.replace(/^\s+/, "")
               .replace(/\s+$/, "");    
}

/** true when the string starts with the pattern */
String.prototype.startsWith = function(s) {
    return this.length >= s.length
        && this.substring(0, s.length) == s;
}

/** true when the string ends in the pattern */
String.prototype.endsWith = function(s) {
    return this.length >= s.length 
        && this.substring(this.length - s.length) == s;
}

/** return text without prefix or null */
String.prototype.scan = function(s) {
    return this.substring(0, s.length) == s
            ? this.substring(s.length)
            : null;
}

/** escapes characters to make them usable as a literal in a regexp */
String.prototype.escapeRegexp = function() {
    return this.replace(/([{}()|.?*+^$\[\]\\])/g, "\\$0");
}

//======================================================================
//## core/functions.js 

/** find an element in document by its id */
function $(id) {
    return document.getElementById(id);
}

/** find descendants of an ancestor by tagName, className and index */
function descendants(ancestor, tagName, className, index) {
    if (ancestor && ancestor.constructor == String) {
        ancestor    = document.getElementById(ancestor);
    }
    if (ancestor == null)   return null;
    var elements    = ancestor.getElementsByTagName(tagName ? tagName : "*");
    if (className) {
        var tmp = new Array();
        for (var i=0; i<elements.length; i++) {
            if (elements[i].className == className) {
                tmp.push(elements[i]);
            }
        }
        elements    = tmp;
    }
    if (typeof index == "undefined")    return elements;
    if (index >= elements.length)       return null;
    return elements[index];
}

/** find the next element from el which has a given nodeName or is non-text */
function nextElement(el, nodeName) {
    for (;;) {
        el  = el.nextSibling;   if (!el)    return null;
        if (nodeName)   { if (el.nodeName.toUpperCase() == nodeName.toUpperCase())  return el; }
        else            { if (el.nodeName.toUpperCase() != "#TEXT")                 return el; }
    }
}

/** find the previous element from el which has a given nodeName or is non-text */
function previousElement(el, nodeName) {
    for (;;) {
        el  = el.previousSibling;   if (!el)    return null;
        if (nodeName)   { if (el.nodeName.toUpperCase() == nodeName.toUpperCase())  return el; }
        else            { if (el.nodeName.toUpperCase() != "#TEXT")                 return el; }
    }
}

/** remove a node from its parent node */
function removeNode(node) {
    node.parentNode.removeChild(node);
}

/** removes all children of a node */
function removeChildren(node) {
    while (node.lastChild)  node.removeChild(node.lastChild);
}

/** inserts an element before another one. allows Array for multiple elements and string for textNodes */
function pasteBefore(targetIdOrElement, element) {
    var target  = targetIdOrElement.constructor == String
                ? document.getElementById(targetIdOrElement)
                : targetIdOrElement;
    if (!target)    return;
    
    function add(element) {
        if (element.constructor == String)  element = document.createTextNode(element);
        target.parentNode.insertBefore(element, target); 
    }
    if (element.constructor == Array) {
        for (var i=0; i<element.length; i++) { add(element[i]); }
    }
    else {
        add(element);
    }
}

/** inserts an element before after one */
function pasteAfter(targetIdOrElement, element) {
    var target  = targetIdOrElement.constructor == String
                ? document.getElementById(targetIdOrElement)
                : targetIdOrElement;
    if (!target)    return;
    
    function addInsert(element) {
        if (element.constructor == String)  element = document.createTextNode(element);
        target.parentNode.insertBefore(element, next);
    }
    function addAppend(element) {
        if (element.constructor == String)  element = document.createTextNode(element);
        target.parentNode.appendChild(element);
    }
    var next    = target.nextSibling;
    var add     = next ? addInsert : addAppend;
    
    if (element.constructor == Array) {
        for (var i=0; i<element.length; i++) { add(element[i]); }
    }
    else {
        add(element);
    }
}

/** insert text, element or elements at the start of a target */
function pasteBegin(targetIdOrElement, element) {
    var target  = targetIdOrElement.constructor == String
                ? document.getElementById(targetIdOrElement)
                : targetIdOrElement;
    if (!target)    return;
    
    function add(element) {
        if (element.constructor == String)  element = document.createTextNode(element);
        if (target.firstChild)  target.insertBefore(element, target.firstChild);
        else                    target.appendChild(element);
    }
    
    if (element.constructor == Array) {
        for (var i=0; i<element.length; i++) { add(element[i]); }
    }
    else {
        add(element);
    }
}

/** insert text, element or elements at the end of a target */
function pasteEnd(targetIdOrElement, element) {
    var target  = targetIdOrElement.constructor == String
                ? document.getElementById(targetIdOrElement)
                : targetIdOrElement;
    if (!target)    return;
    
    function add(element) {
        if (element.constructor == String)  element = document.createTextNode(element);
        target.appendChild(element);
    }
    
    if (element.constructor == Array) {
        for (var i=0; i<element.length; i++) { add(element[i]); }
    }
    else {
        add(element);
    }
}

/** adds or removes className parts from a String */
function className(state, add, remove) {
    // put existing into a map
    var stateSplit  = state.split(/\s+/);
    var stateMap    = new Array();
    for (var i=0; i<stateSplit.length; i++) {
        var stateClass  = stateSplit[i].trim();
        if (stateClass.length == 0) continue;
        stateMap[stateClass] = 1;
    }
        
    // remove parts
    var remSplit    = remove.split(/\s+/);
    for (var i=0; i<remSplit.length; i++) {
        var name    = remSplit[i];
        delete stateMap[name];
    }
    
    // add parts
    var addSplit    = add.split(/\s+/);
    for (var i=0; i<addSplit.length; i++) {
        var name    = addSplit[i];
        stateMap[name] = 1;
    }
    
    // join parts
    var newStr  = "";
    for (var newClass in stateMap) {
        newStr  += " " + newClass;
    }
    if (newStr != "")   newStr  = newStr.substring(1);
    
    return newStr;
}

/** add an OnLoad event handler */
function doOnLoad(callback) {
    //.. gecko, safari, konqueror and standard
    if (typeof window.addEventListener != 'undefined')          
            window.addEventListener('load', callback, false);
    //.. opera 7
    else if (typeof document.addEventListener != 'undefined')   
            document.addEventListener('load', callback, false);
    //.. win/ie
    else if (typeof window.attachEvent != 'undefined')          
            window.attachEvent('onload', callback);
    // mac/ie5 and other crap fails here. on purpose.
    
}

/** concatenate two texts with an optional separator which is left out when one of the texts is empty */
function concatSeparated(left, separator, right) {
    var out = "";
    if (left)                       out += left;
    if (left && right && separator) out += separator;
    if (right)                      out += right;
    return out;
}

//======================================================================
//## core/Page.js 

/** represents the current Page */
Page = { init: function() {
    //------------------------------------------------------------------------------
    //## public info
    
    /** the current wiki site without any path */
    this.site   = location.protocol + "//" + location.host 
                + (location.port ? ":" + location.port : "");
    
    /** search string of the current location decoded into an Array */
    this.params = location.search 
                ? decodeArgs(location.search.substring(1))
                : {};
    
    /** path to read pages */ 
    this.readPath       = null;
    
    /** path for page actions */ 
    this.actionPath     = "/w/index.php";   //### generalize
    
    /** decoded Special namespace */
    this.specialNS      = null;
    
    /** decoded User namespace */
    this.userNS         = null;
    
    /** decoded User_talk namespace */
    this.userTalkNS     = null; 
    
    /** name of the logged in user or null (should never happen) */
    this.user           = null;
    
    /** the namespace of the current page */
    this.namespace      = null;
    
    /** lemma for the current URL ignoring redirects */
    this.lemma          = null;
            
    /** permalink to the current page if one exists or null */
    this.perma          = null;
    
    /** the user a User or User_talk or Special:Contributions page belongs to */
    this.owner          = null;
    
    /** whether a this page could be deleted */
    this.deletable      = false;
    
    //------------------------------------------------------------------------------
    //## public methods
    
    /** compute an URL in action form which may have a title parameter */
    this.actionURL = function(args) {
        var url = this.site + this.actionPath;
        return appendArgs(url, args);
    }
    
    /** compute an URL in the read form without a title parameter. args object and target string are optional */
    this.readURL = function(lemma, args, target) {
        var url = this.site
                + this.readPath
                + encodeTitle(lemma) 
                + (target ? "/" + encodeTitle(target) : "");
        return appendArgs(url, args);
    }
    
    /** returns the specialpage name if this page is (optionally a named) one, or null */
    this.isSpecial = function(name) {
        var current = this.lemma.scan(this.specialNS + ":");
        if (!current)   return null;
        current = current.replace(/\/.*/, "");
        if (name && current != name)    return null;
        return current;
    }
    
    //------------------------------------------------------------------------------
    //## private url encoding and decoding functions
    
    /** append arguments to an url with "?" or "&" depending on context */
    function appendArgs(url, args) {
        var code    = encodeArgs(args);
        if (code == "") return url;
        return url
            + (url.indexOf("?") == -1 ? "?" : "&")
            + code;
    }

    /** encode an Object or Array into URL parameters. */
    function encodeArgs(args) {
        if (!args)  return "";
        var query   = "";
        for (arg in args) {
            var key     = encodeURIComponent(arg);
            var raw     = args[arg];
            if (raw == null) continue;
            var value   = arg == "title" || arg == "target"
                        ? encodeTitle(raw)
                        : encodeURIComponent(raw.toString());
            query   += "&" + key +  "=" + value;
        }
        if (query == "")    return "";
        return query.substring(1);
    }
    
    /** convert URL-parameters into an Array. */
    function decodeArgs(search) {
        var out     = new Object();
        if (search == "")   return out;
        var split   = search.split("&");
        for (i=0; i<split.length; i++) {
            var parts   = split[i].split("=");
            var key     = decodeURIComponent(parts[0]);
            var value   = key == "title" || key == "target"
                        ? decodeTitle(parts[1])
                        : decodeURIComponent(parts[1]);
            out[key]    = value;
        }
        return out;
    }
    
    /** encode a MediaWiki title into URL form */
    function encodeTitle(title) {
        return encodeURIComponent(title)
                .replace(/%3[aA]/g, ":")
                .replace(/%2[fF]/g, "/")
                .replace(/ /g,      "_");
    }
    
    /** decode a MediaWiki title from an URL */
    function decodeTitle(title) {
         // MediaWiki allows "+" for "+" instead of " " since 05jan06
        title   = title.replace(/\+/g, "%2b");
        return decodeURIComponent(title)
                .replace(/_/g, " ");
    }
    
    //------------------------------------------------------------------------------
    //## page info intilialization
    
    // scoping helper
    var self    = this;
    
    // setup readPath and specialNS
    (function() {
        var search  = $('searchform').action;
        var title   = search.scan(self.site);
        var match   = /(.*\/)(.*):.*/(title ? title : search);
        self.readPath   = match[1];
        self.specialNS  = decodeTitle(match[2]);
    })();
    
    // setup user and userNS
    (function() {
        // <li id="pt-userpage"><a href="/wiki/Benutzer:D">D</a></li>
        // <li id="ca-nstab-user"><a href="/wiki/Benutzer:D">Benutzerseite</a></li>
        var a   = descendants('pt-userpage', "a", null, 0);
        if (a == null)  return;
        
        var href    = a.attributes.href.value;
        var split   = href.split(self.readPath);
        if (split.length < 2)   return;
        
        var title   = decodeTitle(split[1]);
        var match   = /([^:]+):(.*)/(title);
        self.userNS = match[1];
        self.user   = match[2];
    })();
    
    // setup userTalkNS
    (function() {
        //<li id="pt-mytalk"><a href="/wiki/Benutzer_Diskussion:D">Eigene Diskussion</a></li>
        var a   = descendants('pt-mytalk', "a", null, 0);
        if (a == null)  return;

        var href    = a.attributes.href.value;
        var split   = href.split(self.readPath);
        if (split.length < 2)   return;
    
        var title   = decodeTitle(split[1]);
        var match   = /([^:]+):(.*)/(title);
        self.userTalkNS = match[1];
    })();

    // setup lemma
    (function() {
        if (self.params.title) {
            if (self.params.target) self.lemma  = self.params.title + "/" + self.params.target;
            else                    self.lemma  = self.params.title;
        }
        else {
            var scanned = this.location.pathname.scan(self.readPath);
            if (scanned != null)    self.lemma  = decodeTitle(scanned);
            else                    self.lemma  = null; //### error!
        }
    })();
    
    // setup perma
    (function() {
        // <li id="t-permalink"><a href="/w/index.php?title=Benutzer_Diskussion:D&amp;oldid=12706568">Permanentlink</a></li>
        var a   = descendants('t-permalink', "a", null, 0);
        if (a == null)  return;
        
        // to get the oldid use this:
        // .replace(/^.*&oldid=([0-9]+).*/, "$1");
        self.perma  = a.href;
    })();
    
    // setup owner
    (function() {
        // try Special:Contributions 
        self.owner  = self.lemma.scan(self.specialNS + ":Contributions/");
        if (self.owner) return;
        
        // try Special:Blockip
        self.owner  = self.lemma.scan(self.specialNS + ":Blockip/");
        if (self.owner) return;
        
        // try Special:Emailuser
        self.owner  = self.lemma.scan(self.specialNS + ":Emailuser/");
        if (self.owner) return;
        
        // try blocklog
        //### does this work with readURLs? and does it have to?
        (function() {
            // http://de.wikipedia.org/w/index.php?title=Spezial:Log&type=block&user=&page=Benutzer%3AD
            if (!self.isSpecial("Log"))         return;
            if (self.params["type"] != "block") return;
            var page    = self.params["page"];
            if (!page)                          return;
            var title   = decodeTitle(page);    //### correct??
            var user    = title.scan(Page.userNS + ":");
            if (!user)                          return;
            self.owner  = user;
        })();
        if (self.owner) return;
        
        // <li id="t-blockip"><a href="/wiki/Spezial:Blockip/D">Benutzer blockieren</a></li>
        var a       = descendants('t-blockip', "a", null, 0);
        if (a == null)  return;
        
        var href    = a.attributes.href.value;
        var split   = href.split(self.readPath);
        if (split.length < 2)   return;
        
        var full    = decodeTitle(split[1]);
        var split2  = full.split(/\//);
        if (split2.length < 2)  return;
        
        self.owner  = split2[1];
    })();
    
    // setup contributor, deletable and namespace
    (function() {
        self.deletable      = $('ca-delete') != null;
        self.namespace      = parseInt(document.body.className.substring("ns-".length));
    })();
} };

//======================================================================
//## core/URLs.js 

/** generates mediawiki-urls */
URLs = {
    /** User:Name */
    userHome: function(user) {
        var title   = Page.userNS + ":" + user;
        return Page.readURL(title);
    },
    
    /** User_talk:Name */
    userTalk: function(user) {
        var title   = Page.userTalkNS + ":" + user;
        return Page.readURL(title);
    },
    
    /** User:Name/Subtitle */
    userPage: function(user, subTitle) {
        var title   = Page.userNS + ":" + user;
        return Page.readURL(title, null, subTitle);
    },
    
    /** Special:Emailuser/Name */
    userEmail: function(user) {
        var title   = Page.specialNS + ":Emailuser";
        return Page.readURL(title, null, user);
    },
    
    /** Special:Contributions/Name */
    userContribs: function(user) {
        var title   = Page.specialNS + ":Contributions";
        return Page.readURL(title, null, user);
    },
    
    /** Special:Blockip/Name */
    userBlock: function(user) {
        var title   = Page.specialNS + ":Blockip";
        return Page.readURL(title, null, user);
    },
    
    /** ?title=Spezial:Log&type=block&user=&page=User%3AName */
    userBlocklog: function(user) {
        var title   = Page.specialNS + ":Log";
        var victim  = Page.userNS + ":" + user;
        return Page.actionURL({
            title: title, 
            type: "block", 
            user: "", 
            page: victim
        });
    },
    
    //------------------------------------------------------------------------------

    /** ?title=Title&oldid=oldid&action=edit */
    pageEdit: function(title, oldid) {
        if (!oldid) oldid   = null;     // oldid is optional
        return Page.actionURL({
            title:  Page.lemma, 
            oldid:  oldid,
            action: "edit",
        });
    },
    
    /** ?title=Special:Newpages&limit=limit */
    newPages: function(limit) {
        if (!limit) limit   = 10;   // limit is optional
        var title   = Page.specialNS + ":Newpages";
        return Page.actionURL({
            title: title, 
            limit: 20
            // , inline: "yes"
        });
    },
    
    /** ?title=Special:Log */
    allLogs: function() {
        var title   = Page.specialNS + ":Log";
        return Page.actionURL({ 
            title: title,
        });
    },
};

//======================================================================
//## core/Ajax.js 

/** ajax helper functions */
Ajax = {
    /** headers preset for POSTs */
    urlEncoded: function(charset) { return { 
        "Content-Type": "application/x-www-form-urlencoded; charset=" + charset 
    }},
    
    /** headers preset for POSTs */
    multipartFormData: function(boundary, charset) { return {
        "Content-Type": "multipart/form-data; boundary=" + boundary + "; charset=" + charset
    }},
    
    /** encode an Object or Array into URL parameters. */
    encodeArgs: function(args) {
        if (!args)  return "";
        var query   = "";
        for (var arg in args) {
            var key     = encodeURIComponent(arg);
            var raw     = args[arg];
            if (raw == null) continue;
            var value   = encodeURIComponent(raw.toString());
            query   += "&" + key +  "=" + value;
        }
        if (query == "")    return "";
        return query.substring(1);
    },

    /** encode form data as multipart/form-data */ 
    encodeFormData: function(boundary, data) {
        var out = "";
        for (name in data) {
            var raw = data[name];
            if (raw == null)    continue;
            out += '--' + boundary + '\r\n';
            out += 'Content-Disposition: form-data; name="' + name + '"\r\n\r\n';
            out += raw.toString()  + '\r\n';
        }
        out += '--' + boundary + '--';
        return out;
    },

    /** create and use an XMLHttpRequest with named parameters */
    call: function(args) {
        // create
        var client  = new XMLHttpRequest();
        client.args = args;
        // open
        client.open(
            args.method ? args.method        : "GET", 
            args.url, 
            args.async  ? args.async == true : true
        );
        // set headers
        if (args.headers) {
            for (var name in args.headers) {
                client.setRequestHeader(name, args.headers[name]);
            }
        }
        // handle state changes
        client.onreadystatechange = function() {
            if (args.state)     args.state(client, args);
            if (client.readyState != 4) return;
            if (args.doneState) args.doneState(client, args);
        }
        // debug status
        client.debug = function() {
            return client.status + " " + client.statusText + "\n" 
                    + client.getAllResponseHeaders() + "\n\n"
                    + client.responseText;
        }
        // and start
        client.send(args.body ? args.body : null);
        return client;
    },
    
    /** parse text into an XML DOM */
    parseXML: function(text) {
        var parser  = new DOMParser();
        return parser.parseFromString(text, "text/xml");
    },
};

//======================================================================
//## core/Editor.js 

/** ajax functions for MediaWiki */
function Editor(progress) {
    if (progress)   this.indicator  = this.progressIndicator(progress);
    else            this.indicator  = this.quietIndicator;
}
Editor.prototype = {
    //------------------------------------------------------------------------------
    //## simple actions without form
    
    /** watch or unwatch a page. the doneFunc gets the new state and is optional */
    watchedPage: function(title, watch, doneFunc) {
        var self    = this;
        var action  = watch ? "watch" : "unwatch";
        self.indicator.header(action + "ing " + title);
        var url = Page.actionURL({
            title:  title,
            action: action,
        });
        self.indicator.getting(url);
        Ajax.call({
            method:     "GET",
            url:        url,
            doneState:  function(source) {
                if (source.status != 200) {
                    self.indicator.failed(source, 200);
                    return;
                }
                if (doneFunc)   doneFunc(watch);
                self.indicator.finished();
            },
        });
    },
    
    //------------------------------------------------------------------------------
    //## complex actions with form
    
    /** add text to the start of a page, the separator is optional */
    prependText: function(title, text, summary, separator) {
        this.indicator.header("prepending to " + title);
        this.action(
            // section=0 does _not_ insert the summary as a headline
            { title: title, action: "edit", section: 0 },
            200,
            "editform",
            function(f) { 
                var oldText = f.wpTextbox1.value.replace(/^[\r\n]+$/, "");
                var newText = concatSeparated(text, separator, oldText);
                return {
                    wpSection:      f.wpSection.value,
                    wpStarttime:    f.wpStarttime.value,
                    wpEdittime:     f.wpEdittime.value,
                    wpScrolltop:    f.wpScrolltop.value,
                    wpSummary:      summary,
                    wpWatchthis:    f.wpWatchthis.checked ? "1" : null,
                    wpMinoredit:    f.wpMinoredit.checked ? "1" : null,
                    wpSave:         f.wpSave.value,
                    wpEditToken:    f.wpEditToken.value,
                    wpTextbox1:     newText,
                };
            },
            200
            
        );
    },
    
    /** add text to the end of a spage, the separator is optional */
    appendText: function(title, text, summary, separator) {
        this.indicator.header("appending to " + title);
        this.action(
            // section=new inserts the summary as a headline
            { title: title, action: "edit" },
            200,
            "editform",
            function(f) {
                var oldText = f.wpTextbox1.value.replace(/^[\r\n]+$/, "");
                var newText = concatSeparated(oldText, separator, text);
                return {
                    wpSection:      f.wpSection.value,
                    wpStarttime:    f.wpStarttime.value,
                    wpEdittime:     f.wpEdittime.value,
                    wpScrolltop:    f.wpScrolltop.value,
                    wpSummary:      summary,
                    wpWatchthis:    f.wpWatchthis.checked ? "1" : null,
                    wpMinoredit:    f.wpMinoredit.checked ? "1" : null,
                    wpSave:         f.wpSave.value,
                    wpEditToken:    f.wpEditToken.value,
                    wpTextbox1:     newText,
                };
            },
            200
        );
    },

    /** delete a page. if the reason is null, the original reason is deleted */
    deletePage: function(title, reason) {
        this.indicator.header("deleting " + title);
        this.action(
            { title: title, action: "delete" },
            200,
            "deleteconfirm",
            function(f) {
                var separator   = " - ";    //### hardcoded
                var rs  = reason != null
                        ? concatSeparated(reason, separator, f.wpReason.value)
                        : "";
                return {
                    wpReason:       rs,
                    wpConfirmB:     f.wpConfirmB.value,
                    wpEditToken:    f.wpEditToken.value,
                };
            },
            200
        );
    },
    
    /** block a user */
    blockUser: function(user, duration, reason) {
        this.indicator.header("blocking " + user);
        var title   = Page.specialNS + ":Blockip";
        this.action(
            { title: title, target: user },
            200,
            "blockip",
            function(f) { return {
                wpBlockAddress: user,
                wpBlockReason:  reason,
                wpBlockExpiry:  f.wpBlockExpiry.value,
                wpBlockOther:   duration,
                wpEditToken:    f.wpEditToken.value,
                wpBlock:        f.wpBlock.value,
            }},
            200
        );
    },
    
    /** move a page */
    movePage: function(oldTitle, newTitle, reason, withDiscussion) {
        this.indicator.header("moving " + oldTitle + " to " + newTitle);
        var title   = Page.specialNS + ":Movepage";
        this.action(
            { title: title, target: oldTitle },
            200,
            "movepage",
            function(f) { return {
                wpOldTitle:     oldTitle,
                wpNewTitle:     newTitle,
                wpReason:       reason,
                wpMovetalk:     withDiscussion ? "1" : null,
                wpEditToken:    f.wpEditToken.value,
                wpMove:         f.wpMove.value,
            }},
            200
        );
    },
    
    //------------------------------------------------------------------------------
    //## action helper
    
    /** 
     * get a form, change it, post it.
     * 
     * makeData gets form.elements to create a Map 
     */
    action: function(actionArgs, expectedGetStatus, 
            formName, makeData, expectedPostStatus) {
        function phase1() {
            var url = Page.actionURL(actionArgs);
			
            self.indicator.getting(url);
            Ajax.call({
                method:     "GET",
                url:        url,
                doneState:  phase2,
            });
        }
        function phase2(source) {
            if (expectedGetStatus && source.status != expectedGetStatus) {
                self.indicator.failed(source, expectedGetStatus);
                return;
            }   
            
            var doc     = Ajax.parseXML(source.responseText);
            var form    = self.findForm(doc, formName);
            if (form == null) { self.indicator.missingForm(source, formName); return; }
            
            var url     = form.action;
            var data    = makeData(form.elements);
            var headers = Ajax.urlEncoded("UTF-8"); // Ajax.multipartFormData(boundary, "UTF-8")
            var body    = Ajax.encodeArgs(data);    // Ajax.encodeFormData(boundary, data)
            
            // bug #246651 - is this still open?
            //headers["Connection"] = "close";
                                                
            //var   boundary    = "134j5hkvgnarw4t82raflfjl3aklsjdfhsdlkhflkqe";
            //var   headers = Ajax.multipartFormData(boundary, "UTF-8")
            //var   body    = Ajax.encodeFormData(boundary, data)
            
            self.indicator.posting(url);
            Ajax.call({
                method:     "POST",
                url:        url,
                headers:    headers,    
                body:       body,
                doneState:  phase3,
            });
        }
        function phase3(source) {
            if (expectedPostStatus && source.status != expectedPostStatus) {
                self.indicator.failed(source, expectedPostStatus);
                return;
            }
            self.indicator.finished();
        }
        var self    = this;
        phase1();
    },
    
    
    /** finds a HTMLForm within an XMLDocument (!) */
    findForm: function(doc, name) {
        // firefox does _not_ provide document.forms,
        // but within the form we get HTMLInputElements (!)
        var forms   = doc.getElementsByTagName("form");
        for (var i=0; i<forms.length; i++) {
            var form    = forms[i];
            if (this.elementName(form) == name) return form;
        }
        return null;
    },
    
    /** finds the name or id of an element */
    elementName: function (element) {
        return  element.name    ? element.name
            :   element.id      ? element.id
            :   null;
    },

    //------------------------------------------------------------------------------
    //## progress
    
    /** progress indicator */
    progressIndicator: function(progress) {
        return {
            header: function(text)  { progress.header(text); },
            getting: function(url)  { progress.body("getting " + url); },
            posting: function(url)  { progress.body("posting " + url); },
            finished: function()    { progress.body("done"); progress.fade(); },
            missingForm: function(client, name) { progress.body("form not found: " + name); },
            failed: function(client, expectedStatus) {
                progress.body(
                    client.args.method + " " + client.args.url + " "
                    + client.status + " " + client.statusText + " "
                    + " (expected " + expectedStatus + ")"
                );
            },
        };
    },
    
    /** progress indicator */
    quietIndicator: {
        header: function(text) {},
        body:   function(text) {},
        getting: function(url) {},
        posting: function(url) {},
        finished: function() {},
        missingForm: function(client, name) { throw "form not found: " + name; }, 
        failed: function(client, expectedStatus) { throw "status unexpected\n" + client.debug(); }
    },
};

//======================================================================
//## ui/ProgressArea.js 

/** uses a messageArea to display ajax progress */
function ProgressArea() {
    var close   = closeButton(this.destroy.bind(this));
        
    var headerDiv   = document.createElement("div");
    headerDiv.className = "progress-header";
    
    var bodyDiv     = document.createElement("div");
    bodyDiv.className   = "progress-body";
    
    var outerDiv    = document.createElement("div");
    outerDiv.className  = "progress-area";
    outerDiv.appendChild(close);
    outerDiv.appendChild(headerDiv);
    outerDiv.appendChild(bodyDiv);
    
    var mainDiv     = $('progress-global');
    if (mainDiv == null) {
        mainDiv = document.createElement("div");
        mainDiv.id          = 'progress-global';
        mainDiv.className   = "progress-global";
        //var   bc  = $('bodyContent');
        //bc.insertBefore(mainDiv, bc.firstChild);
        pasteBefore('bodyContent', mainDiv);
    }
    mainDiv.appendChild(outerDiv);
    
    this.headerDiv  = headerDiv;
    this.bodyDiv    = bodyDiv;
    this.outerDiv   = outerDiv;
}
ProgressArea.prototype = {
    /** destructor, called by fade */
    destroy: function() {
        removeNode(this.outerDiv); 
    },

    /** display a header text */
    header: function(content) {
        removeChildren(this.headerDiv);
        pasteEnd(this.headerDiv, content);
    },
    
    /** display a body text */
    body: function(content) {
        removeChildren(this.bodyDiv);
        pasteEnd(this.bodyDiv, content);
    },
    
    /** fade out */
    fade: function() {
        var self    = this;
        setTimeout(function() { self.destroy(); }, 1500);
    },
};

//======================================================================
//## ui/closeButton.js 

/** creates a close button calling a function on click */
function closeButton(closeFunc) {
    var button  = document.createElement("input");
    button.type         = "submit";
    button.value        = "x";
    button.className    = "closeButton";
    if (closeFunc)  button.onclick  = closeFunc;
    return button;
}

//======================================================================
//## ui/Action.js 

/** creates links */
Action = {
    /** 
     * create an action link which
     * - onclick queries a text or
     * - oncontextmenu opens a popup with default texts
     * and calls a single-argument function with it.
     * the groups are an Array of preset string Arrays.
     * a separator is placed between rows. 
     */
    promptPopupLink: function(label, query, groups, func) {
        // the main link calls back with a prompted reason
        var mainLink    = this.promptLink(label, query, func);
    
        // create the menu
        var menu    = document.createElement("div");
        menu.className  = "popup-menu hidden";
    
        /** add a preset link to the menu */
        function addPreset(preset) {
            var link    = self.functionLink(preset, null);
            var item    = document.createElement("div");
            item.className  = "popup-menu-item";
            item.preset     = preset;   // user data
            item.appendChild(link);
            menu.appendChild(item);
        }
        
        /** add a separator to the menu */
        function addSeparator() {
            var separator   = document.createElement("hr");
            separator.className = "popup-menu-separator";
            menu.appendChild(separator);
        }
        
        // setup groups of items
        var self    = this;
        for (var i=0; i<groups.length; i++) {
            var group   = groups[i];    // maybe skip null groups
            if (i != 0) addSeparator();
            for (var j=0; j<group.length; j++) {
                var preset  = group[j];
                addPreset(preset);
            }
        }
    
        //### is displayed at the wrong position if not inserted at document root
        //mainLink.appendChild(menu);
        document.body.appendChild(menu);
        
        // intialize popup
        function selected(item) { func(item.preset); }
        popup(mainLink, menu, selected, null);
        
        return mainLink;
    },

    /** create an action link which onclick queries a text and calls a function with it */
    promptLink: function(label, query, func) {
        return this.functionLink(label, function() {
            var reason  = prompt(query);
            if (reason != null) func(reason);
        });
    },

    /** create an action link calling a function on click */
    functionLink: function(label, func) {
        var a   = document.createElement("a");
        a.className     = "functionLink";
        a.onclick       = func;
        a.textContent   = label;
        return a;
    },
    
    /** create a link to an url within the current list item */
    urlLink: function(label, url) {
        var a   = document.createElement("a");
        a.href          = url;
        a.textContent   = label;
        return a;
    },
};

//======================================================================
//## ui/Portlet.js 

/** create a portlet which has to be initialized with either createNew or useExisting */
function Portlet() {}
Portlet.prototype = {
    //------------------------------------------------------------------------------
    //## initialization
    
    /** create a new portlet, but do not yet display */
    createNew: function(id) {
        this.outer  = document.createElement("div");
        this.outer.id           = id;
        this.outer.className    = "portlet";
        this.header = document.createElement("h5");
        this.body   = document.createElement("div");
        this.body.className     = "pBody";
        this.outer.appendChild(this.header);
        this.outer.appendChild(this.body);
        this.ul = null;
        this.li = null;
        return this;
    },
    
    /** init from a MediaWiki-provided portlet and hide it */
    useExisting: function(id) {
        this.outer  = $(id);
        this.header = descendants(this.outer,   "h5",   null,       0);
        this.body   = descendants(this.outer,   "div",  "pBody",    0);
        this.ul     = descendants(this.body,    "ul",   null,       0);
        this.li     = null;
        removeNode(this.outer);
        return this;
    },
    
    /** 
     * display in the sidebar. 
     * has to be called after useExisting and createNew and after
     * all other methods. this is way faster than messing with nodes
     * connected to the DOM-tree
     */
    show: function() {
        SideBar.appendPortlet(this.outer);
        return this;
    },
    
    //------------------------------------------------------------------------------
    //## properties
    
    /** get the header text */
    getTitle: function() {
        return this.header.textContent;
    },

    /** set the header text */
    setTitle: function(text) {
        this.header.textContent = text;
        return this;
    },

    /** get the inner node */
    getInner: function() {
        return this.body.firstChild;
    },
    
    /** set the inner node */
    setInner: function(newChild) {
        removeChildren(this.body);
        this.body.appendChild(newChild);
        return this;
    },

    //------------------------------------------------------------------------------
    //## content builing

    /** 
     * render an array of arrays of links.
     * the outer array may contains strings to steal list items  
     * null items in the outer array are legal and skipped
     */
    build: function(rows) {
        this.list();
        for (var y=0; y<rows.length; y++) {
            var row = rows[y];
            // null rows are silently skipped
            if (row == null)    continue;
            // String objects are ids of elements to be stolen
            if (row.constructor == String) {
                this.steal(row);
                continue;
            }
            this.item();
            for (var x=0; x<row.length; x++) {
                var cell    = row[x];
                if (x > 0)  this.space();
                this.link(cell);
            }
        }
        return this;
    },
    
    //------------------------------------------------------------------------------
    //## private
    
    /** make a list */
    list: function() {
        if (this.ul)    return this;
        this.ul = document.createElement("ul");
        this.setInner(this.ul);
        return this;
    },
    
    
    /** insert a list item, content is optional */
    item: function(content) {
        this.li = document.createElement("li");
        if (content)    
            this.li.appendChild(content);
        this.ul.appendChild(this.li);
        return this;
    },
    
    
    /** steal a list item from another portlet */
    steal: function(id) {
        var element = $(id);
        if (!element)   return this;
        removeNode(element);
        this.ul.appendChild(element);
        return this;
    },
    
    
    /** append a link element created by the Action */
    link: function(link) {
        this.li.appendChild(link);
        return this;
    },
    
    /** create a small space within the current list item */
    space: function() {
        var s   = document.createTextNode(" ");
        this.li.appendChild(s);
        return this;
    },
};

//======================================================================
//## ui/SideBar.js 

/** encapsulates column-one */
SideBar = {
    //------------------------------------------------------------------------------
    //## public methods 
    
    /** change labels of item links. data is an Array of name/label Arrays */
    labelItems: function(data) {
         function sa(action, text) {
             var    el  = $(action);
             if (!el)   return;
             var    a   = el.getElementsByTagName("a")[0];
             if (!a)    return;
             a.textContent  = text;
         }
         for (var i=0; i<data.length; i++) {
            sa(data[i][0], data[i][1]);
         }
    },
    
    /** remove the go button, i want to search */
    removeSearchGoButton: function() {
        var node    = document.forms['searchform'].elements['go']; 
        removeNode(node);
    },
    
    /** move p-cactions out of column-one so it does not inherit its position:fixed */
    unfixCactions: function() {
        var pCactions       = $('p-cactions');
        var columnContent   = $('column-content');
        pCactions.parentNode.removeChild(pCactions);
        columnContent.insertBefore(pCactions, columnContent.firstChild);
    },
    
    /** insert a select box to replace the pLang portlet */
    langSelect: function() {
        var pLang       = $('p-lang');
        if (!pLang) return;
        
        var select  = document.createElement("select");
        select.id   = "langSelect";
        select.options[0]   = new Option(SideBar.msg.select, "");
        
        var list    = pLang.getElementsByTagName("a");
        for (var i=0; i<list.length; i++) {
            var a   = list[i];
            select.options[i+1] = new Option(a.firstChild.textContent, a.href);
        }   
        
        select.onchange = function() {
            var selected    = this.options[this.selectedIndex].value;
            if (selected == "") return;
            location.href   = selected;
        }
        
        // replace portlet contents
        this.usePortlet('p-lang').setInner(select).show();
    },
    
    //------------------------------------------------------------------------------

    /** return a portlet object for an existing portlet */
    usePortlet: function(id) {
        return new Portlet().useExisting(id);
    },
    
    /** create a new portlet object */
    newPortlet: function(id) {
        return new Portlet().createNew(id);
    },
    
    /** append a portlet to column-one, called by Portlet.show */
    appendPortlet: function(portlet) {
        var columnOne   = $('column-one');
        columnOne.appendChild(portlet);
        // navigation.parentNode.insertBefore(search, navigation);
    },
    
    /** move a single or multiple (shown) portlets portlets to the end of the toolbar */
    moveDown: function(id) {
        if (id.constructor == Array) {
            for (var i=0; i<id.length; i++) {
                this.moveDown(id[i]);
            }
            return;
        }
        var element = $(id);
        if (!element)   return;
        removeNode(element);
        this.appendPortlet(element);
    },
};
SideBar.msg = {
    select: "auswählen",
};

//======================================================================
//## ui/FoldButton.js 

/** FoldButton class */
function FoldButton(initiallyOpen, reactor) {
    var self        = this;
    this.button     = document.createElement("span");
    this.button.className   = "folding-button";
    this.button.onclick     = function() { self.flip(); }
    this.open       = initiallyOpen ? true : false;
    this.reactor    = reactor;
    this.display();
}
FoldButton.prototype = {
    /** flip the state and tell the reactor */
    flip: function() {
        this.change(!this.open);
        return this;
    },
    /** change state and tell the reactor when changed */
    change: function(open) {
        if (open == this.open)  return;
        this.open   = open;
        if (this.reactor)   this.reactor(open);
        this.display();
        return this;
    },
    /** change the displayed state */
    display: function() {
        this.button.innerHTML   = this.open 
                                ? "&#x25BC;" 
                                : "&#x25BA;";
        return this;
    },
};

//======================================================================
//## ui/popup.js 

/** display a prefab popup menu */
function popup(sourceOrId, menuOrId, selectListener, abortListener) {
    // init
    var menu    = typeof menuOrId   == "string" ? document.getElementById(menuOrId)   : menuOrId;
    var source  = typeof sourceOrId == "string" ? document.getElementById(sourceOrId) : sourceOrId;

    // initially hide popup menu
    menu.className  = "popup-menu hidden";
    var visible     = false;
    
    //------------------------------------------------------------------------------
    //## actions
    
    /** show the popup at mouse position */
    function showMenu(mouse) {
        //TODO: does not work whith an arbitrary parent, the menu must be top-level in the document
        var leftPos     = window.pageXOffset + mouse.x;
        var topPos      = window.pageYOffset + mouse.y;
        var rightEdge   = window.innerWidth  - mouse.x;
        var bottomEdge  = window.innerHeight - mouse.y;
        if (menu.offsetWidth  + mouse.x > window.innerWidth)    leftPos -= menu.offsetWidth;
        if (menu.offsetHeight + mouse.y > window.innerHeight)   topPos  -= menu.offsetHeight;
        menu.style.left = leftPos + "px";
        menu.style.top  = topPos  + "px";
        menu.className  = className(menu.className, "popup-menu visible", "hidden");
        visible         = true;
    }
    
    /** hide the popup */
    function hideMenu() {
        menu.className  = className(menu.className, "popup-menu hidden", "visible");
        visible         = false;
    }
    
    /** the user selected an item */
    function selectItem(item) {
        hideMenu();
        if (selectListener) selectListener(item, menu, source);
    }
    
    /** the user possibly aborted selection */
    function finish() {
        if (!visible)   return;
        hideMenu();
        if (abortListener)  abortListener(menu, source);
    }
    
    //------------------------------------------------------------------------------
    //## wiring
    
    function maybeSelectItem(ev) {
        // target           is within the item
        // currentTarget    is the menu
        // this             is ???
        var target  = ev.target;
        for (;;) {
            if (target.className
            && target.className.search(/\bpopup-menu-item\b/) != -1) {
                selectItem(target);
                return;
            }
            target  = target.parentNode;
            if (!target)    return;
        }
    }
    
    source.oncontextmenu = function(ev) {
        showMenu({ x: ev.clientX, y: ev.clientY});
        return false;
    }

    document.addEventListener("click", 
        function(ev) {
            if (visible)    finish();   
            return false; 
        },
        false
    );
    
    menu.onclick = function(ev) {
        maybeSelectItem(ev);
        return false;
    }
    
    menu.onmouseup = function(ev) {
        if (ev.button == 2) maybeSelectItem(ev);
        return false;
    }
}

//======================================================================
//## Template.js 

/** puts templates into the current page */
Template = new function() {
    /** return an Array of links to actions for normal pages */
    this.allPageActions = function(lemma) {
        var msg = Template.msg;
        return [
            Action.promptLink(msg.qs.label,  msg.qs.prompt,  function(reason) { qs(lemma, reason);  }),
            Action.promptLink(msg.la.label,  msg.la.prompt,  function(reason) { la(lemma, reason);  }),
            Action.promptLink(msg.sla.label, msg.sla.prompt, function(reason) { sla(lemma, reason); }),
        ];
    };
    
    /** return an Array of links for userTalkPages */
    this.userTalkPageActions = function(user, userTemplateNames) {
        var out     = new Array();
        out.push(this.doTest(user));
        if (userTemplateNames) {
            for (var i=0; i<userTemplateNames.length; i++) {
                out.push(this.doPersonal(user, userTemplateNames[i]));
            }
        }
        return out;
    }
    
    /** link inserting Vorlage:Test */
    this.doTest = function(user) {
        var lemma   = Page.userTalkNS + ":" + user;
        return Action.functionLink("Test", function() { test(lemma); });
    };
    
    /** link inserting User:FooBar/name template */
    this.doPersonal = function(user, name) {
        var lemma   = Page.userTalkNS + ":" + user;
        return Action.functionLink(name, function() { personal(lemma, name); });
    };
                
    //------------------------------------------------------------------------------
    //## text constants
    
    var r = {
        template:   function(title)         { return "{" + "{" + title + "}" + "}";                 },
        link:       function(title)         { return "[" + "[" + title + "]" + "]";                 },
        link2:      function(title, label)  { return "[" + "[" + title + "|" + label + "]" + "]";   },
        header:     function(text)          { return "==" + text + "==";                            },
        
        dash:       "--",   // "—" em dash U+2014 &#8212;
        sig:        "~~" + "~~",
        sigapp:     " -- ~~" + "~~\n",
        line:       "----",
        sp:         " ",
        lf:         "\n",
    };

    //------------------------------------------------------------------------------
    //## append templates without a reason 
    
    /** returns a function that puts an Test template into an article */
    function test(lemma) {
        var template    = "subst:Test";
        var editor      = new Editor(new ProgressArea());
        editor.appendText(
            lemma,
            r.template(template) + r.sp + r.sig + r.lf,
            r.template(template), 
            r.line + r.lf
        );
    }
    
    /** returns a function that puts a named user template into an article */
    function personal(lemma, name) {
        var template    = "subst:" + Page.userNS + ":" + Page.user + "/" + name;
        var editor      = new Editor(new ProgressArea());
        editor.appendText(
            lemma, 
            r.template(template) + r.sigapp,
            name,
            r.line + r.lf
        );
    }

    //------------------------------------------------------------------------------
    //## prepend wikipedia templates with a reason 
    
    /** puts an QS template into an article */
    function qs(lemma, reason) {
        enlist(lemma, "subst:QS", "Wikipedia:Qualitätssicherung", reason);
    }
    
    /** puts an LA template into an article */
    function la(lemma, reason) {
        enlist(lemma, "subst:Löschantrag", "Wikipedia:Löschkandidaten", reason);
    }
    
    /** puts an SLA template into an article */
    function sla(lemma, reason) {
        stamp(lemma, "löschen", reason);
    }
    
    /** put template in a page */
    function stamp(title, template, reason) {
        var editor  = new Editor(new ProgressArea());
        editor.prependText(
            title, 
            r.template(template) + r.sp + reason + r.sigapp,
            r.template(template) + r.sp + reason,
            r.line + r.lf
        );
    }
    
    /** list page on a list page */
    function enlist(title, template, listPage, reason) {
        stamp(title, template, reason);
        var editor  = new Editor(new ProgressArea());
        editor.appendText(
            listPage + "/" + currentDate(), 
            r.header(r.link(title)) + r.lf + reason + r.sigapp,
            r.link(title) + r.sp + r.dash + r.sp + reason,
            r.lf
        );
    }
    
    /** returns the current date in the format the LKs are organized */
    function currentDate() {
        var months  = [ "Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", 
                        "August", "September", "Oktober", "November", "Dezember" ];
        var now     = new Date();
        var year    = now.getYear();
        if (year < 999) year    += 1900;
        return now.getDate() + ". " + months[now.getMonth()] + " " + year;
    }
};
Template.msg = {
    qs: {
        label:  "qs",
        prompt: "warum qs?",
    },
    la: {
        label:  "la",
        prompt: "warum la?",
    },
    sla: {
        label:  "sla",
        prompt: "warum sla?",
    },
};

//======================================================================
//## FastDelete.js 

/** one-click delete */
FastDelete = { 
    /** returns a link which prompts or popups reasons and then deletes */
    doDeletePopup: function(title) {
        var self    = this;
        var msg     = FastDelete.msg;
        return Action.promptPopupLink(msg.label, msg.prompt, msg.reasons, function(reason) {
            self.fastDelete(title, reason);
        });
    },  
    
    /** return whole banks of delete links in an array of arrays */ 
    doDeleteBanks: function(title) {
        var inp1    = this.msg.reasons;
        var out1    = new Array();
        for (var i=0; i<inp1.length; i++) {
            var inp2    = inp1[i];
            var out2    = new Array();
            for (var j=0; j<inp2.length; j++) {
                var reason  = inp2[j];
                out2.push(this.doDeleteLink(title, reason));
            }
            out1.push(out2);
        }
        return out1;
    },
    
    /** return a single delete link */  
    doDeleteLink: function(title, reason) {
        var self    = this;
        return Action.functionLink(reason, function() {
            self.fastDelete(title, reason);
        });
    },
    
    /** delete an article with a reason*/
    fastDelete: function(title, reason) {
        var editor  = new Editor(new ProgressArea());
        editor.deletePage(title, reason);
    },
};
FastDelete.msg = {
    label:  "wech",
    prompt: "warum löschen?",
    reasons: [
        [   "schrott",          "kein artikel", 
            "irrelevant",       "unfug", 
            "tastaturtest",     "zu mager", 
            "linkcontainer",    "werbung",
        ],
        [   "wörterbucheintrag", 
            "falsches lemma", 
            "unnötiger redirect",
            "versehen", 
        ],
        [   "veraltet", "erledigt"
        ],
    ],
};

//======================================================================
//## FoldHeaders.js 

/** manages folding with a button */
FoldHeaders = {
    /** onload initializer */
    init: function() {
        if (Page.params["action"])  return;
        if (Page.params["diff"])    return;
        if ($('autofold'))          this.install();
    },
    
    /** a link folding the current page */
    doFold: function() {
        return Action.functionLink(FoldHeaders.msg.fold, this.install.bind(this));
    },
    
    //------------------------------------------------------------------------------
    
    /** provide an icon for each heading to fold or unfold it */
    install: function() {
        /** a folder attaching a folder span to a header. end is inclusive. returns a Folder */
        function Folder(header, end, even) {
            // make body
            var body        = document.createElement("div");
            body.className  = "foldHeaders folding-body";
            body.style.display  = "none";

            // fill body
            var range   = document.createRange();
            range.setStartAfter(header);
            range.setEndAfter(end);
            var contents    = range.extractContents();
            body.appendChild(contents);

            // clear body
            var clear   = document.createElement("div");
            clear.className = "visualClear";
            body.appendChild(clear);
            
            // remove the header
            var targetParent    = header.parentNode;
            var targetSibling   = header.previousSibling;
            targetParent.removeChild(header);
            
            // create a FoldButton
            var self        = this;
            var foldButton  = new FoldButton(false, function(open) {
                header.style.marginBottom   = open ? null : "0";
                body.style.display          = open ? null : "none";
                // fold children injected by recurse
                if (self.children) {
                    for (var i=0; i<self.children.length; i++) {
                        self.children[i].foldButton.change(open);
                    }
                    // HACK: remove children, so only the first open is executed
                    self.children   = null;
                }
            });

            // change the header
            header.className    = "foldHeaders folding-header";
            header.insertBefore(foldButton.button, header.firstChild);
        
            // insert filled container instead of the header
            var container   = document.createElement("div");
            container.className = "foldHeaders folding-container " 
                                + (even ? "folding-even" : "folding-odd"); 
            container.appendChild(header);
            container.appendChild(body);
            
            // replace the old header
            if (targetSibling)  targetParent.insertBefore(container, targetSibling.nextSibling);
            else                targetParent.appendChild(container);

            this.body       = body;
            this.foldButton = foldButton;
        }
        
        /** attach folders to all header tags of a given depth and below. parent is given children, if present. */
        function recurse(parent, depth) {
            var element = parent.body ? parent.body : parent;
            var tagName = "H" + depth;  
            var even    = depth % 2 == 0;
    
            // inserts a synthetic header if a lower-level header after an editsection is found
            function fake() {
                // find first editsection
                var maybe   = descendants(element, "div", "editsection", 0);
                if (!maybe)                 return;
                
                // find link paragraph 
                maybe   = nextElement(maybe);
                if (!maybe)                 return;
                if (maybe.nodeName != "P")  return;
                
                // find header tag
                maybe   = nextElement(maybe);
                var re  = new RegExp("H[" + (depth+1) + "-7]");
                if (!re(maybe.nodeName))    return;
    
                var faked   = document.createElement(tagName);
                var target  = element.firstChild;
                // hack: in level 2, skip the contentsub
                if (depth == 2) target  = $('contentSub').nextSibling;
                element.insertBefore(faked, target);
            }
            fake();
    
            var headers = element.getElementsByTagName(tagName);
            var folders = new Array();
            for (var i=0; i<headers.length; i++) {
                var header  = headers[i];
                // skip some headers we do not want to modify
                if (depth == 3 && header.id == "siteSub")  continue;
                //if (header.parentNode.id == "toctitle") continue;
                // TODO: remove <h2>Aktuelle Version</h2>
                var end     = i < headers.length-1 
                            ? contentEnd(headers[i+1]) 
                            : element.lastChild;
                // in level2 we may need to use #catliks instead of element.lastChild
                if (!end)   continue;
                var folder  = new Folder(header, end, even);
                if (depth < 6)  recurse(folder, depth+1);
                folders.push(folder);
            }
            // inject children into Folder object
            if (parent.body)    parent.children = folders;
            
            return folders;
        }
        
        /** (backwards) find a (inclusive) endpoint for folder content */
        function contentEnd(next) {
            var maybe   = next;
            
            // skip previous paragraph
            maybe   = previousElement(maybe);       if (!maybe) return null;
            if (maybe.nodeName != "P")              return next.previousSibling;
    
            // skip editsection div
            maybe   = previousElement(maybe);       if (!maybe) return null;
            if (maybe.nodeName != "DIV" 
            || maybe.className != "editsection")    return next.previousSibling;
            
            return maybe.previousSibling;
        }
        
        var bodyContent = $('bodyContent');
        
        // prevent double installation
        if (bodyContent.foldersInstalled)   return;
        bodyContent.foldersInstalled    = true;
    
        // rip out toc
        var toc = $('toc');
        if (toc)    removeNode(toc);    // toc.style.display    = "none";
        
        // attach folders at level 2
        var pageFolders = recurse(bodyContent, 2);
        
        // attach a meta FoldButton for the page
        var pageTitle   = descendants('content', "h1", "firstHeading", 0);
        var pageButton  = new FoldButton(false, function(open) {
            for (var i=0; i<pageFolders.length; i++) {
                pageFolders[i].foldButton.change(open);
            }
        });
        pageTitle.insertBefore(pageButton.button, pageTitle.firstChild);
    },
}
FoldHeaders.msg = {
    fold:   "fold",
};

//======================================================================
//## Usermessage.js 

/** changes usermessages */
Usermessage = {
    /** onload initializer */
    init: function() {
        this.historyLink();
    },
    
    /** modify the new usermessages to contain only a link to the history of the talkpage */ 
    historyLink: function() {
        var um  = descendants('bodyContent', "div", "usermessage", 0);
        var a   = descendants(um, "a", null, 1);
        if (!a) return;
        a.href          = a.href.replace(/&diff=cur$/, "&action=history");
        a.textContent   = Usermessage.msg.history;
    }
};
Usermessage.msg = {
    history: "History",
};

//======================================================================
//## UserPage.js 

/** cares for pages below the user namespace */
UserPage = {
    /** create bank of readLinks to private pages */
    doGotoBank: function() {
        var names   = UserPage.msg.pages;
        function addLink(name) {
            var link    = Action.urlLink(name, URLs.userPage(Page.user, name));
            out.push(link);
        }
        var out = new Array();
        for (var i=0; i<names.length; i++)  addLink(names[i]);
        return out;
    },
};
UserPage.msg = {
    pages: [ "new", "tmp", "todo", "test" ],
};

//======================================================================
//## UserBookmarks.js 

/** manages a personal bookmarks page  */
UserBookmarks = {
    /** return an Array of links for a lemma */
    actions: function(lemma) {
        return [ this.doView(), this.doMark(lemma) ];
    },

    /** return the absolute page link */
    doView: function() {
        return Action.urlLink(UserBookmarks.msg.view, URLs.userPage(Page.user, this.PAGE_TITLE));
    },
    
    /** add a bookmark on a user's bookmark page. if the page is left out, the current is added */
    doMark: function(lemma) {
        var self    = this;
        var msg     = UserBookmarks.msg;
        return Action.promptPopupLink(msg.add, msg.prompt, msg.reasons, function(reason) { 
            if (lemma)  self.arbitrary(reason, lemma);
            else        self.current(reason);
        });
    },
    
    //------------------------------------------------------------------------------

    /** user page name */
    PAGE_TITLE: "bookmarks",
    
    /** add a bookmark on a user's bookmark page */
    current: function(remark) {
        var lemma   = Page.lemma;
        var mode    = "perma";
        var perma   = Page.perma;
        if (!perma) {
            var params  = Page.params;
            var oldid   = params["oldid"];
            var target  = params["target"];
            if (oldid) {
                var diff    = params["diff"];
                if (diff) {
                    mode    = "diff";
                    if (diff == "prev"
                    ||  diff == "next"
                    ||  diff == "next"
                    ||  diff == "cur")  mode    = diff;
                    else
                    if (diff == "cur"
                    ||  diff == "0")    mode    = "cur";
                    perma   = Page.actionURL({ title: lemma, oldid: oldid, diff: diff});
                }
                else {
                    mode    = "old";
                    perma   = Page.actionURL({ title: lemma, oldid: oldid});
                }
            }
        }
        
        var text    = "*[[:" + lemma + "]]";
        if (perma)  text    += " <small>[" + perma + " " + mode + "]</small>";
        if (remark) text    += " " + remark;
        text        += "\n";
        this.prepend(text);
    },
    
    /** add a bookmark for an arbitrary page */
    arbitrary: function(remark, lemma) {
        var text    = "*[[:" + lemma + "]]";
        if (remark) text    += " " + remark;
        text        += "\n";
        this.prepend(text);
    },
    
    /** add text to the bookmarks page */
    prepend: function(text) {
        var title   = Page.userNS + ":" + Page.user + "/" + this.PAGE_TITLE;
        var editor  = new Editor(new ProgressArea());
        editor.prependText(title, text, "");
    },
};
UserBookmarks.msg = {
    view:   "bookmarks",
    add:    "add",
    prompt: "bemerkung?",
    reasons: [
        [   "wech mager",
            "wech kein artikel",
            "relevanz?",
            "urv?",
        ],
        [   "überarbeiten inhalt",
            "überarbeiten form",
        ],
        [   "gesperrt",
            "interessant",
            "wech bleiben",
            "faktencheck",
        ],
    ],
};

//======================================================================
//## ActionHistory.js 

/** helper for action=history */
ActionHistory = {
    /** onload initializer */
    init: function() {
        if (Page.params["action"] != "history") return;
        this.addBlockAndEditLinks();
    },
    
    //------------------------------------------------------------------------------
    
    /** add an edit link and a block link every version in a page history */
    addBlockAndEditLinks: function() {
        function addLink(li) {
            var diffInput   = descendants(li, "input", null, 1);
            if (!diffInput) return;
            
            // gather data
            var histSpan    = descendants(li, "span", "history-user", 0);
            var histA       = descendants(histSpan, "a", null, 0);
            var dateA       = nextElement(diffInput, "a");
            var oldid       = diffInput.value;
            var user        = histA.textContent;
            var date        = dateA.textContent;
            
            // add edit link
            var edit    = Action.urlLink(ActionHistory.msg.edit, URLs.pageEdit(Page.lemma, oldid));
            var before  = diffInput.nextSibling;    // li.firstChild;
            pasteBefore(before, [ " [", edit, "] "]);
            
            // add block link
            var block   = Action.urlLink(ActionHistory.msg.block, URLs.userBlock(user));
            pasteBefore(histSpan, [ " [", block, "] "]);
        }
        
        var lis = descendants('pagehistory', "li");
        if (!lis)   return;
        for (var i=0; i<lis.length; i++) {
            addLink(lis[i]);
        }
    },
};
ActionHistory.msg = {
    edit:   "edit",
    block:  "blocken",
};

//======================================================================
//## ActionDiff.js 

/** helper for action=diff */
ActionDiff = {
    /** onload initializer */
    init: function() {
        if (!Page.params["diff"])   return;
        this.addLinks();
    },
    
    //------------------------------------------------------------------------------
    
    /** add editlinks to diff-pages */
    addLinks: function() {
        /** extends one of the two sides */
        function extend(tdClassName) {
            // get cell
            var td  = descendants(document, "td", tdClassName, 0);
            if (!td)            return;
            
            // extract data
            var as  = descendants(td, "a");
            if (as.length < 2)  return;
            var a0      = as[0];
            var a1      = as[1];
            var oldidP  = /.*&oldid=([0-9]+)/(a0.href);
            var dateP   = /Version vom (.*)/(a0.textContent);   // hardcoded!
            var oldid   = oldidP ? oldidP[1] : null; 
            var date    = dateP  ? dateP[1]  : null;
            var user    = a1.textContent;
        
            
            // add edit link
            var edit    = Action.urlLink(ActionDiff.msg.edit, URLs.pageEdit(Page.lemma, oldid));
            var after   = a0.parentNode;
            pasteAfter(after, [ " [", edit, "] "]);
        }
        
        extend("diff-ntitle");
        extend("diff-otitle");
    },
};
ActionDiff.msg = {
    
    edit:   "edit",
};

//======================================================================
//## ActionWatch.js 

/** page watch and unwatch without reloading the page */
ActionWatch = {
    //<li id="ca-unwatch"><a href="/w/index.php?title=Benutzer:D/test&amp;action=unwatch">Nicht mehr beobachten</a></li>
    //<li id="ca-watch"><a href="/w/index.php?title=Benutzer:D/test&amp;action=watch">Beobachten</a></li>
    init: function() {
        /** initialize link */
        function initView() {
            var watch   = $('ca-watch');
            var unwatch = $('ca-unwatch');
                 if (watch)     exchangeItem(watch,     true);
            else if (unwatch)   exchangeItem(unwatch,   false);
        }
        
        /** show we are talking to the server */
        function progressView() {
            var watch   = descendants('ca-watch',   "a", null, 0);
            var unwatch = descendants('ca-unwatch', "a", null, 0);
            if (watch)      watch.className     = "active";
            if (unwatch)    unwatch.className   = "active";
        }
            
        /** talk to the server */
        function changeRemote(watched) {
            var editor  = new Editor(); // new ProgressArea()
            editor.watchedPage(Page.lemma, watched, updateView);
        }

        /** replace link */
        function updateView(watched) {
            var watch   = $('ca-watch');
            var unwatch = $('ca-unwatch');
            if ( watched && watch  )    exchangeItem(watch,     false);
            if (!watched && unwatch)    exchangeItem(unwatch,   true);
        }
        
        /** create a li with a link in it */
        function exchangeItem(target, watchable) {
            var li      = document.createElement("li");
            li.id       = watchable ? "ca-watch"            : "ca-unwatch";
            var label   = watchable ? ActionWatch.msg.watch : ActionWatch.msg.unwatch;
            var a       = Action.functionLink(label, function() {
                progressView();
                changeRemote(watchable);
            });
            li.appendChild(a);
            target.parentNode.replaceChild(li, target);
        }
        
        initView();
    },
};
ActionWatch.msg = {
    watch:      "Beobachten",
    unwatch:    "Vergessen",
};

//======================================================================
//## Communication.js 

/** communication with Page.owner */
Communication = {
    /** generate banks of links */
    doCommunicationBanks: function() {
        var msg = Communication.msg;
        var talkActions         = Template.userTalkPageActions(Page.owner,  msg.talkActions);
        var userRipeAction      = [ Action.urlLink(msg.userRipeAction,      this.ripeURL(Page.owner))       ];
        var userBlocklogAction  = [ Action.urlLink(msg.userBlocklogAction,  URLs.userBlocklog(Page.owner))  ];
        var userBlockAction     = [ Action.urlLink(msg.userBlockAction,     URLs.userBlock(Page.owner))     ]; 
        var userHomeAction      = [ Action.urlLink(msg.userHomeAction,      URLs.userHome(Page.owner))      ];
        var userTalkAction      = [ Action.urlLink(msg.userTalkAction,      URLs.userTalk(Page.owner))      ];
        var userEmailAction     = [ Action.urlLink(msg.userEmailAction,     URLs.userEmail(Page.owner))     ];
        var userContribsAction  = [ Action.urlLink(msg.userContribsAction,  URLs.userContribs(Page.owner))  ];
        
        // remove email for IP-users and ripe for non-IP-users
        var ipOwner = this.isIP(Page.owner);
        if (ipOwner)    userEmailAction = null;
        else            userRipeAction  = null;
        
        
        return [
            talkActions,
            userRipeAction,
            userEmailAction,
            userBlocklogAction,
            userBlockAction,
            userContribsAction,
            userHomeAction, 
            userTalkAction,
        ];
    },
    
    /** true when the name String denotes an v4 IP-address */
    isIP: function(ip) {
        if (!ip.match(/^(\d{1,3}\.){3}\d{1,3}$/))   return false;
        var parts   = ip.split(/\./);
        if (parts.length != 4)                      return false;
        for (var i=0; i<parts.length; i++) {
            var byt = parseInt(parts[i]);
            if (byt < 0 || byt > 255)               return false;
        }
        return true;
    },
    
    /** ripe check URL */
    ripeURL: function(ip) {
        return "http://www.ripe.net/fcgi-bin/whois?form_type=simple&full_query_string=&&do_search=Search&searchtext=" + ip;
    },
};
Communication.msg = {
    talkActions:        [ "spielen", "genug" ],
    userRipeAction:     "Ripe", 
    userBlocklogAction: "Blocklog",
    userBlockAction:    "Blockieren",
    userHomeAction:     "Benutzerseite",
    userTalkAction:     "Diskussion",
    userEmailAction:    "Anmailen",
    userContribsAction: "Beiträge",
};

//======================================================================
//## SpecialNewpages.js 

/** extends Special:Newpages */
SpecialNewpages = {
    /** onload initializer */
    init: function() {
        // early exit, wrong page
        if (!Page.isSpecial("Newpages"))    return;
        //if (Page.params["inline"] != "yes")   return;
        this.displayInline();
    },
    
    /** a link to new pages */
    doNewpages: function() {
        return Action.urlLink(SpecialNewpages.msg.newpages, URLs.newPages(20));
    },

    //------------------------------------------------------------------------------

    /** extend Special:Newpages with the content of the articles */
    displayInline: function() {
        // maximum number of bytes an article may have to be loaded immediately
        var maxSize     = 2048;
        
        /** parse one list item and insert its content */
        function extendItem(li) {
            // fetch data
            var a       = li.getElementsByTagName("a")[0];
            var title   = a.title;
            var bytes   = parseInt(/^[^0-9]*([0-9\.]+)/(a.nextSibling.textContent)[1].replace(/\./g, ""));
            
            // HACK: modify link in the header
            a.href  = a.href + "?action=history";
            
            // make header
            var header  =  document.createElement("div");
            header.className    = "folding-header";
            header.innerHTML    = li.innerHTML;
            
            // make body
            var body    = document.createElement("div");
            body.className      = "folding-body";
            
            // add a FoldButton to the header
            var foldButton  = new FoldButton(true, function(open) {
                body.style.display  = open ? null : "none";
            });
            pasteBegin(header, foldButton.button);
            
            // add action links
            pasteBegin(header, UserBookmarks.doMark(title));
            pasteBegin(header, Template.allPageActions(title));
            pasteBegin(header, FastDelete.doDeletePopup(title));
    
            // change listitem
            li.pageTitle    = title;
            li.contentBytes = bytes;
            li.headerDiv    = header;
            li.bodyDiv      = body;
            //TODO: set folding-even and folding-odd
            li.className    = "folding-container";
            li.innerHTML    = "";
            li.appendChild(header);
            li.appendChild(body);
            
            if (li.contentBytes <= maxSize) {
                loadContent(li);
            }
            else {
                //### BÄH
                var inner   = [
                    SpecialNewpages.msg.longer1,
                    ""+maxSize,
                    SpecialNewpages.msg.longer2,
                    SpecialNewpages.msg.load1,
                    Action.functionLink(SpecialNewpages.msg.loadX, 
                        function() { loadContent(li); }),
                    SpecialNewpages.msg.load2,
                ];
                for (var i=0; i<inner.length; i++) {
                    var node    = inner[i];
                    if (node.constructor == String) body.appendChild(document.createTextNode(node));
                    else                            body.appendChild(node);
                }
            }
        }
        
        function loadContent(li) {
            // load the article content and display it inline
            Ajax.call({
                url:        Page.readURL(li.pageTitle, { redirect: "no" }),
                doneState:  function(source) {
                    var content = /<!-- start content -->([^]*)<div class="printfooter">/(source.responseText);
                    if (content)    li.bodyDiv.innerHTML    = content[1] + '<div class="visualClear" />';
                }
            });
        }
    
        // find article list
        var ol  = descendants('bodyContent', "ol", null, 0);
        ol.className    = "specialNewPages";
        
        // find article list items
        var lis = descendants(ol, "li");
        for (var i=0; i<lis.length; i++) {
            extendItem(lis[i]);
        }
    },
};
SpecialNewpages.msg = {
    newpages:   "Neue Artikel",
    
    longer1:    "länger als ",
    longer2:    " bytes. ", 
    
    load1:      "trotzdem ",
    loadX:      "laden",
    load2:      ".",
};

//======================================================================
//## SpecialBlockip.js 

/** extends Special:Blockip */
SpecialBlockip = {
    /** onload initializer */
    init: function() {
        if (!Page.isSpecial("Blockip")) return;
        this.presetBlockip();
    },
    
    //------------------------------------------------------------------------------
    
    /** fill in default values into the blockip form */
    presetBlockip: function() {
        var form    = document.forms["blockip"];
        if (!form)  return; // action=success
        
        form.elements["wpBlockExpiry"].value    = "other";
        form.elements["wpBlockOther"].value     = "1 hour";
        form.elements["wpBlockReason"].value    = SpecialBlockip.msg.standardReason;
        form.elements["wpBlockReason"].select();
        form.elements["wpBlockReason"].focus();
    },
};
SpecialBlockip.msg = {
    standardReason: "vandalismus",
};

//======================================================================
//## SpecialUndelete.js 

/** extends Special:Undelete */
SpecialUndelete = {
    /** onload initializer */
    init: function() {
        if (!Page.isSpecial("Undelete"))    return;
        this.toggleAll();
    },
    
    //------------------------------------------------------------------------------

    /** add an invert button for all checkboxes */
    toggleAll: function() {
        var form    = document.forms["undelete"];
        if (!form)  return;
    
        var button  = document.createElement("input");
        button.type     = "button";
        button.value    = SpecialUndelete.msg.invert;
        button.onclick = function() {
            var els = form.elements;
            for (var i=0; i<els.length; i++) {
                var el  = els[i];
                if (el.type == "checkbox")  
                    el.checked  = !el.checked;
            }
        }
        
        var target  = descendants('undelete', "ul", null, 1);
        target.parentNode.insertBefore(button, target);
    },
};
SpecialUndelete.msg = {
    invert: "Invertieren",
};

//======================================================================
//## SpecialWatchlist.js 

/** extensions for Special:Watchlist */
SpecialWatchlist = {
    /** onload initializer */
    init: function() {
        if (!Page.isSpecial("Watchlist"))   return;
        if (Page.params["edit"] == "yes") {
            this.exportLinks();     // call before extendHeaders!
            this.toggleLinks();
        }
        else {
            this.filterLinks();
        }
    },
    
    //------------------------------------------------------------------------------
    //## normal mode
    
    /** change the watchlist to make it filterable */
    filterLinks: function() {
        if (!Page.isSpecial("Watchlist"))   return;
        if (Page.params["edit"])            return;
        
        var ip  = /^(\d{1,3}\.){3}\d{1,3}$/;
        
        /** set a source-ip or source-name class on every list item in every ul-special */
        function init() {
            var uls = descendants(document, "ul", "special");
            for (var i=0; i<uls.length; i++) {
                var ul  = uls[i];
                var lis = descendants(ul, "li");
                for (var j=0; j<lis.length; j++) {
                    var li  = lis[j];
                    var a   = descendants(li, "a", null, 3);
                    li.className    = ip(a.textContent) 
                                    ? "source-ip"
                                    : "source-name";
                }
            }
        }
        
        /** change bodyContent classes */
        function mode(add,sub) { 
            var bodyContent = $('bodyContent');
            bodyContent.className   = className(bodyContent.className, add, sub); 
        }
        
        /** show all list items */
        function showAll() { 
            mode("source-all", "source-ip source-name");
        }
        
        /** show all list items from ip users */
        function showIp() { 
            mode("source-ip", "source-all source-name");
        }
        
        /** show all list items from logged in users */
        function showName() {
            mode("source-name", "source-all source-ip");
        }
        
        // change list items and add buttons
        init();
        
        // add buttons to the bodyContent
        // TODO: show current state
        var target  = nextElement($('jump-to-nav'), "h4");
        pasteBefore(target, [
            SpecialWatchlist.msg.show1,
            Action.functionLink(SpecialWatchlist.msg.all,   showAll),   ' | ',
            Action.functionLink(SpecialWatchlist.msg.names, showName),  ' | ',
            Action.functionLink(SpecialWatchlist.msg.ips,   showIp),
            SpecialWatchlist.msg.show2,
        ]);
    },
    
    //------------------------------------------------------------------------------
    //## edit mode
    
    /** extend Special:Watchlist?edit=yes with a link to a text/plain version */
    exportLinks: function() {
        // parse and generate wiki and csv text
        var wiki    = "";
        var csv     = '"title","namespace","exists"\n';
        var ns      = "";
        var form    = descendants(document, "form", null, 0);
        var uls     = descendants(form, "ul");
        for (var i=0; i<uls.length; i++) {
            var ul  = uls[i];
            var h2  = previousElement(ul);
            if (h2) ns  = h2.textContent;
            wiki    += "== " + ns + " ==\n";
            
            var lis = descendants(ul, "li");
            for (var j=0; j<lis.length; j++) {
                var li  = lis[j];
                var as  = descendants(li, "a");
                var a   = as[0];
                var title   = a.title;
                var exists  = a.className != "new";
                wiki    += '*[[' + title + ']]'
                        +  (exists ? "" : " (new)")
                        +  '\n';
                csv     += '"' + title.replace(/"/g, '""')  + '"'   + ','
                        +  '"' + ns.replace(/"/g, '""')     + '"'   + ','
                        +  '"' + (exists ? "yes": "no")     + '"'   + '\n';
            }
        }
        
        // create wiki link
        var wikiLink    = document.createElement("a");
        wikiLink.textContent    = "watchlist.wkp";
        wikiLink.title          = "Markup";
        wikiLink.href           = "data:text/plain;charset=utf-8," + encodeURIComponent(wiki);

        // create csv link
        var csvLink     = document.createElement("a");
        csvLink.textContent     = "watchlist.csv";
        csvLink.title           = "CSV";
        csvLink.href            = "data:text/csv;charset=utf-8,"   + encodeURIComponent(csv);

        // insert links
        var target  = nextElement($('jump-to-nav'), "form");
        pasteBefore(target, [
            "export as ",   wikiLink,   " (Markup) ",
            "or as ",       csvLink,    " (CSV).",
        ]);
    },
    
    /** extends header structure and add toggle buttons for all checkboxes */
    toggleLinks: function() {
        var form    = descendants(document, "form", null, 0)
        
        // be folding-friendly: add a header for the article namespace
        var ul          = descendants(form, "ul", null, 0);
        var articleHdr  = document.createElement("h2");
        articleHdr.textContent  = SpecialWatchlist.msg.article;
        pasteBefore(ul, articleHdr);
        
        // add invert buttons for single namespaces
        var uls     = descendants(form, "ul");
        for (var i=0; i<uls.length; i++) {
            var ul      = uls[i];
            var button  = this.toggleButton(ul);
            var target  = previousElement(ul, "h2");
            pasteAfter(target.lastChild, [ ' ', button ]);
        }
        
        // be folding-friendly: add a header for the global controls
        var globalHdr   = document.createElement("h2");
        globalHdr.textContent   = SpecialWatchlist.msg.global;
        var target  = form.elements["remove"];
        pasteBefore(target, globalHdr);
        
        // add a gobal invert button
        var button  = this.toggleButton(form);
        pasteAfter(globalHdr.lastChild, [ ' ', button ]);
    },

    /** creates a toggle button for all input children of an element */
    toggleButton: function(container) {
        return Action.functionLink(SpecialWatchlist.msg.invert, function() {
            var inputs  = container.getElementsByTagName("input");
            for (var i=0; i<inputs.length; i++) {
                var el  = inputs[i];
                if (el.type == "checkbox")  
                    el.checked  = !el.checked;
            }
        });
    },
};
SpecialWatchlist.msg = {
    invert:     "Invertieren",
    article:    "Artikel",
    global:     "Alle",
    
    show1:      "Änderungen ",
    all:        "von allen Nutzern",
    ips:        "nur von Ips",
    names:      "nur von Angemeldeten",
    show2:      " anzeigen.",
};

//======================================================================
//## _private.js 

/** onload hook */
function initialize() {
    //------------------------------------------------------------------------------
    //## init functions
    
    Page.init();                // gather globally used information
    Usermessage.init();         // replace diff=cur with action=history
    ActionWatch.init();         // one-click watch and unwatch
    ActionHistory.init();       // add edit and block link for every version
    ActionDiff.init();          // add edit and block link for left and right version
    SpecialUndelete.init();     // add invert button toggling all checkboxes
    SpecialNewpages.init();     // display article content inline, a link is added to the sideBar later
    SpecialBlockip.init();      // set default values for a blockip page, a link is addded to the sideBar later
    SpecialWatchlist.init();    // add export links to the top of the page and an toggle button for all checkboxes
    FoldHeaders.init();         // fold all headers when a div with id autofold is found

    //------------------------------------------------------------------------------
    //## p-search
    
    // remove the go button, i want to search on enter
    SideBar.removeSearchGoButton();
    
    //------------------------------------------------------------------------------
    //## p-cactions
    
    // cactions should move with the page
    SideBar.unfixCactions();
    
    SideBar.labelItems([
        [ 'ca-talk',        "Diskussion"    ],
        [ 'ca-edit',        "Bearbeiten"    ],
        [ 'ca-viewsource',  "Source"        ],
        [ 'ca-history',     "History"       ],
        [ 'ca-protect',     "Schützen"      ],
        [ 'ca-unprotect',   "Freigeben"     ],
        [ 'ca-delete',      "Löschen"       ],
        [ 'ca-move',        "Verschieben"   ],
        // done in ActionWatch
        //[ 'ca-watch',     "Beobachten"    ],
        //[ 'ca-unwatch',   "Vergessen"     ],
    ]);
    
    //------------------------------------------------------------------------------
    //## p-tb
    
    SideBar.labelItems([
        [ 't-whatlinkshere',        "Links hierher"     ],
        [ 't-recentchangeslinked',  "Nahe Änderungen"   ],
        [ 't-emailuser',            "Anmailen"  ],
    ]);
    
    var tools1  = [ FoldHeaders.doFold() ];
    if (Page.deletable) {
        var multi   = FastDelete.doDeletePopup(Page.lemma);
        tools1.unshift(multi);
    }
    SideBar.usePortlet('p-tb').setTitle("Tools").build([
        tools1,
        Template.allPageActions(Page.lemma),
        UserBookmarks.actions(),
        UserPage.doGotoBank(),
    ]).show();
    
    //------------------------------------------------------------------------------
    //## portlet-delete
    
    if (Page.deletable)
    SideBar.newPortlet("portlet-delete").setTitle("Wech").build(
        FastDelete.doDeleteBanks(Page.lemma)
    ).show();
    
    //------------------------------------------------------------------------------
    //## p-navigation

    SideBar.usePortlet('p-navigation').setTitle("Navigation").build([
        'n-recentchanges',
        [   SpecialNewpages.doNewpages()                ],
        't-specialpages',
        [   Action.urlLink("Logbücher", URLs.allLogs()) ],  //TODO: wrap up
        't-permalink',
        'pt-watchlist',
        't-recentchangeslinked',
        't-whatlinkshere',
    ]).show(); 

    //------------------------------------------------------------------------------
    //## portlet-communication

    if (Page.owner) {
        var banks   = Communication.doCommunicationBanks();
        // build portlet
        SideBar.newPortlet('portlet-communication').setTitle("Kommunikation").build(
            Communication.doCommunicationBanks()
        ).show();
    }

    //------------------------------------------------------------------------------
    //## p-personal
    
    SideBar.labelItems([
        [ 'pt-mytalk',      "Diskussion"    ],
        [ 'pt-mycontris',   "Beiträge"      ],
    ]);

    // cannot use p-personal which has too much styling
    SideBar.newPortlet('portlet-personal').setTitle("Persönlich").build([
        'pt-userpage',
        'pt-mytalk',
        'pt-mycontris',
        'pt-preferences',
        'pt-logout',
        // 'pt-watchlist'
    ]).show();

    //------------------------------------------------------------------------------
    //## p-lang

    // transform list into a select box
    SideBar.langSelect();
}

doOnLoad(initialize);
/* </nowiki></pre> */