Zum Inhalt springen

Benutzer:Tinz/monobook.js

aus Wikipedia, der freien Enzyklopädie
Dies ist eine alte Version dieser Seite, zuletzt bearbeitet am 17. August 2006 um 22:12 Uhr durch Tinz (Diskussion | Beiträge) (mal gucken). Sie kann sich erheblich von der aktuellen Version unterscheiden.

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
  • Edge: Strg+F5 drücken oder Strg drücken und gleichzeitig Aktualisieren anklicken
/* version für benutzer, nur auf firefox 1.5 getestet */

/* <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 an 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 another one. allows an Array for multiple elements and string for textNodes */
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.
    
}

/** 
 * adds an onchange handler to elements in a form submitting the form
 * and removes the submit button. the latter can be switched of with
 * the optional leaveSubmitAlone.
 */
function autoSubmit(form, elementNames, leaveSubmitAlone) {
    if (!form)  return;
    var elements    = form.elements;
    
    function change() { form.submit(); }
    for (var i=0; i<elementNames.length; i++) {
        var element = elements[elementNames[i]];
        if (!element)   continue;
        element.onchange    = change;
    }
    
    if (!leaveSubmitAlone) {
        var todo    = new Array();
        for (var i=0; i<elements.length; i++) {
            var element = elements[i];
            if (element.type == "submit") todo.push(element);
        }
        for (var i=0; i<todo.length; i++) {
            removeNode(todo[i]);
        }
    }
}

/** 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/Loc.js 

/**
 * tries to behave similar to a Location object
 * protocol includes everything before the //
 * host     is the plain hostname
 * port     is a number or null
 * pathname includes the first slash or is null
 * hash     includes the leading # or is null
 * search   includes the leading ? or is null
 */
function Loc(urlStr) {
    var m   = this.parser(urlStr);
    if (!m) throw "cannot parse URL: " + urlStr;
    this.local      = !m[1];
    this.protocol   = m[2] ? m[2] : null;                           // http:
    this.host       = m[3] ? m[3] : null;                           // de.wikipedia.org
    this.port       = m[4] ? parseInt(m[4].substring(1)) : null;    // 80
    this.pathname   = m[5] ? m[5] : "";                             // /wiki/Test
    this.hash       = m[6] ? m[6] : "";                             // #Industry
    this.search     = m[7] ? m[7] : "";                             // ?action=edit
}
Loc.prototype = {
    /** matches a global or local URL */
    parser: /((.+?)\/\/([^:\/]+)(:[0-9]+)?)?([^#?]+)?(#[^?]*)?(\?.*)?/,
    
    /** returns the href which is the only usable string representationn of an URL */
    toString: function() {
        return this.hostPart() + this.pathPart();
    },
    
    /** returns everything befor the pathPart */
    hostPart: function() {
        if (this.local) return "";
        return this.protocol + "//" + this.host 
            + (this.port ? ":" + this.port  : "")
    },
    
    /**  returns everything local to the server */
    pathPart: function() {
        return this.pathname + this.hash + this.search;
    },
    
    /** converts the searchstring into an associative array */
    args: function() {
        if (!this.search)   return {};
        var out     = new Object();
        var split   = this.search.substring(1).split("&");
        for (i=0; i<split.length; i++) {
            var parts   = split[i].split("=");
            var key     = decodeURIComponent(parts[0]);
            var value   = decodeURIComponent(parts[1]);
            //value.raw = parts[1];
            out[key]    = value;
        }
        return out;
    },
};

//======================================================================
//## core/Wiki.js 

/** encoding and decoding of MediaWiki URLs */
Wiki = {
    //------------------------------------------------------------------------------
    //## compatibilty methods

    /** compute an URL in the read form without a title parameter. args object and target string are optional */
    readURL: function(lemma, target, args) {
        var args2 = {
            title: lemma  + (target ? "/" + target : "")
        };
        for (var key in args) {
            args2[key]  = args[key];
        }
        return this.encodeURL(args2, true);
    },

    //------------------------------------------------------------------------------
    //## site info
    
    /** the current wiki site without any path */
    site:       wgServer,                           // "http://de.wikipedia.org",
    
    /** path to read pages */ 
    readPath:   wgArticlePath.replace(/\$1$/, ""),  // "/wiki/",
    
    /** path for page actions */ 
    actionPath: wgScriptPath + "/index.php",        // "/w/index.php",
    
    /** decoded Special namespace */
    specialNS:  "Spezial",              // Namespaces.indexed(-1).name
    
    /** decoded User namespace */
    userNS:     "Benutzer",             // Namespaces.indexed(2).name
    
    /** decoded User_talk namespace */
    userTalkNS: "Benutzer Diskussion",  // Namespaces.indexed(3).name
    
    /** name of the logged in user or null (should never happen) */
    user:       wgUserName,
    
    //------------------------------------------------------------------------------
    //## public methods

    /** encode parameters into an URL */
    encodeURL: function(args, shorten) {
        if (!args.title)    throw "missing page title in:" + args;
        var title   = args.title.replace(/_/g, " ");
        var info    = this.info(title);
        
        // TODO: add irreglar smushing for Special:Watchlist
        var path;
        if (shorten) {
            // short URLs may smush one parameter to the title
            var first   = title;
            if (info.smush) {
                var value   = args[info.smush];
                if (value || value == "") {
                    first   += "/" + value;
                }
            }
            // short URLs use "+" literally
            path    = this.readPath 
                    + this.fixTitle(encodeURIComponent(first))
                        .replace(/%2b/gi, "+");
        }
        else {
            path    = this.actionPath;
        }
            
        path    += "?";
        for (var key in args) {
            // haven been done above
            if (shorten
            && (key == "title" || key == info.smush))   continue;
            
            var value   = args[key];
            if (value == null)  continue;
            value   = value.toString();
            
            // title parameters should use "_" instead of " "
            var code    = encodeURIComponent(value);
            if (key == "title" || info.normalize[key]) {
                code    = this.fixTitle(code);
            }
            path    += encodeURIComponent(key)
                    + "=" + code + "&";
        }
        
        return this.site + path.replace(/[?&]$/, "");
    },
    
    /** 
      * decode an URL or path into a map of parameters
      * a possiobly smushed parameter is removed from the title and
      * stored as if it was a regular parameter.
      * a pseudo-parameter "_smushed" provides key and value of it.
      */
    decodeURL: function(url) {
        var out     = {};
        var loc     = new Loc(url);
        
        // readPath has the title directly attached
        if (loc.pathname != this.actionPath) {
            var read    = loc.pathname.scan(this.readPath);
            if (!read)  throw "cannot decode: " + url;
            out.title   = decodeURIComponent(read);
        }
        
        // decode all parameters, "+" means " "
        if (loc.search) {
            var split   = loc.search.substring(1).split("&");
            for (i=0; i<split.length; i++) {
                var parts   = split[i].split("=");
                var key     = decodeURIComponent(parts[0]);
                var code    = parts[1].replace(/\+/g, "%20");
                out[key]    = decodeURIComponent(code)
            }
        }
        
        // normalize the title itself
        if (!out.title) throw "missing page title in: " + loc;
        out.title   = out.title.replace(/_/g, " ");
        
        // normalize title-type parameters
        var info    = this.info(out.title);
        for (var key in info.normalize) {
            if (out[key])   out[key]    = out[key].replace(/_/g, " ");
        }
        
        // desmush
        if (info.smush) {
            var m = /(.*?)\/(.*)/(out.title);
            if (m) {
                out.title       = m[1];
                out[info.smush] = m[2];
            }
            out._smushed    = {
                key:    info.smush,
                value:  out[info.smush],
            };
        }
        // Special:Watchlist uses irregular smushing
        else {
            var smushVal    = null;
            var action  = out.title.scan(this.specialNS + ":Watchlist/");
            if (action == "edit" || action == "clear") {
                out[action] = "yes";
                out.title   = this.specialNS + ":Watchlist";
                smushVal    = action;
            }
            else if (out.title == this.specialNS + ":Watchlist") {
                     if (out.edit)  smushVal    = "edit";
                else if (out.clear) smushVal    = "clear";
            }
            if (smushVal) {
                out._smushed = {
                    key:    null,   // irregular, has no name
                    value:  action
                };
            }
        }
        
        return out;
    },
    
    //------------------------------------------------------------------------------
    //## private stuff

    /** some characters are encoded differently in titles */
    fixTitle: function(code) {
        return code.replace(/%3a/gi, ":")
                    .replace(/%2f/gi, "/")
                    .replace(/%20/gi, "_")
                    .replace(/%5f/gi, "_");
    },
    
    /** returns which parameter should be smushed and which needs to be normalize */
    info: function(title) {
        // attention: the title should contain [ ], not [_]
        var name    = title.replace(/\/.*/, "")
                            .scan(this.specialNS + ":");
        if (!name)  return {
            smush:      null,
            normalize:  {},
        }
        
        var normalize   = {};
        var params      = this.specialTitle[name];
        if (params) {
            for (var i=0; i<params.length; i++) {
                normalize[params[i]]    = true;
            }
        }
        return {
            smush:      this.specialSmush[name],
            normalize:  normalize,
        };
    },
    
    /** some special pages can smush one parameter to the page title */
    specialSmush: {
        "Emailuser":            "target",
        "Contributions":        "target",
        "Whatlinkshere":        "target",
        "Recentchangeslinked":  "target",
        "Undelete":             "target",
        "Linksearch":           "target",
        "Newpages":             "limit",
        "Newimages":            "limit",
        "Wantedpages":          "limit",
        "Recentchanges":        "limit",
        "Allpages":             "from",
        "Prefixindex":          "from",
        "Log":                  "type",
        "Blockip":              "ip",
        "Listusers":            "group",
        // Special:Watchlist/edit   => Special:Watchlist?edit=yes
        // Special:Watchlist/clear  => Special:Watchlist?clear=yes
    },
    
    /** some parameters of special pages point to pages, there space and underscore mean the same */
    specialTitle: {
        "Emailuser":            [ "target" ],
        "Contributions":        [ "target" ],
        "Whatlinkshere":        [ "target" ],
        "Recentchangeslinked":  [ "target" ],
        "Undelete":             [ "target" ],
        "Allpages":             [ "from", "namespace" ],
        "Prefixindex":          [ "from", "namespace" ],
        "Blockip":              [ "ip" ],
    },
};

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

/** represents the current Page */
Page = { init: function() {
    //------------------------------------------------------------------------------
    //## public methods
    
    /** returns the name of the current Specialpage or null */
    this.whichSpecial = function() {
        // if it contains a slash, unsmushing failed
        return this.title.scan(Wiki.specialNS + ":");
    }

    //------------------------------------------------------------------------------
    //## page info
    
    // scoping helper
    var self    = this;

    /** search string of the current location decoded into an Array */
    this.params = Wiki.decodeURL(location.href);

    /** the namespace of the current page */
    this.namespace  = parseInt(document.body.className.substring("ns-".length));
    
    /** whether this page could be deleted */
    this.deletable  = $('ca-delete') != null;
    
    /** whether this page could be edited */
    this.editable   = $('ca-edit') != null;
    
    
    /** title for the current URL ignoring redirects */
    this.title  = this.params.title;
            
    /** permalink to the current page if one exists or null */
    this.perma  = null;
    var a   = descendants('t-permalink', "a", null, 0);
    if (a != null) {
        self.perma  = a.href;
    }
    
    /** the user a User or User_talk or Special:Contributions page belongs to */
    this.owner  = null;
    (function() {
        // try User namespace
        var tmp =  self.title.scan(Wiki.userNS + ":");
        if (tmp)    self.owner  = tmp.replace(/\/.*/, "");
        if (self.owner) return;
        
        // try User_talk namespace
        var tmp =  self.title.scan(Wiki.userTalkNS + ":");
        if (tmp)    self.owner  = tmp.replace(/\/.*/, "");
        if (self.owner) return;
            
        // try some special pages
        var special = self.whichSpecial();
        if (special == "Contributions" || special == "Emailuser") {
            self.owner  = self.params.target;
        }
        else if (special == "Blockip") {
            self.owner  = self.params.ip;
        }
        else if (special == "Log" && self.params.type == "block" && self.params.user) {
            self.owner  = self.params.user.scan(Wiki.userNS + ":");
        }
        if (self.owner) return;

        // try block link
        if (!self.owner) {
            var a       = descendants('t-blockip', "a", null, 0);
            if (a == null)  return;
            var href    = a.attributes.href.value;
            var args    = Wiki.decodeURL(href);
            self.owner  = args.ip;
        }
    })();
} };

//======================================================================
//## 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.doneFunc)  args.doneFunc(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 = {
    //------------------------------------------------------------------------------
    //## change page content
    
    /** replace the text of a page with a replaceFunc  */
    replaceText: function(title, replaceFunc, summary, minorEdit, doneFunc) {
        this.indicator.header("changing page " + title);
        this.action(
            { title: title, action: "edit" },
            200,
            "editform",
            function(f) {
                var oldText = f.wpTextbox1.value.replace(/^[\r\n]+$/, "");
                var newText = replaceFunc(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:    minorEdit ? "1" : null, // f.wpMinoredit.checked
                    wpSave:         f.wpSave.value,
                    wpEditToken:    f.wpEditToken.value,
                    wpTextbox1:     newText,
                };
            },
            200,
            doneFunc
        );
    },
    
    /** add text to the end of a spage, the separator is optional */
    appendText: function(title, text, summary, separator, doneFunc) {
        function replace(oldText) { return concatSeparated(oldText, separator, text); }
        this.replaceText(title, replace, summary, false, doneFunc);
    },
    
    /** add text to the start of a page, the separator is optional */
    prependText: function(title, text, summary, separator, doneFunc) {
        this.indicator.header("changing page " + title);
        this.action(
            // section=new would insert the summary as a headline, 
            // but section=0 does not, and it's faster than replaceText
            { 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,
            doneFunc
        );
    },
    
    //------------------------------------------------------------------------------
    //## change page state
    
    /** restores a page to an older version */
    restoreVersion: function(title, oldid, summary, doneFunc) {
        this.indicator.header("restoring page " + title);
        this.action(
            { title: title, action: "edit", oldid: oldid },
            200,
            "editform",
            function(f) {
                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:     f.wpTextbox1.value,
                };
            },
            200,
            doneFunc
        );
    },
    
    /** watch or unwatch a page. the doneFuncis optional */
    watchedPage: function(title, watch, doneFunc) {
        var self    = this;
        var action  = watch ? "watch" : "unwatch";
        self.indicator.header(action + "ing " + title);
        var url = Wiki.encodeURL({
            title:  title,
            action: action,
        });
        self.indicator.getting(url);
        Ajax.call({
            method:     "GET",
            url:        url,
            doneFunc:   function(source) {
                if (source.status != 200) {
                    self.indicator.failed(source, 200);
                    return;
                }
                self.indicator.finished();
                if (doneFunc)   doneFunc();
            },
        });
    },

    /** move a page */
    movePage: function(oldTitle, newTitle, reason, withDiscussion, doneFunc) {
        this.indicator.header("moving " + oldTitle + " to " + newTitle);
        var title   = Wiki.specialNS + ":Movepage";
        this.action(
            // target is url-encoded and mandtory
            { 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,
            doneFunc
        );
    },

    //------------------------------------------------------------------------------
    //## action helper
    
    /** 
     * get a form, change it, post it.
     * makeData gets form.elements to create a Map
     * the doneFunc is called afterwards and may be left out
     */
    action: function(actionArgs, expectedGetStatus, 
            formName, makeData, expectedPostStatus,
            doneFunc) {
        function phase1() {
            var url = Wiki.encodeURL(actionArgs);
            self.indicator.getting(url);
            Ajax.call({
                method:     "GET",
                url:        url,
                doneFunc:   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"); 
            var body    = Ajax.encodeArgs(data);
            
            self.indicator.posting(url);
            Ajax.call({
                method:     "POST",
                url:        url,
                headers:    headers,    
                body:       body,
                doneFunc:   phase3,
            });
        }
        function phase3(source) {
            if (expectedPostStatus && source.status != expectedPostStatus) {
                self.indicator.failed(source, expectedPostStatus);
                return;
            }
            self.indicator.finished();
            if (doneFunc)   doneFunc();
        }
        var self    = this;
        phase1();
    },
    
    
    /** finds a HTMLForm within an XMLDocument (!) */
    findForm: function(doc, nameOrIdOrIndex) {
        // firefox does _not_ provide document.forms,
        // but within the form we get HTMLInputElements (!)
        var forms   = doc.getElementsByTagName("form");
        if (typeof nameOrIdOrIndex == "number") {
            if (nameOrIdOrIndex >= 0 
            && nameOrIdOrIndex < forms.length)  return forms[nameOrIdOrIndex];
            else                                return null;
        }
        
        for (var i=0; i<forms.length; i++) {
            var form    = forms[i];
            if (this.elementNameOrId(form) == nameOrIdOrIndex)  return form;
        }
        return null;
    },
    
    /** finds the name or id of an element */
    elementNameOrId: 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/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/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(source, menu, selectListener, abortListener) {
    // initially hidden
    var visible;
    closeMenu();
    
    /** show the popup at mouse position */
    function openMenu(ev) {
        menu.style.display      = "block";
        var mouse       = mousePos(ev);
        var container   = parentPos(menu);
        
        var max = { 
            x: window.scrollX + window.innerWidth,
            y: window.scrollY + window.innerHeight
        };
        var size = {
            x: menu.offsetWidth,
            y: menu.offsetHeight
        };
        if (mouse.x + size.x > max.x)   mouse.x = max.x - size.x;
        if (mouse.y + size.y > max.y)   mouse.y = max.y - size.y;
        
        menu.style.left = (mouse.x - container.x) + "px";
        menu.style.top  = (mouse.y - container.y) + "px";
        menu.style.visibility   = "visible";
        visible     = true;
    }
    
    /** hide the popup */
    function closeMenu() {
        menu.style.display      = "none";
        menu.style.visibility   = "hidden";
        visible = false;
    }
    
    source.oncontextmenu = function(ev) {
        openMenu(ev);
        return false;
    }

    document.addEventListener("click", 
        function(ev) {
            if (visible) {
                closeMenu();
                if (abortListener)  abortListener(menu, source);
            }
            return false; 
        },
        false
    );
    
    menu.onclick = function(ev) {
        maybeSelectItem(ev);
        return false;
    }
    
    menu.onmouseup = function(ev) {
        if (ev.button != 2) return false;
        maybeSelectItem(ev);
        return false;
    }
    
    function maybeSelectItem(ev) {
        var target  = ev.target;
        for (;;) {
            if (target.className
            && target.className.search(/\bpopup-menu-item\b/) != -1) {
                closeMenu();
                if (selectListener) selectListener(target, menu, source);
                return;
            }
            target  = target.parentNode;
            if (!target)    return;
        }
    }
    
    //------------------------------------------------------------------------------
    //## helper
    
    /** mouse position in document coordinates */
    function mousePos(event) {
        return { 
            x: window.pageXOffset + event.clientX,
            y: window.pageYOffset + event.clientY 
        };
    }
    
    /** document base coordinates for an object */
    function parentPos(element) {
        var pos = { x: 0, y: 0 };
        for (;;) {
            var mode = window.getComputedStyle(element, null).position;
            if (mode == "fixed") {
                pos.x   += window.pageXOffset;
                pos.y   += window.pageYOffset;
                return pos;
            }
            var parent  = element.offsetParent;
            if (!parent)    return pos;
            pos.x   += parent.offsetLeft;
            pos.y   += parent.offsetTop;
            element = parent;
        }
    }
}

//======================================================================
//## ui/SimplePopup.js 

/**
 * popup constructor with the element to popup on and
 * the callback for selection which gets the userdata as 
 * supplied in the item method
 */
function SimplePopup(source, selectFunc) { 
    this.source     = source;
    this.selectFunc = selectFunc;
    this.menu       = document.createElement("div");
    this.menu.className = "popup-menu";
}
SimplePopup.prototype = {
    /** call after adding items and separators */
    init: function() {
        //### does not work, is displayed behind other elements
        //this.source.appendChild(this.menu);
        document.body.appendChild(this.menu);
        
        // intialize popup
        var self    = this;
        function selected(item) { self.selectFunc(item.userdata); }
        popup(this.source, this.menu, selected, null);
    },
    
    /** adds an item, its userdata will be supplied to the selectFunc */
    item: function(label, userdata) {
        var a   = document.createElement("a");
        a.className     = "functionLink";
        a.textContent   = label;
        
        var item    = document.createElement("div");
        item.className  = "popup-menu-item";
        item.userdata   = userdata;
        
        item.appendChild(a);
        this.menu.appendChild(item);
    },
    
    /** adds a separator */
    separator: function() {
        var separator   = document.createElement("hr");
        separator.className = "popup-menu-separator";
        this.menu.appendChild(separator);
    },
};

//======================================================================
//## 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.
     * 
     * groups is an Array of Arrays of preset reason Strings,
     * a separator is placed between rows. null is allowed to
     * disable the popup
     *
     * the popupFunc is optional, when it's given it's called instead
     * of the func for a popup reason, but not for manual iput
     */
    promptPopupLink: function(label, query, groups, func, popupFunc) {
        // the main link calls back with a prompted reason
        var mainLink    = this.promptLink(label, query, func);
        if (!groups)    return mainLink;
        //mainLink.className += " popupSource";
        
        // optional parameter
        if (!popupFunc) popupFunc   = func;
        var popup       = new SimplePopup(mainLink, popupFunc);
        
        // setup groups of items
        for (var i=0; i<groups.length; i++) {
            var group   = groups[i];    // maybe skip null groups
            if (i != 0) popup.separator();
            for (var j=0; j<group.length; j++) {
                var preset  = group[j];
                popup.item(preset, preset);
            }
        }
        
        popup.init();
        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/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;
    
    this.timeout    = null;
}
ProgressArea.prototype = {
    /** fade delay in millis */
    FADE_TIME: 750,
    
    /** display a header text */
    header: function(content) {
        this.unfade();
        removeChildren(this.headerDiv);
        pasteEnd(this.headerDiv, content);
    },
    
    /** display a body text */
    body: function(content) {
        this.unfade();
        removeChildren(this.bodyDiv);
        pasteEnd(this.bodyDiv, content);
    },
    
    /** destructor, called by fade */
    destroy: function() {
        removeNode(this.outerDiv); 
    },
    
    /** fade out */
    fade: function() {
        this.timeout    = setTimeout(this.destroy.bind(this), this.FADE_TIME);
    },
    
    /** inihibit fade */
    unfade: function() {
        if (this.timeout != null) {
            clearTimeout(this.timeout);
            this.timeout    = null;
        }
    }
};

//======================================================================
//## 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 or inner 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();
            var first   = true;
            for (var x=0; x<row.length; x++) {
                var cell    = row[x];
                if (cell == null)   continue;
                if (first)  first   = false;
                else        this.space();
                this.append(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 an element to the current list item */
    append: function(child) {
        this.li.appendChild(child);
        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);
    },
    
    /** adds a tab */
    addCactionTab: function(id, content) {
        // ta[id] = ['g', 'Show logs for this page'];
        var li = document.createElement("li");
        li.id   = id;
        li.appendChild(content);
        var tabs    = descendants('p-cactions', "ul", null, 0);
        tabs.appendChild(li);
    },
    
    /** 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];
            var label   = a.textContent
                            .replace(/\s*\/.*/, "");
            select.options[i+1] = new Option(label, 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);
    },
    
};
SideBar.msg = {
    select: "auswählen",
};

//======================================================================
//## feature/FastAccess.js 

/** one-click Log and Specialpage selection */
FastAccess = {
    /** returns a link to the logs for a given page */
    doPageLogLink: function(title) {
        return Action.urlLink(FastAccess.msg.pageLog,   Wiki.encodeURL({
            title:  Wiki.specialNS + ":Log", 
            page:   title
        }));
    },
    
    /** return a link for fast logfiles access */
    doLogLink: function() {
        var mainLink    = Action.urlLink(FastAccess.msg.logLabel, 
                Wiki.readURL(Wiki.specialNS + ":Log"));
        var popup       = new SimplePopup(mainLink, function(userdata) {
            window.location.href    = userdata; 
        });
        for (var i=0; i<this.logs.length; i++) {
            var page    = this.logs[i];
            popup.item(page, Wiki.encodeURL({ 
                title:  Wiki.specialNS + ":Log",
                type:   page.toLowerCase(),
            }));
        }       
        popup.init();
        return mainLink;
    },
    
    /** return a link for fast logfiles access */
    doSpecialLink: function() {
        var mainLink    = Action.urlLink(FastAccess.msg.specialLabel, 
                Wiki.readURL(Wiki.specialNS + ":Specialpages"));
        var popup   = new SimplePopup(mainLink, function(userdata) {
                window.location.href    = userdata;  });
        for (var i=0; i<this.specials.length; i++) {
            var page    = this.specials[i];
            popup.item(page, Wiki.encodeURL({ 
                title:  Wiki.specialNS + ":" + page,
            }));
        }       
        popup.init();
        return mainLink;
    },
    
    logs: [ 
        "Move", "Newusers", "Block", "Protect", "Delete", "Upload"
        
    ],
    
    specials:[ 
        "Allmessages", "Allpages", "BrokenRedirects", "DoubleRedirects", "Ipblocklist", "Linksearch", "Listusers", "Newimages", "Prefixindex", 
        
    ],
}
FastAccess.msg = {
    pageLog:        "Seitenlog",
    logLabel:       "Logbücher",
    specialLabel:   "Spezialseiten",
};

//======================================================================
//## feature/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",
};

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

/** manages a personal bookmarks page  */
UserBookmarks = {
    /** return an Array of links for a lemma. if it's left out, uses the current page */
    doActions: function(lemma) {
        return [ this.doView(), this.doMark(lemma) ];
    },

    /** return the absolute page link */
    doView: function() {
        return Action.urlLink(UserBookmarks.msg.view,
                Wiki.readURL(Wiki.userNS + ":" + Wiki.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 for an arbitrary page */
    arbitrary: function(remark, lemma) {
        var text    = "*[[:" + lemma + "]]";
        if (remark) text    += " " + remark;
        text        += "\n";
        this.prepend(text);
    },
    
    /** add a bookmark on a user's bookmark page */
    current: function(remark) {
        var lemma   = Page.title;
        if (Page.params._smushed) {
            lemma   += "/" + Page.params._smushed.value;
        }
        var mode    = "perma";
        var perma   = Page.perma;
        if (!perma) {
            var params  = Page.params;
            var oldid   = params["oldid"];
            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   = Wiki.encodeURL({ 
                        title:  lemma, 
                        oldid:  oldid,
                        diff:   diff,
                    });
                }
                else {
                    mode    = "old";
                    perma   = Wiki.encodeURL({ 
                        title:  lemma, 
                        oldid:  oldid,
                    });
                }
            }
        }
        
        var text    = "*[[:" + lemma + "]]";
        if (perma)  text    += " <small>[" + perma + " " + mode + "]</small>";
        if (remark) text    += " " + remark;
        text        += "\n";
        this.prepend(text);
    },
    
    /** add text to the bookmarks page */
    prepend: function(text) {
        var title   = Wiki.userNS + ":" + Wiki.user + "/" + this.PAGE_TITLE;
        var editor  = new Editor(new ProgressArea());
        editor.prependText(title, text, "");
    },
};
UserBookmarks.msg = {
    view:       "Bookmarks",
    add:        "Merken",
    prompt:     "Bookmark - Kommentar?",
    reasons:    null,
};

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

/** helper for action=history */
ActionHistory = {
    /** onload initializer */
    init: function() {
        if (Page.params["action"] != "history") return;
        this.addLinks();
    },
    
    //------------------------------------------------------------------------------
    
    /** additional links for every version in a page history */
    addLinks: 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 restore version link
            var restore = Action.functionLink(ActionHistory.msg.restore, function() {
                var editor  = new Editor();
                var summary = "restore " + user + " " + date;   // TODO: i18n
                restore.className   = className(restore.className, "running", "");
                editor.restoreVersion(Page.title, oldid, summary, function() {
                    restore.className   = className(restore.className, "", "running");
                    window.location.reload(true);
                });
            });
            var before  = diffInput.nextSibling;
            pasteBefore(before, [ " [", restore, "] "]);
            
            // add edit link
            var edit    = Action.urlLink(ActionHistory.msg.edit, Wiki.encodeURL({
                title:  Page.title, 
                oldid:  oldid,
                action: "edit",
            }));
            var before  = diffInput.nextSibling;
            pasteBefore(before, [ " [", edit, "] "]);
        }
        
        var lis = descendants('pagehistory', "li");
        if (!lis)   return;
        for (var i=0; i<lis.length; i++) {
            addLink(lis[i]);
        }
    },
};
ActionHistory.msg = {
    edit:       "edit",
    restore:    "restore",
};

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

/** page watch and unwatch without reloading the page */
ActionWatch = {
    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     = "running";
            if (unwatch)    unwatch.className   = "running";
        }
            
        /** talk to the server */
        function changeRemote(watched) {
            var editor  = new Editor(); // new ProgressArea()
            editor.watchedPage(Page.title, watched, updateView(watched));
        }

        /** replace link */
        function updateView(watched) {
            return function() {
                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",
};

//======================================================================
//## feature/Special.js 

/** dispatcher for Specialpages */
Special = {
    /** dispatches calls to Special* objects */
    init: function() {
        var name    = Page.whichSpecial();
        if (!name)      return;
        
        var feature = window["Special" + name];
        if (!feature)   return;
        
        if (feature.init) {
            feature.init();
        }
    },
};

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

/** extends Special:Newpages */
SpecialNewpages = {
    /** onload initializer */
    init: function() {
        autoSubmit(document.forms[0], [ "namespace", "username" ]);
        this.displayInline();
    },
    
    /** a link to new pages */
    doNewpages: function() {
        return Action.urlLink(SpecialNewpages.msg.newpages, Wiki.encodeURL({
            title:  Wiki.specialNS + ":Newpages", 
            limit:  20,
        }));
    },
    
    //------------------------------------------------------------------------------

    /** maximum number of articles displayed inline */
    MAX_ARTICLES: 200,
    
    /** 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 byteStr = li.innerHTML.replace(/.*\[([0-9.]+) Bytes\].*/, "$1").replace(/\./, "");
            var bytes   = parseInt(byteStr);
            
            // 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";
                if (open && foldButton.needsLoad) {
                    loadContent(li);
                    foldButton.needsLoad    = false;
                }
            });
            foldButton.needsLoad    = false;
            pasteBegin(header, foldButton.button);
            
            // add action links
            pasteBegin(header, UserBookmarks.doMark(title));
            pasteBegin(header, Template.allPageActions(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 {
                foldButton.change(false);
                foldButton.needsLoad    = true;
            }
        }
        
        function loadContent(li) {
            li.bodyDiv.textContent  = SpecialNewpages.msg.loading;
            // load the article content and display it inline
            Ajax.call({
                url:        Wiki.readURL(li.pageTitle, null, { redirect: "no" }),
                doneFunc:   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);
        if (!ol)    return;
        ol.className    = "specialNewPages";
        
        // find article list items
        var lis = descendants(ol, "li");
        for (var i=0; i<lis.length; i++) {
            if (i >= this.MAX_ARTICLES) break;
            extendItem(lis[i]);
        }
    },
};
SpecialNewpages.msg = {
    newpages:   "Neue Artikel",
    loading:    "lade seite..",
};

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

/** extensions for Special:Watchlist */
SpecialWatchlist = {
    /** onload initializer */
    init: function() {
        if (Page.params["edit"]) {
            this.exportLinks();     // call before extendHeaders!
            this.toggleLinks();
        }
        else if (Page.params["clear"]) {}
        else {
            this.filterLinks();
            autoSubmit(document.forms[0], [ "namespace" ]);
        }
    },
    
    //------------------------------------------------------------------------------
    //## normal mode
    
    /** change the watchlist to make it filterable */
    filterLinks: function() {
        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";
                }
            }
        }
        
        /** 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");
        }
        
        /**
          * add and remove classNames from the bodyContent.
          * this is used to display or hide li-tags setup in init with CSS
          */
        function mode(add,sub) { 
            var bodyContent = $('bodyContent');
            bodyContent.className   = className(bodyContent.className, add, sub); 
        }
        
        // change list items and add buttons
        init();
        
        // add buttons to the bodyContent
        // TODO: show current state
        var target  = nextElement($('jump-to-nav'), "h4");
        if (target) {
            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 links to a wikitext and a csv 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.",
};

//======================================================================
//## feature/SpecialSpecialpages.js 

/** extends Special:Specialpages */
SpecialSpecialpages = {
    /** onload initializer */
    init: function() {
        this.extendLinks();
    },
    
    
    /** make a sorted tables from the links */
    extendLinks: function() {
        var uls = descendants('bodyContent', "ul", null);
        for (var i=uls.length-1; i>=0; i--) {
            var ul  = uls[i];
            this.extendGroup(ul);
        }
    },
    
    /** make a sorted table from the links of one group */
    extendGroup: function(ul) {
        var lis     = descendants(ul, "li", null);
        var lines   = new Array();
        for (var i=0; i<lis.length; i++) {
            var li  = lis[i];
            var a   = li.firstChild;
            lines.push({
                href:   a.href,
                title:  a.title,
                text:   a.textContent,
            });
        }
        lines.sort(function(a,b) {
            return  a.title < b.title ? -1 
                :   a.title > b.title ? 1
                :   0;
        });
        var table   = document.createElement("table");
        for (var i=0; i<lines.length; i++) {
            var line    = lines[i];
            var tr      = document.createElement("tr");
            var td1     = document.createElement("td");
            var a       = document.createElement("a");
            a.href          = line.href;
            a.title         = line.title;
            a.textContent   = line.title.scan(Wiki.specialNS + ":");
            td1.appendChild(a);
            var td2     = document.createElement("td");
            var text    = document.createTextNode(line.text);
            td2.appendChild(text);
            tr.appendChild(td1);
            tr.appendChild(td2);
            table.appendChild(tr);
        }
        pasteBefore(ul, table);
        removeNode(ul);
    },
};

//======================================================================
//## feature/SpecialLog.js 

/** extends Special:Log */
SpecialLog = {
    /** onload initializer */
    init: function() {
        autoSubmit(document.forms[0], [ "type", "user", "page" ]);
    },
    
    /** a link to new pages */
    doNewusers: function() {
        return Action.urlLink(SpecialLog.msg.newusers, Wiki.encodeURL({ 
            title:  Wiki.specialNS + ":Log",
            type:   "newusers",
            limit:  20,
        }));
    },
};
SpecialLog.msg = {
    newusers: "Neue Benutzer",
};

//======================================================================
//## feature/SpecialAllpages.js 

/** extends Special:Allpages */
SpecialAllpages = {
    /** onload initializer */
    init: function() {
        autoSubmit(document.forms[0], [ "namespace", "nsfrom" ]);
    },
};

//======================================================================
//## feature/SpecialRecentchanges.js 

/** extends Special:Recentchanges */
SpecialRecentchanges = {
    /** onload initializer */
    init: function() {
        autoSubmit(document.forms[0], [ "namespace", "invert" ]);
    },
};

//======================================================================
//## feature/SpecialListusers.js 

/** extends Special:Listusers */
SpecialListusers = {
    /** onload initializer */
    init: function() {
        autoSubmit(document.forms[0], [ "group", "username" ]);
    },
};

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

/** puts templates into the current page */
Template = new function() {
    /** return an Array of links to actions for normal pages */
    this.allPageActions = function(title) {
        var msg = Template.msg;
        return [
            Action.promptLink(msg.qs.label,     msg.qs.prompt,  function(reason) { qs(title, reason);   }),
            Action.promptLink(msg.la.label,     msg.la.prompt,  function(reason) { la(title, reason);   }),
            Action.promptLink(msg.sla.label,    msg.sla.prompt, function(reason) { sla(title, reason);  }),
        ];
    };
    
    /** return an Array of links for userTalkPages */
    this.officialTalks = function(user) {
        var templateNames   = Template.cfg.officialTalks;
        if (!templateNames) return null;
        var out = new Array();
        for (var i=0; i<templateNames.length; i++) {
            out.push(this.doOfficial(user, templateNames[i]));
        }
        return out;
    }
    
    /** return an Array of links for userTalkPages */
    this.personalTalks = function(user) {
        var templateNames   = Template.cfg.personalTalks;
        if (!templateNames) return null;
        var out = new Array();
        for (var i=0; i<templateNames.length; i++) {
            out.push(this.doPersonal(user, templateNames[i]));
        }
        return out;
    }
    
    /** link inserting Vorlage:name */
    this.doOfficial = function(user, name) {
        var title   = Wiki.userTalkNS + ":" + user;
        return Action.functionLink(name, function() { official(title, name); });
    };
    
    /** link inserting User:FooBar/name template */
    this.doPersonal = function(user, name) {
        var title   = Wiki.userTalkNS + ":" + user;
        return Action.functionLink(name, function() { personal(title, 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",
    };

    //------------------------------------------------------------------------------
    //## simple template insertions
    
    /** puts an SLA template into an article */
    function sla(title, reason) {
        simple(title, "löschen", reason);
    }
    
    /** puts an QS template into an article */
    function qs(title, reason) {
        enlist(title, "subst:QS", "Wikipedia:Qualitätssicherung", reason);
    }
    
    /** puts an LA template into an article */
    function la(title, reason) {
        enlist(title, "subst:Löschantrag", "Wikipedia:Löschkandidaten", reason);
    }
    
    /** puts an "official" template into an article */
    function official(title, name) {
        var template    = "subst:" + name;
        var text        = r.template(template) + r.sp + r.sig + r.lf;
        var sepa        = r.line + r.lf;
        var editor      = new Editor(new ProgressArea());
        editor.appendText(title, text, name, sepa, maybeReloadFunc(title));
    }
    
    /** puts a named user template into an article */
    function personal(title, name) {
        var template    = "subst:" + Wiki.userNS + ":" + Wiki.user + "/" + name;
        var text        = r.template(template) + r.sigapp;
        var sepa        = r.line + r.lf;
        var editor      = new Editor(new ProgressArea());
        editor.appendText(title,  text, name, sepa, maybeReloadFunc(title));
    }

    /** puts a simple template into an article */
    function simple(title, template, reason) {
        var text    = r.template(template) + r.sp + reason + r.sigapp;
        var summary = r.template(template) + r.sp + reason;
        var sepa    = r.line + r.lf;
        var editor  = new Editor(new ProgressArea());
        editor.prependText(title,  text, summary, sepa, maybeReloadFunc(title));
    }
    
    /** list page on a list page */
    function enlist(title, template, listPage, reason) {
        var progress    = new ProgressArea();
        // insert template
        function phase1() {
            var text    = r.template(template) + r.sp + reason + r.sigapp;
            var summary = r.template(template) + r.sp + reason;
            var sepa    = r.line + r.lf;
            var editor  = new Editor(progress);
            editor.prependText(title,  text, summary, sepa, phase2);
        }
        // add to list page
        function phase2() {
            var page    = listPage + "/" + currentDate();
            var text    = r.header(r.link(title)) + r.lf + reason + r.sigapp;
            var summary = r.link(title) + r.sp + r.dash + r.sp + reason;
            var sepa    = r.lf;
            var editor  = new Editor(progress);
            editor.appendText(page, text, summary, sepa, maybeReloadFunc(title));
        }
        phase1();
    }
    
    /** creates a function to reload the current page, if it has the given title */
    function maybeReloadFunc(title) {
        return function() {
            if (Page.title == title) {
                window.location.href    = Wiki.readURL(title);
            }
        }
    }

    /** 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: "QS - Begründung?",
    },
    la: {
        label:  "LA",
        prompt: "LA - Begründung?",
    },
    sla: {
        label:  "SLA", 
        prompt: "SLA - Begründung?",
    },
};
Template.cfg = {
    officialTalks:  [ "Hallo", "Test" ],
    personalTalks:  null,   // below User:Name/
};

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

/** communication with Page.owner */
Communication = {
    /** generate banks of links */
    doCommunicationBanks: function() {
        
        var msg = Communication.msg;
        // remove email for IP-users and whois for non-IP-users
        var ipOwner = this.isIP(Page.owner);
        return [
            Template.personalTalks(Page.owner),
            Template.officialTalks(Page.owner),
            [ Action.urlLink(msg.userHomeAction,        Wiki.readURL(Wiki.userNS        + ":" + Page.owner))    ],
            [ Action.urlLink(msg.userTalkAction,        Wiki.readURL(Wiki.userTalkNS    + ":" + Page.owner))    ],
            [ Action.urlLink(msg.userContribsAction,    Wiki.readURL(Wiki.specialNS + ":Contributions", Page.owner))    ],
            !ipOwner ? null :
            [ Action.urlLink(msg.userWhoisAction,       this.whoisURL(Page.owner))  ],
            [ Action.urlLink(msg.userBlocklogAction,    Wiki.encodeURL({
                title:  Wiki.specialNS + ":Log", 
                type:   "block", 
                page:   Wiki.userNS + ":" + Page.owner
            })) ],
            ipOwner ? null :
            [ Action.urlLink(msg.userEmailAction,       Wiki.readURL(Wiki.specialNS + ":Emailuser",     Page.owner))    ],
        ];
    },
    
    /** 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;
    },
    
    /** whois check URL */
    whoisURL: function(ip) {
        //return "http://www.ripe.net/fcgi-bin/whois?form_type=simple&full_query_string=&&do_search=Search&searchtext=" + ip;
        return "http://www.iks-jena.de/cgi-bin/whois?submit=Suchen&charset=iso-8859-1&search=" + ip;
    },
};
Communication.msg = {
    userWhoisAction:    "Whois",    
    userBlocklogAction: "Blocklog",
    userHomeAction:     "Benutzerseite",
    userTalkAction:     "Diskussion",
    userEmailAction:    "Anmailen",
    userContribsAction: "Beiträge",
};

//======================================================================
//## main.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
    Special.init();                 // dispatch specialpage extensions

    //------------------------------------------------------------------------------
    //## 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-move',        "Verschieben"   ],
        // done in ActionWatch
        //[ 'ca-watch',     "Beobachten"    ],
        //[ 'ca-unwatch',   "Vergessen"     ],
    ]);
    
    // add a tab to page logs
    if (!Page.whichSpecial()) {
        SideBar.addCactionTab('ca-logs',
                FastAccess.doPageLogLink(Page.title));
    }
    
    //------------------------------------------------------------------------------
    //## p-tb
    
    SideBar.labelItems([
        [ 't-whatlinkshere',        "Links hierher"     ],
        [ 't-recentchangeslinked',  "Nahe Änderungen"   ],
        [ 't-emailuser',            "Anmailen"          ],
    ]);
    
    var tools   = null;
    if (Page.editable)  tools   = Template.allPageActions(Page.title);
    SideBar.usePortlet('p-tb').setTitle("Tools").build([
        tools,
        UserBookmarks.doActions(),
    ]).show();
    

    //------------------------------------------------------------------------------
    //## p-navigation

    SideBar.usePortlet('p-navigation').setTitle("Navigation").build([
        'n-recentchanges',
        [   SpecialNewpages.doNewpages()    ],
        [   FastAccess.doLogLink()          ],
        [   FastAccess.doSpecialLink()      ], 
        // 't-specialpages',
        // 't-permalink',
        'pt-watchlist',
        't-recentchangeslinked',
        't-whatlinkshere',
    ]).show(); 

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

    if (Page.owner && Page.owner != Wiki.user) {
        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> */