Zum Inhalt springen

Benutzer:D/monobook/admin.js

aus Wikipedia, der freien Enzyklopädie
Dies ist eine alte Version dieser Seite, zuletzt bearbeitet am 30. August 2006 um 23:03 Uhr durch D (Diskussion | Beiträge). Sie kann sich erheblich von der aktuellen Version unterscheiden.
(Unterschied) ← Nächstältere Version | Aktuelle Version (Unterschied) | Nächstjüngere Version → (Unterschied)

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 admins, nur auf firefox 1.5 getestet''' -- [[Benutzer:D/monobook/admin.js]] */

/* <pre><nowiki> */

//======================================================================
//## util/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");
};

//======================================================================
//## util/functions.js 

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

/** 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.
    
}

/** HACK: call a target's zero-argument method after a timeout */
function doLater(target, method, timeout) {
    return window.setTimeout(
            method.bind(target), timeout);
}

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

/** matches IPv4-like strings */
var ipRE    = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;

/** true when the ip String denotes an IPv4-address */
function isIP(s) {
    var m   = ipRE(s);
    if (!m) return false;
    for (var i=1; i<=4; i++) {
        var byt = parseInt(m[i]);
        if (byt < 0 || byt > 255)   return false;
    }
    return true;
}

//======================================================================
//## util/DOM.js 

/** DOM helper functions */
DOM = {
    //------------------------------------------------------------------------------
    //## finder

    /** find descendants of an ancestor by tagName, className and index */
    fetch: function(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 = [];
            for (var i=0; i<elements.length; i++) {
                if (this.hasClass(elements[i], 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 */
    nextElement: function(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 */
    previousElement: function(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; }
        }
    },

    //------------------------------------------------------------------------------
    //##  manipulation

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

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

    /** inserts an element before another one. allows an Array for multiple elements and string for textNodes */
    pasteBefore: function(target, element) {
        function add(element) {
            if (element.constructor == String)  element = document.createTextNode(element);
            target.parentNode.insertBefore(element, target);
        }
        if (element.constructor == Array) { // works
            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 */
    pasteAfter: function(target, element) {
        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) { // works
            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 */
    pasteBegin: function(target, element) {
        function add(element) {
            if (element.constructor == String)  element = document.createTextNode(element);
            if (target.firstChild)  target.insertBefore(element, target.firstChild);
            else                    target.appendChild(element);
        }

        // TODO: does not work with an Array (?)
        if (element.constructor == Array) { // order fixed
            for (var i=element.length-1; i>=0; i--) { add(element[i]); }
        }
        else {
            add(element);
        }
    },

    /** insert text, element or elements at the end of a target */
    pasteEnd: function(target, element) {
        function add(element) {
            if (element.constructor == String)  element = document.createTextNode(element);
            target.appendChild(element);
        }

        if (element.constructor == Array) { // order correct
            for (var i=0; i<element.length; i++) { add(element[i]); }
        }
        else {
            add(element);
        }
    },

    //------------------------------------------------------------------------------
    //## css classes

    /** creates a RegExp matching a className */
    classNameRE: function(className) {
        return new RegExp("(^|\\s+)" + className.escapeRegexp() + "(\\s+|$)");
    },

    /** returns an Array of the classes of an element */
    getClasses: function(element) {
        return element.className.split(/\s+/);
    },

    /** returns whether an element has a class */
    hasClass: function(element, className) {
        if (!element.className) return false;
        var re  = this.classNameRE(className);
        return re.test(element.className);
    },

    /** adds a class to an element, maybe a second time */
    addClass: function(element, className) {
        if (this.hasClass(element, className))  return;
        var old = element.className ? element.className : "";
        element.className = (old + " " + className).trim();
    },

    /** removes a class to an element */
    removeClass: function(element, className) {
        var re  = this.classNameRE(className);
        var old = element.className ? element.className : "";
        element.className = old.replace(re, "");
    },

    /** replaces a class in an element with another*/
    replaceClass: function(element, oldClassName, newClassName) {
        this.removeClass(element, oldClassName);
        this.addClass(element, newClassName);
    },
    
    //------------------------------------------------------------------------------
    //## positioning

    /** mouse position in document coordinates */
    mousePos: function(event) {
        return {
            x: window.pageXOffset + event.clientX,
            y: window.pageYOffset + event.clientY
        };
    },

    /** document base coordinates for an object */
    parentPos: function(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;
        }
    },
};

//======================================================================
//## util/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     = {};
        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;
    },
};

//======================================================================
//## util/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/Wiki.js 

/** encoding and decoding of MediaWiki URLs */
Wiki = {
    /** the current wiki site without any path */
    site:       wgServer,                               // "http://de.wikipedia.org",

    /** language of the site */
    language:   wgContentLanguage,                      // "de"

    /** path to read pages */
    readPath:   wgArticlePath.replace(/\$1$/, ""),      // "/wiki/",

    /** path for page actions */
    actionPath: wgScriptPath + "/index.php",            // "/w/index.php",

    /** decoded Special namespace */
    specialNS:  null,

    /** decoded User namespace */
    userNS:     null,

    /** decoded User_talk namespace */
    userTalkNS: null,

    /** name of the logged in user */
    user:       wgUserName,

    /** whether user has news */
    haveNews: function() {
        return DOM.fetch('bodyContent', "div", "usermessage", 0) != null;
    },

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

    /** encode parameters into an URL */
    encodeURL: function(args, shorten) {
        
        if (!args.title)    throw "encode: 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 "decode: 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

    /** to be called onload */
    init: function() {
        var nss = this.namespaces[this.language];
        if (!nss)   throw "unconfigured language: " + language;
        this.specialNS  = nss.special;  // -1
        this.userNS     = nss.user;     // 2
        this.userTalkNS = nss.userTalk; // 3
    },

    /** 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,
        };
    },

    /** indexed by language */
    namespaces: {
        de: {
            special:    "Spezial",
            user:       "Benutzer",
            userTalk:   "Benutzer Diskussion",
        },
        en: {
            special:    "Special",
            user:       "User",
            userTalk:   "User talk",
        },
    },

    /** 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",
        "Filepath":             "file",
        // 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",   ],
        "Prefixindex":          [ "from"    ],
        "Blockip":              [ "ip"      ],
        "Log":                  [ "page"    ],
        "Filepath":             [ "file"    ],
    },
};

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

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

    /** search string of the current location decoded into an Array */
    params:     null,

    /** the namespace of the current page */
    namespace:  null,

    /** title for the current URL ignoring redirects */
    title:      null,

    /** permalink to the current page if one exists or null */
    perma:      null,

    /** whether this page could be deleted */
    deletable:  false,

    /** whether this page could be edited */
    editable:   false,

    /** the user a User or User_talk or Special:Contributions page belongs to */
    owner:      false,

    //------------------------------------------------------------------------------
    //## private

    /** to be called onload */
    init: function() {
        this.params = Wiki.decodeURL(location.href);

        var m   = /(^| )ns-(-?[0-9]+)( |$)/(document.body.className);
        if (m)  this.namespace  = parseInt(m[2]);
        // else error

        this.title      = this.params.title;

        this.deletable  = $('ca-delete') != null;
        this.editable   = $('ca-edit') != null;
        
        var a   = DOM.fetch('t-permalink', "a", null, 0);
        if (a != null) {
            this.perma  = a.href;
        }

        var self    = this;
        (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.page) {
                self.owner  = self.params.page.scan(Wiki.userNS + ":");
            }
            if (self.owner) return;

            // try block link
            if (!self.owner) {
                var a       = DOM.fetch('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/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);
        var args = {
            title:  title,
            action: "edit"
        };
        function change(v) { return {
            wpSummary:      summary,
            wpMinoredit:    minorEdit,
            wpTextbox1:     replaceFunc(
                                v.wpTextbox1.replace(/^[\r\n]+$/, "")),
        }; }
        this.action(args, 200, "editform", change, 200, doneFunc);
    },

    /** add text to the end of a spage, the separator is optional */
    appendText: function(title, text, summary, separator, doneFunc) {
        this.indicator.header("appending to page " + title);
        var args = {
            title:  title,
            action: "edit"
        };
        function change(v) { return {
            wpSummary:      summary,
            wpTextbox1:     concatSeparated(
                                v.wpTextbox1.replace(/^[\r\n]+$/, ""),
                                separator,
                                text),
        }; }
        this.action(args, 200, "editform", change, 200, doneFunc);
    },

    /** add text to the start of a page, the separator is optional */
    prependText: function(title, text, summary, separator, doneFunc) {
        this.indicator.header("prepending to page " + title);
        var args = {
            title:  title,
            action: "edit",
            section: 0
        };
        function change(v) { return {
            wpSummary:  summary,
            wpTextbox1: concatSeparated(
                            text,
                            separator,
                            v.wpTextbox1.replace(/^[\r\n]+$/, "")),
        }; }
        this.action(args, 200, "editform", change, 200, doneFunc);
    },

    /** restores a page to an older version */
    restoreVersion: function(title, oldid, summary, doneFunc) {
        this.indicator.header("restoring page " + title);
        var args = {
            title:  title,
            oldid:  oldid,
            action: "edit"
        };
        function change(v) { return {
            wpSummary: summary,
        }; }
        this.action(args, 200, "editform", change, 200, doneFunc);
    },

    //------------------------------------------------------------------------------
    //## change page state

    /** watch or unwatch a page. the doneFuncis optional */
    watchedPage: function(title, watch, doneFunc) {
        var action  = watch ? "watch" : "unwatch";
        this.indicator.header(action + "ing " + title);
        var url = Wiki.encodeURL({
            title:  title,
            action: action,
        });
        this.indicator.getting(url);
        var self    = this;
        function done(source) {
            if (source.status != 200) {
                self.indicator.failed(source, 200);
                return;
            }
            self.indicator.finished();
            if (doneFunc)   doneFunc(   );
        }
        Ajax.call({
            method:     "GET",
            url:        url,
            doneFunc:   done,
        });
    },

    /** move a page */
    movePage: function(oldTitle, newTitle, reason, withDiscussion, doneFunc) {
        this.indicator.header("moving " + oldTitle + " to " + newTitle);
        var args = {
            title:  Wiki.specialNS + ":Movepage",
            target: oldTitle,   // url-encoded, mandtory
        };
        function change(v) { return {
            wpOldTitle:     oldTitle,
            wpNewTitle:     newTitle,
            wpReason:       reason,
            wpMovetalk:     withDiscussion,
        }; }
        this.action(args, 200, "movepage", change, 200, doneFunc);
    },

    /** delete a page. if the reason is null, the original reason text is deleted */
    deletePage: function(title, reason, doneFunc) {
        this.indicator.header("deleting " + title);
        var args = {
            title:  title,
            action: "delete"
        };
        function change(v) { return {
            wpReason:   reason != null
                        ? concatSeparated(reason,  " - ", v.wpReason)
                        : "",
        }; }
        this.action(args, 200, "deleteconfirm", change, 200, doneFunc);
    },

    /**
     * change a page's protection state
     * allowed values for the levels are "", "autoconfirmed" and "sysop"
     */
    protectPage: function(title, levelEdit, levelMove, reason, doneFunc) {
        this.indicator.header("protecting " + title);
        var args    = {
            title:  title,
            action: "protect"
        };
        function change(v) { return {
            "mwProtect-level-edit": levelEdit,
            "mwProtect-level-move": levelMove,
            "mwProtect-reason":     reason,
        }; }
        // this form does not have a name
        this.action(args, 200, 0, change, 200, doneFunc);
    },

    //------------------------------------------------------------------------------
    //## change other data

    /** block a user, anonOnly defaults to false, createAccounts defaults to true */
    blockUser: function(user, duration, reason, anonOnly, createAccount, doneFunc) {
        this.indicator.header("blocking " + user);
        var args = {
            title:  Wiki.specialNS + ":Blockip",
            ip:     user,   // url-encoded, optional
        };
        function change(v) { return {
            wpBlockAddress:     user,
            wpBlockReason:      reason,
            wpAnonOnly:         anonOnly,
            wpCreateAccount:    createAccount,
            wpBlockOther:       duration,
        }; }
        this.action(args, 200, "blockip", change,  200, doneFunc);
    },

    //------------------------------------------------------------------------------
    //## private

    /**
     * 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, changeFunc, 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    = self.changedForm(form, changeFunc);
            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 XHTML document*/
    findForm: function(doc, nameOrIdOrIndex) {
        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;
    },

    /** uses a changeFunc to create Ajax arguments from modified form contents */
    changedForm: function(form, changeFunc) {
        var original    = {};
        for (var i=0; i<form.elements.length; i++) {
            var element = form.elements[i];
            var check   = element.type == "radio" || element.type == "checkbox";
            original[element.name]  = check ? element.checked : element.value;
            // select has no value, but (possibly multiple) Options
            // the type can be "select-one" or "select-multiple"
            // with select-one selectedIndex is usable, else option.selected 
        }
        var changes = changeFunc(original);
        var out     = {};
        for (var i=0; i<form.elements.length; i++) {
            var element = form.elements[i];
            var changed = element.name in changes;
            var value   = changed ? changes[element.name] : original[element.name];
            if (element.type == "submit" || element.type == "button") {
                if (changed)    out[element.name]   = changes[element.name].toString();
            }
            else if (element.type == "radio" || element.type == "checkbox") {
                if (value)      out[element.name]   = "1";
            }
            else if (element.type != "file") {  // hidden select password text textarea
                out[element.name]   = value.toString();
            }
        }
        return out;
    },

    //------------------------------------------------------------------------------
    //## 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(); }
    },
};

//======================================================================
//## core/Markup.js 

/** WikiText constants */
Markup = {
    // own creations
    dash:       "--",   // "—" em dash U+2014 &#8212;
    sigapp:     " -- ~\~\~\~\n",

    // enclosing
    template_:  "\{\{",
    _template:  "\}\}",
    link_:      "\[\[",
    _link:      "\]\]",
    _link_:     "\|",
    h2_:        "==",
    _h2:        "==",

    // simple
    sig:        "~\~\~\~",
    line:       "----",

    // control chars
    star:       "*",
    hash:       "#",
    colon:      ":",
    semi:       ";",
    sp:         " ",
    lf:         "\n",
};

//======================================================================
//## 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/PopupMenu.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 PopupMenu(source, selectFunc, abortFunc) {
    this.source     = source;
    this.selectFunc = selectFunc;
    this.abortFunc  = abortFunc;
    this.init();
}
PopupMenu.prototype = {
    /** adds an item, its userdata will be supplied to the selectFunc */
    item: function(label, userdata) {
        var a   = document.createElement("a");
        a.className     = "link-function";
        a.textContent   = label;

        var item    = document.createElement("div");
        item.className  = "popup-menu-item";
        item.userdata   = userdata;

        item.appendChild(a);
        this.menu.appendChild(item);
        // could return the item...
    },

    /** adds a separator */
    separator: function() {
        var separator   = document.createElement("hr");
        separator.className = "popup-menu-separator";
        this.menu.appendChild(separator);
    },

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

    /** setup */
    init: function() {
        this.menu       = document.createElement("div");
        this.menu.className = "popup-menu";
        
        // initially hidden
        this.visible    = true;
        this.closeMenu();
        DOM.addClass(this.source, "popup-source");
        
        document.body.appendChild(this.menu);
        //### does not work, is displayed behind other elements
        //this.source.appendChild(this.menu);

        // install mouse listeners
        var self    = this;
        this.source.oncontextmenu = function(ev) {
            self.openMenu(ev);
            return false;
        }
        this.menu.onmouseup = function(ev) {
            // TODO: ensure the window is not closed when 
            // the button was held down for a really short time
            self.maybeSelectItem(ev);
            return false;
        }
        document.addEventListener("mouseup", function(ev) {
            self.maybeAbortSelection(ev);
            return false;
        }, false);
    },

    /** show the popup at mouse position */
    openMenu: function(ev) {
        
        // with display:none we'd havwe no koordinates, 
        // and the menu still has visibiliy:hidden
        this.menu.style.display = "block";
        
        var mouse       = DOM.mousePos(ev);
        var container   = DOM.parentPos(this.menu);
        var max = {
            x: window.scrollX + window.innerWidth,
            y: window.scrollY + window.innerHeight
        };
        var size = {
            x: this.menu.offsetWidth,
            y: this.menu.offsetHeight
        };
        // HACK, why does the menu go too far to the right without this?
        size.x  += 16;
        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;
        this.menu.style.left    = (mouse.x - container.x) + "px";
        this.menu.style.top     = (mouse.y - container.y) + "px";
        
        // make menu visible
        this.menu.style.visibility  = "visible";
        this.visible                = true;
    },
    
    /** hide the popup */
    closeMenu: function() {
        this.menu.style.display     = "none";
        this.menu.style.visibility  = "hidden";
        this.visible                = false;
    },
    
    /** if a selection happened, tell the client and close */
    maybeSelectItem: function(ev) {
        var target  = ev.target;
        for (;;) {
            if (DOM.hasClass(target, "popup-menu-item")) {
                this.closeMenu();
                if (this.selectFunc)    this.selectFunc(target.userdata, this.menu, this.source);
                return;
            }
            target  = target.parentNode;
            if (!target)    return;
        }
    },
    
    /** if no selection happened, tell the client and close */
    maybeAbortSelection: function(ev) {
        if (!this.visible)  return;
        this.closeMenu();
        if (this.abortFunc) this.abortFunc(this.menu, this.source);
    }
};

//======================================================================
//## ui/SwitchBoard.js 

/** contains a number of on/off-switches */
function SwitchBoard() {
    this.knobs  = [];
    this.board  = document.createElement("span");
    this.board.className    = "switch-board";
    
    // public
    this.component  = this.board;
}
SwitchBoard.prototype = {
    /** add a knob and set its className */
    add: function(knob) {
        DOM.addClass(knob, "switch-knob");
        DOM.addClass(knob, "switch-off");
        this.knobs.push(knob);
        this.board.appendChild(knob);
    },
    /** selects a single knob */
    select: function(knob) {
        this.changeAll(false);
        this.change(knob, true);
    },
    /** changes selection state of one knob */
    change: function(knob, selected) {
        if (selected)   DOM.replaceClass(knob, "switch-off", "switch-on");
        else            DOM.replaceClass(knob, "switch-on", "switch-off");
    },
    /** changes selection state of all knobs */
    changeAll: function(selected) {
        for (var i=0; i<this.knobs.length; i++) {
            this.change(this.knobs[i], selected);
        }
    },
};

//======================================================================
//## ui/Links.js 

/** creates links */
Links = {
    /**
     * 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;

        // optional parameter
        if (!popupFunc) popupFunc   = func;
        var popup       = new PopupMenu(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);
            }
        }

        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     = "link-function";
        a.onclick       = func;
        a.textContent   = label;
        return a;
    },

    /** create a link to a readURL */
    readLink: function(label, title,  args) {
        return this.urlLink(label, Wiki.readURL(title, args));
    },

    /** create a link to an actionURL */
    pageLink: function(label, args) {
        return this.urlLink(label, Wiki.encodeURL(args));
    },

    /** 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);
        DOM.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();
        DOM.removeChildren(this.headerDiv);
        DOM.pasteEnd(this.headerDiv, content);
    },

    /** display a body text */
    body: function(content) {
        this.unfade();
        DOM.removeChildren(this.bodyDiv);
        DOM.pasteEnd(this.bodyDiv, content);
    },

    /** destructor, called by fade */
    destroy: function() {
        DOM.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(id, title, rows, withoutPBody) {
    this.outer  = document.createElement("div");
    this.outer.id       = id;
    this.outer.className    = "portlet";

    if (withoutPBody) {
        this.body   = this.outer;
    }
    else {
        this.header = document.createElement("h5");
        this.header.textContent = title;

        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;
    this.canLabel   = {};
    this.render(rows);

    // public
    this.component  = this.outer;
}
Portlet.prototype = {
    /** change labels of action links */
    labelStolen: function(labels) {
         for (var id in labels) {
             var target = this.canLabel[id];
             if (target)     target.textContent = labels[id];
         }
    },

    render: function(rows) {
        if (rows.constructor == Array) {
            // add rows
            this.ul = document.createElement("ul");
            this.body.appendChild(this.ul);
            this.renderRows(rows);
        }
        else {
            // add singlerow
            this.body.appendChild(rows);
        }
    },

    renderRows: function(rows) {
        for (var y=0; y<rows.length; y++) {
            var row = rows[y];
            if (row === null)   continue;
            if (row.constructor == String) {
                // steal row
                var element = $(row);
                if (element) {
                    var clone   = element.cloneNode(true);
                    this.ul.appendChild(clone);
                    this.canLabel[element.id] = clone.firstChild;
                }
            }
            else if (row.constructor == Array) {
                // add cells
                this.li = document.createElement("li");
                this.ul.appendChild(this.li);
                this.renderCells(row);
            }
            else {
                // singlecell
                this.li = document.createElement("li");
                this.ul.appendChild(this.li);
                this.li.appendChild(row);
            }
        }
    },

    renderCells: function(row) {
        var first   = true;
        for (var x=0; x<row.length; x++) {
            var cell    = row[x];
            if (cell === null)  continue;

            // insert separator
            if (!first) this.li.appendChild(document.createTextNode(" "));
            else        first   = false;

            if (cell.constructor == String) {
                // steal singlerow as cell
                var element = $(cell);
                // problem: interferes with relabelling later!
                if (element) {
                    var clone   = element.firstChild.cloneNode(true);
                    this.li.appendChild(clone);
                    this.canLabel[element.id] = clone;
                }
            }
            else {
                // add link
                this.li.appendChild(cell);
            }
        }
    },
};

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

/** encapsulates column-one */
SideBar = {
    /**
     * change labels of action links
     * root is a common parent of all items, f.e. document
     * labels is a Map from id to label
     */
    labelItems: function(labels) {
         for (var id in labels) {
             var el = document.getElementById(id);
             if (!el)   continue;
             var a  = el.getElementsByTagName("a")[0];
             if (!a)    continue;
              a.textContent = labels[id];
         }
    },

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

    /** the portlets remembered in createPortlet and sidplayed in showPortlets */
    preparedPortlets: [],

    /**
     * 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
     * withoutPBody is optional
     */
    createPortlet: function(id, title, rows, withoutPBody) {
        var portlet = new Portlet(id, title, rows, withoutPBody);
        this.preparedPortlets.push(portlet);
        return portlet;
    },

    /** display the portlets created before and remove older ones with the same id */
    showPortlets: function() {
        var columnOne   = $('column-one');
        for (var i=0; i<this.preparedPortlets.length; i++) {
            var portlet     = this.preparedPortlets[i];
            var replaces    = $(portlet.component.id);
            if (replaces)   DOM.removeNode(replaces);
            columnOne.appendChild(portlet.component);
        }
        // HACK for speedup, hidden in sideBar.css
        columnOne.style.visibility  = "visible";
    },
};

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

/** helper for action=history */
ActionHistory = {
    /** onload initializer */
    init: function() {
        if (Page.params["action"] != "history") return;
        this.addLinks();
    },

    //------------------------------------------------------------------------------
    //## private

    /** additional links for every version in a page history */
    addLinks: function() {
        function addLink(li) {
            var diffInput   = DOM.fetch(li, "input", null, 1);
            if (!diffInput) return;

            // gather data
            var histSpan    = DOM.fetch(li, "span", "history-user", 0);
            var histA       = DOM.fetch(histSpan, "a", null, 0);
            var dateA       = DOM.nextElement(diffInput, "a");
            var oldid       = diffInput.value;
            var user        = histA.textContent;
            var date        = dateA.textContent;

            var msg = ActionHistory.msg;

            // add restore version link
            function done() { window.location.reload(true); }
            var summary = msg.restored + " " + user + " " + date;
            var restore = FastRestore.linkFastRestoreVersion(Page.title, oldid, summary, done);
            var before  = diffInput.nextSibling;
            DOM.pasteBefore(before, [ " [", restore, "] "]);

            // add edit link
            var edit    = Links.pageLink(msg.edit, {
                title:  Page.title,
                oldid:  oldid,
                action: "edit",
            });
            var before  = diffInput.nextSibling;
            DOM.pasteBefore(before, [ " [", edit, "] "]);

            // add block link
            var block   = Links.readLink(msg.block, Wiki.specialNS + ":Blockip/" + user);
            DOM.pasteBefore(histSpan, [ " [", block, "] "]);
        }

        var lis = DOM.fetch('pagehistory', "li");
        if (!lis)   return;
        for (var i=0; i<lis.length; i++) {
            addLink(lis[i]);
        }
    },
};
ActionHistory.msg = {
    edit:       "edit",
    restored:   "zurück auf ",
    block:      "blocken",
};

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

/** revert in the background for action=diff */
ActionDiff = {
    /** onload initializer */
    init: function() {
        if (!Page.params["diff"])   return;     //if (Page.params["action"] != "history")
        this.fastRevert();
        this.addLinks();
    },

    //------------------------------------------------------------------------------
    //## private

    /** extend rollback links */
    fastRevert: function() {
        function done(link) {
            link.textContent        = ActionDiff.msg.reverted;
            window.location.href    = Wiki.encodeURL({
                title:  Page.title,
                action: "history",
            });
        }
        var td  = DOM.fetch(document, "td", "diff-ntitle", 0);
        if (td === null)    return;
        var as  = DOM.fetch(td, "a");
        for (var i=0; i<as.length; i++) {
            var a   = as[i];
            var params  = Wiki.decodeURL(a.href);
            if (params.action != "rollback")    continue;
            Background.immediatize(a, done);
        }
    },

    /** add restore-links */
    addLinks: function() {
        var msg = ActionDiff.msg;

        /** extends one of the two sides */
        function extend(tdClassName) {
            // get cell
            var td  = DOM.fetch(document, "td", tdClassName, 0);
            if (!td)    return;

            // extract data
            var as  = DOM.fetch(td, "a");
            if (as.length < 3)  return;
            var a0      = as[0];
            var a1      = as[1];
            var a2      = as[2];

            // get oldid
            var params  = Wiki.decodeURL(a0.href);
            if (!params.oldid)  return;
            var oldid   = params.oldid;

            // get version date
            //### hardcoded RegExp!
            var versionRE   = /Version vom (.*)/;
            var dateP       = versionRE(a0.textContent);
            var date        = dateP  ? dateP[1]  : null;

            // get version user
            var user    = a2.textContent;   // not a1!

            // add restore version link
            function done() {
                window.location.href    = Wiki.encodeURL({
                    title:  Page.title,
                    action: "history",
                });
            }
            var summary = msg.restored + " " + user + " " + date;
            var restore = FastRestore.linkFastRestoreVersion(Page.title, oldid, summary, done);
            DOM.pasteBefore(a1, [ restore, " | "]);
        }
        extend("diff-ntitle");
        extend("diff-otitle");
    },
};
ActionDiff.msg = {
    reverted:   "zurückgesetzt",
    restored:   "zurück auf ",
};

//======================================================================
//## extend/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 && feature.init) {
            feature.init();
        }

        var elements    = this.autoSubmitElements[name];
        if (elements) {
            this.autoSubmit(document.forms[0], elements);
        }
    },

    /** adds an onchange handler to elements in a form submitting the form and removes the submit button. */
    autoSubmit: function(form, elementNames, leaveSubmitAlone) {
        if (!form)  return;
        // if there is only one form, it's the searchform
        if (document.forms.length < 2)  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;
        }

        // would make sense in case of multiple submit buttons!
        // if (leaveSubmitAlone)    return;
        var todo    = [];
        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++) {
            DOM.removeNode(todo[i]);
        }
    },

    /** maps Specialpage names to the autosubmitting form elements */
    autoSubmitElements: {
        Allpages:       [ "namespace", "nsfrom"     ],
        Contributions:  [ "namespace"               ],
        Ipblocklist:    [ "title"                   ],
        Linksearch:     [ "title"                   ],
        Listusers:      [ "group", "username"       ],
        Log:            [ "type", "user", "page"    ],
        Newimages:      [ "wpIlMatch"               ],
        Newpages:       [ "namespace", "username"   ],
        Prefixindex:    [ "namespace", "nsfrom"     ],
        Recentchanges:  [ "namespace", "invert"     ],
        Watchlist:      [ "namespace"               ],
    },
};

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

/** extends Special:Blockip */
SpecialBlockip = {
    /** onload initializer */
    init: function() {
        this.presetBlockip();
    },

    //------------------------------------------------------------------------------
    //## private

    /** fill in default values into the blockip form */
    presetBlockip: function() {
        var form    = document.forms["blockip"];
        if (!form)  return; // action=success

        form.elements["wpBlockExpiry"].value    = "other";
        form.elements["wpBlockOther"].value     = "1 hour";
        form.elements["wpBlockReason"].value    = SpecialBlockip.msg.standardReason;
        form.elements["wpBlockReason"].select();
        form.elements["wpBlockReason"].focus();
    },
};
SpecialBlockip.msg = {
    standardReason: "vandalismus",
};

//======================================================================
//## extend/SpecialContributions.js 

/** revert in the background for Special:Contributions */
SpecialContributions = {
    /** onload initializer */
    init: function() {
        doLater(this, this.fastRevert, 150);
    },

    //------------------------------------------------------------------------------
    //## private

    /** extend rollback links */
    fastRevert: function() {
        var ul  = DOM.fetch('bodyContent', "ul", null, 0);
        if (ul === null)    return;
        var all = [];

        function done(link) { link.textContent = SpecialContributions.msg.reverted; }
        var as  = DOM.fetch(ul, "a");
        for (var i=0; i<as.length; i++) {
            var a   = as[i];
            var params  = Wiki.decodeURL(a.href);
            if (params.action != "rollback")    continue;
            Background.immediatize(a, done);
            all.push(a);
        }
    },
};
SpecialContributions.msg = {
    reverted:   "zurückgesetzt",
};

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

/** extends Special:Newpages */
SpecialNewpages = {
    /** onload initializer */
    init: function() {
        this.displayInline();
    },

    //------------------------------------------------------------------------------
    //## private

    /** 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";

            // a FoldButton for 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;
            DOM.pasteBegin(header, foldButton.button);

            // add action links
            
            DOM.pasteBegin(header, UserBookmarks.linkMark(title));
            DOM.pasteBegin(header, Template.bankAllPage(title));
            DOM.pasteBegin(header, FastDelete.linkDeletePopup(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, { 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  = DOM.fetch('bodyContent', "ol", null, 0);
        if (!ol)    return;
        ol.className    = "specialNewPages";

        // find article list items
        var lis = DOM.fetch(ol, "li");
        for (var i=0; i<lis.length; i++) {
            if (i >= this.MAX_ARTICLES) break;
            extendItem(lis[i]);
        }
    },
};
SpecialNewpages.msg = {
    loading:    "lade seite..",
};

//======================================================================
//## extend/SpecialSpecialpages.js 

/** extends Special:Specialpages */
SpecialSpecialpages = {
    /** onload initializer */
    init: function() {
        this.extendLinks();
    },

    //------------------------------------------------------------------------------
    //## private

    /** make a sorted tables from the links */
    extendLinks: function() {
        var uls = DOM.fetch('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     = DOM.fetch(ul, "li", null);
        var lines   = [];
        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);
        }
        DOM.pasteBefore(ul, table);
        DOM.removeNode(ul);
    },
};

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

/** extends Special:Undelete */
SpecialUndelete = {
    /** onload initializer */
    init: function() {
        this.toggleAll();
    },

    //------------------------------------------------------------------------------
    //## private

    /** add an invert button for all checkboxes */
    toggleAll: function() {
        var form    = document.forms[0];
        if (!form)  return;

        var button  = document.createElement("input");
        button.type     = "button";
        button.value    = SpecialUndelete.msg.invert;
        button.onclick = function() {
            var els = form.elements;
            for (var i=0; i<els.length; i++) {
                var el  = els[i];
                if (el.type == "checkbox")
                    el.checked  = !el.checked;
            }
        }

        var target  = DOM.fetch(form, "ul", null, 2);
        // no list if there is only one deleted version
        if (target === null)    return;
        target.parentNode.insertBefore(button, target);
    },
};
SpecialUndelete.msg = {
    invert: "Invertieren",
};

//======================================================================
//## extend/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 {
            doLater(SpecialRecentchanges, SpecialRecentchanges.filterLinks, 150);
        }
    },

    //------------------------------------------------------------------------------
    //## 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    = DOM.fetch(document, "form", null, 0);
        var uls     = DOM.fetch(form, "ul");
        for (var i=0; i<uls.length; i++) {
            var ul  = uls[i];
            var h2  = DOM.previousElement(ul);
            if (h2) ns  = h2.textContent;
            wiki    += "== " + ns + " ==\n";
            var lis = DOM.fetch(ul, "li");
            for (var j=0; j<lis.length; j++) {
                var li  = lis[j];
                var as  = DOM.fetch(li, "a");
                var a   = as[0];
                var title   = a.title;
                var exists  = a.className != "new"; // TODO: use hasClass
                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  = DOM.nextElement($('jump-to-nav'), "form");
        DOM.pasteBefore(target, [
            "export as ",   wikiLink,   " (Markup) ",
            "or as ",       csvLink,    " (CSV).",
        ]);
    },

    /** extends header structure and add toggle buttons for all checkboxes */
    toggleLinks: function() {
        var form    = DOM.fetch(document, "form", null, 0)

        // be folding-friendly: add a header for the article namespace
        var ul          = DOM.fetch(form, "ul", null, 0);
        var articleHdr  = document.createElement("h2");
        articleHdr.textContent  = SpecialWatchlist.msg.article;
        DOM.pasteBefore(ul, articleHdr);

        // add invert buttons for single namespaces
        var uls     = DOM.fetch(form, "ul");
        for (var i=0; i<uls.length; i++) {
            var ul      = uls[i];
            var button  = this.toggleButton(ul);
            var target  = DOM.previousElement(ul, "h2");
            DOM.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"];
        DOM.pasteBefore(target, globalHdr);

        // add a gobal invert button
        var button  = this.toggleButton(form);
        DOM.pasteAfter(globalHdr.lastChild, [ ' ', button ]);
    },

    /** creates a toggle button for all input children of an element */
    toggleButton: function(container) {
        return Links.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",
};

//======================================================================
//## extend/SpecialRecentchanges.js 

/** extensions for Special:Recentchanges */
SpecialRecentchanges = {
    /** onload initializer */
    init: function() {
        // HACK: uses exactly the same format
        doLater(this, this.filterLinks, 150);
    },

    //------------------------------------------------------------------------------
    //## private

    /** change the watchlist to make it filterable */
    filterLinks: function() {
        var bodyContent = $('bodyContent');

        // tag list items with is-ip or is-named
        var uls = DOM.fetch(bodyContent, "ul", "special");
        for (var i=0; i<uls.length; i++) {
            var ul  = uls[i];
            var lis = DOM.fetch(ul, "li");
            for (var j=0; j<lis.length; j++) {
                var li  = lis[j];
                var a   = DOM.fetch(li, "a", null, 3);
                if (isIP(a.textContent))    DOM.addClass(li, "is-ip");
                else                        DOM.addClass(li, "is-named");
            }
        }

        // filtering is done with CSS
        function all() {
            board.select(linkAll);
            DOM.removeClass(bodyContent, "hide-ip");
            DOM.removeClass(bodyContent, "hide-named");
        }
        function ips() {
            board.select(linkIPs);
            DOM.removeClass(bodyContent, "hide-ip");
            DOM.addClass(   bodyContent, "hide-named");
        }
        function names() {
            board.select(linkNames);
            DOM.addClass(   bodyContent, "hide-ip");
            DOM.removeClass(bodyContent, "hide-named");
        }
        
        var linkAll     = Links.functionLink(SpecialRecentchanges.msg.all,      all);
        var linkIPs     = Links.functionLink(SpecialRecentchanges.msg.ips,      ips);
        var linkNames   = Links.functionLink(SpecialRecentchanges.msg.names,    names);
        
        var board   = new SwitchBoard();
        board.add(linkAll);
        board.add(linkIPs);
        board.add(linkNames);
        board.select(linkAll);

        var target  = DOM.nextElement($('jump-to-nav'), "h4");
        if (!target)    return;
        
        DOM.pasteBefore(target, [
            document.createElement("br"),   //### HACK
            SpecialRecentchanges.msg.intro,
            board.component
        ]);
    },
};
SpecialRecentchanges.msg = {
    intro:      "Filter: ",
    all:        "Alle Edits",
    ips:        "nur von Ips",
    names:      "nur von Angemeldeten",
};

//======================================================================
//## extend/SpecialPrefixindex.js 

/** extends Special:Prefixindex */
SpecialPrefixindex = {
    /** onload initializer */
    init: function() {
        this.sortItems();
    },

    //------------------------------------------------------------------------------
    //## private

    /** sort items into a straight list */
    sortItems: function() {
        var table   = DOM.fetch('bodyContent', "table", null, 2);
        if (!table) return; // no results
        var tds     = DOM.fetch(table, "td");
        var ol      = document.createElement("ol");
        for (var i=0; i<tds.length; i++) {
            var td  = tds[i];
            var li  = document.createElement("li");
            var c   = td.firstChild.cloneNode(true)
            li.appendChild(c);
            ol.appendChild(li);
        }
        table.parentNode.replaceChild(ol, table);
    },
};

//======================================================================
//## feature/ForSite.js 

/** links for the whole siwe */
ForSite = {
    /** a link to new pages */
    linkNewpages: function() {
        return Links.pageLink(ForSite.msg.newpages, {
            title:  Wiki.specialNS + ":Newpages",
            limit:  20,
        });
    },

    /** a link to new pages */
    linkNewusers: function() {
        return Links.pageLink(ForSite.msg.newusers, {
            title:  Wiki.specialNS + ":Log",
            type:   "newusers",
            limit:  20,
        });
    },

    /** a link to speedy delete candidates */
    bankProjectPages: function() {
        var pages   = ForSite.cfg.projectPages[Wiki.site];
        if (!pages) return null;
        var out = [];
        for (var i=0; i<pages.length; i++) {
            var page    = pages[i];
            var link    =  Links.readLink(page[0], page[1]);
            out.push(link);
        }
        return out;
    },

    /** return a link for fast logfiles access */
    linkAllLogsPopup: function() {
        function selected(userdata) {
            window.location.href    = Wiki.readURL(Wiki.specialNS + ":Log" + "/" + userdata.toLowerCase());
        }
        return this.linkAllPopup(
            ForSite.msg.logLabel, 
            Wiki.specialNS + ":Log",
            ForSite.cfg.logs,
            selected);
    },

    /** return a link for fast logfiles access */
    linkAllSpecialsPopup: function() {
        function selected(userdata) {
            window.location.href    = Wiki.readURL(Wiki.specialNS + ":" + userdata);
        }
        return this.linkAllPopup(
            ForSite.msg.specialLabel, 
            Wiki.specialNS + ":Specialpages",
            ForSite.cfg.specials,
            selected);
    },
    
    //------------------------------------------------------------------------------
    //## private
    
    /** returns a linkPopup */
    linkAllPopup: function(linkLabel, mainPage, pages, selectFunc) {
        var mainLink    = Links.readLink(linkLabel, mainPage);
        var popup       = new PopupMenu(mainLink, selectFunc);
        for (var i=0; i<pages.length; i++) {
            var page    = pages[i];
            popup.item(page, page); // the page is the userdata
        }
        return mainLink;
    },
}
ForSite.cfg = {
    /** maps sites to an Array of interresting pages */
    projectPages: {
        "http://de.wikipedia.org": [
            [   "WW",   "Wikipedia:Wiederherstellungswünsche"   ],
            [   "EW",   "Wikipedia:Entsperrwünsche"             ],
            [   "VS",   "Wikipedia:Vandalensperrung"            ],
            [   "LK",   "Wikipedia:Löschkandidaten"             ],
            [   "SL",   "Kategorie:Wikipedia:Schnelllöschen"    ],
            
        ],
        "http://de.wikiversity.org": [
            [   "Löschen",  "Kategorie:Wikiversity:Löschen" ],
        ],
    },

    /** which logs are displayed in the opoup */
    logs: [
        "Move", "Newusers", "Block", "Protect", "Delete", "Upload"
        
    ],

    /** which specialpages are displayed in the opoup */
    specials:[
        "Allmessages", "Allpages", "BrokenRedirects", "Ipblocklist", "Linksearch", "Listusers", "Newimages", "Prefixindex",
        
    ],
};
ForSite.msg = {
    logLabel:       "Logs",
    specialLabel:   "Spezial",

    newpages:       "Neuartikel",
    newusers:       "Newbies",
};

//======================================================================
//## feature/ForPage.js 

/** links for arbitrary pages */
ForPage = {
    /** returns a link to the logs for a given page */
    linkLogAbout: function(title) {
        return Links.pageLink(ForPage.msg.pageLog,  {
            title:  Wiki.specialNS + ":Log",
            page:   title
        });
    },
};
ForPage.msg = {
    pageLog:    "Seitenlog",
};

//======================================================================
//## feature/ForUser.js 

/** links for users */
ForUser = {
    /** returns a link to the homepage of a user */
    linkHome: function(user) {
        return Links.readLink(ForUser.msg.home, Wiki.userNS  + ":" + user);
    },

    /** returns a link to the talkpage of a user */
    linkTalk: function(user) {
        return Links.readLink(ForUser.msg.talk, Wiki.userTalkNS  + ":" + user);
    },

    /** returns a link to new messages or null when none exist */
    linkNews: function(user) {
        return Links.readLink(ForUser.msg.news, Wiki.userTalkNS + ":" + user, { diff: "cur" });
    },

    /** returns a link to a users contributions */
    linkContribs: function(user) {
        return Links.readLink(ForUser.msg.contribs, Wiki.specialNS  + ":Contributions/" + user);
    },


    /** returns a link to the blockpage for a user */
    linkBlock: function(user) {
        return Links.readLink(ForUser.msg.block,    Wiki.specialNS  + ":Blockip/" + user);
    },

    /** returns a link to a users emailpage */
    linkEmail: function(user) {
        // does not make sense with ipOwner or without realOwner
        return Links.readLink(ForUser.msg.email,        Wiki.specialNS  + ":Emailuser/" + Page.owner)
    },

    /** returns a link to a users log entries */
    linkLogsAbout: function(user) {
        return Links.pageLink(ForUser.msg.logsAbout, {
            title:  Wiki.specialNS + ":Log",
            page:   Wiki.userNS + ":" + user
        });
    },

    /** returns a link to a users log entries */
    linkLogsActor: function(user) {
        return Links.pageLink(ForUser.msg.logsActor, {
            title:  Wiki.specialNS + ":Log",
            user:   user
        });
    },

    /** returns a link to show subpages of a user */
    linkSubpages: function(user) {
        return Links.pageLink(ForUser.msg.subpages, {
            title:      Wiki.specialNS + ":Prefixindex",
            namespace:  2,  // User
            from:       user + "/",
        });
    },

    /** whois check */
    linkWhois: function(user) {
        // do not make sense without ipOwner!
        return Links.urlLink(ForUser.msg.whois,
                "http://www.iks-jena.de/cgi-bin/whois?submit=Suchen&charset=iso-8859-1&search=" + user);
        //return "http://www.ripe.net/fcgi-bin/whois?form_type=simple&full_query_string=&&do_search=Search&searchtext=" + ip;
    },

    /** senderbase check */
    linkSenderbase: function(user) {
        // do not make sense without ipOwner!
        return Links.urlLink(ForUser.msg.senderbase,
                "http://www.senderbase.org/search?searchString=" + user);
    },
};
ForUser.msg = {
    home:       "Benutzer",
    talk:       "Diskussion",
    email:      "Anmailen",
    contribs:   "Beiträge",
    block:      "Blocken",

    news:       "☏",
    logsAbout:  "Logs",
    logsActor:  "Logs",
    subpages:   "Sub",

    whois:      "Whois",
    senderbase: "More",
};

//======================================================================
//## feature/FastWatch.js 

/** page watch and unwatch without reloading the page */
FastWatch = {
    init: function() {
        /** initialize link */
        function initView() {
            var watch   = $('ca-watch');
            var unwatch = $('ca-unwatch');
            if (watch)      exchangeItem(watch,     true);
            if (unwatch)    exchangeItem(unwatch,   false);
        }

        /** talk to the server, then updates the UI */
        function changeState(watched) {
            function update() {
                var watch   = $('ca-watch');
                var unwatch = $('ca-unwatch');
                if ( watched && watch  )    exchangeItem(watch,     false);
                if (!watched && unwatch)    exchangeItem(unwatch,   true);
            }
            var editor  = new Editor(); // new ProgressArea()
            editor.watchedPage(Page.title, watched, update);
        }

        /** 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 ? FastWatch.msg.watch   : FastWatch.msg.unwatch;
            var a       = Links.functionLink(label, function() {
                DOM.addClass(a, "link-running");
                changeState(watchable);
            });
            DOM.addClass(a, "link-immediate");
            li.appendChild(a);
            target.parentNode.replaceChild(li, target);
        }

        initView();
    },
};
FastWatch.msg = {
    watch:      "Beobachten",
    unwatch:    "Vergessen",
};

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

/** one-click delete */
FastDelete = {
    /** returns a link which prompts or popups reasons and then deletes */
    linkDeletePopup: function(title) {
        var self    = this;
        var msg     = FastDelete.msg;
        var cfg     = FastDelete.cfg;
        return Links.promptPopupLink(msg.label, msg.prompt, cfg.reasons, function(reason) {
            self.fastDelete(title, reason);
        });
    },

    
    /** delete an article with a reason */
    fastDelete: function(title, reason) {
        var editor  = new Editor(new ProgressArea());
        editor.deletePage(title, reason);
    },
};
FastDelete.cfg = {
    reasons:    null;
};
FastDelete.msg = {
    prompt: "Warum löschen?",
    label:  "löschen",
};

//======================================================================
//## feature/FastRestore.js 

/** page restore mechanisms */
FastRestore = {
    /** returns a link restoring a given version */
    linkFastRestoreVersion: function(title, oldid, summary, doneFunc) {
        var restore = Links.functionLink(FastRestore.msg.restore, function() {
            var editor  = new Editor();
            DOM.addClass(restore, "link-running");
            editor.restoreVersion(title, oldid, summary, function() {
                DOM.removeClass(restore, "link-running");
                doneFunc();
            });
        });
        DOM.addClass(restore, "link-immediate");
        return restore;
    },
};
FastRestore.msg = {
    restore:    "restore",
};

//======================================================================
//## feature/Background.js 

/** page restore mechanisms */
Background = {
    /** make a link act in the background, the doneFunc is called wooth the link */
    immediatize: function(link, doneFunc) {
        DOM.addClass(link, "link-immediate");
        link.onclick    = this.immediateOnclick;
        link._doneFunc  = doneFunc;
    },

    /** onclick handler function for immediateLink */
    immediateOnclick: function() {
        var link    = this; // (!)
        DOM.addClass(link, "link-running");
        Ajax.call({
            url:        link.href,
            doneFunc:   function(source) {
                DOM.removeClass(link, "link-running");
                if (link._doneFunc) link._doneFunc(link);
            }
        });
        return false;
    },
};

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

/** puts templates into the current page */
Template = {
    //------------------------------------------------------------------------------
    //## templates for user talk pages

    /** return an Array of links for userTalkPages or null if none exist */
    bankTalks: function(user) {
        var official    = this.talksArray(user, Template.cfg.officialTalks, false, false);
        var personal    = this.talksArray(user, Template.cfg.personalTalks, true, true);
        var all         = official.concat(personal);
        return all.length > 0 ? all : null;
    },

    /** returns an Array of links to "talk" to a user in different templates */
    talksArray: function(user, templateNames, ownTemplate, dashSig) {
        var out = [];
        for (var i=0; i<templateNames.length; i++) {
            out.push(this.linkTalkTo(user, templateNames[i], ownTemplate, dashSig));
        }
        return out;
    },

    /** creates a link to "talk" to a user */
    linkTalkTo: function(user, templateName, ownTemplate, dashSig) {
        var self    = this;
        // this is simple currying!
        function handler() { self.talkTo(user, templateName, ownTemplate, dashSig); }
        var link    = Links.functionLink(templateName, handler);
        DOM.addClass(link, "link-immediate");
        return link;
    },

    /** puts a signed talk-template into a user's talkpage */
    talkTo: function(user, templateName, ownTemplate, dashSig) {
        var r       = Markup;
        var title   = Wiki.userTalkNS + ":" + user;
        var text    =  r.template_ + "subst:";
        if (ownTemplate)
            text    += Wiki.userNS + ":" + Wiki.user + "/";
        text        += templateName + r._template + r.sp;
        if (dashSig)
            text    += r.dash + r.sp;
        text        += r.sig + r.lf;
        var sepa    = r.line + r.lf;
        var editor  = new Editor(new ProgressArea());
        editor.appendText(title, text, templateName, sepa, this.maybeReloadFunc(title));
    },

    //------------------------------------------------------------------------------
    //## termplates for arbitrary pages

    /** return an Array of links to actions for normal pages */
    bankAllPage: function(title) {
        var msg     = Template.msg;
        var self    = this;
        return [
            Links.promptLink(msg.qs.label,  msg.qs.prompt,  function(reason) { self.qs(title, reason);  }),
            Links.promptLink(msg.la.label,  msg.la.prompt,  function(reason) { self.la(title, reason);  }),
            Links.promptLink(msg.sla.label, msg.sla.prompt, function(reason) { self.sla(title, reason); }),
        ];
    },

    /** puts an SLA template into an article */
    sla: function(title, reason) {
        this.simple(title, "löschen", reason);
    },

    /** puts an QS template into an article */
    qs: function(title, reason) {
        this.enlist(title, "subst:QS", "Wikipedia:Qualitätssicherung", reason);
    },

    /** puts an LA template into an article */
    la: function(title, reason) {
        this.enlist(title, "subst:Löschantrag", "Wikipedia:Löschkandidaten", reason);
    },

    /** puts a simple template into an article */
    simple: function(title, template, reason) {
        var r       = Markup;
        var summary = r.template_ + template + r._template + r.sp + reason;
        var text    = summary + r.sigapp;
        var sepa    = r.line + r.lf;
        var editor  = new Editor(new ProgressArea());
        editor.prependText(title,  text, summary, sepa, this.maybeReloadFunc(title));
    },

    /** list page on a list page */
    enlist: function(title, template, listPage, reason) {
        var r           = Markup;
        var self        = this;
        var progress    = new ProgressArea();
        // insert template
        function phase1() {
            var summary = r.template_ + template + r._template + r.sp + reason;
            var text    = summary + r.sigapp;
            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 + "/" + self.currentDate();
            var text    = r.h2_ + r.link_ + title + r._link + r._h2 + r.lf + reason + r.sigapp;
            var summary = r.link_ + title + r._link + r.sp + r.dash + r.sp + reason;
            var sepa    = r.lf;
            var editor  = new Editor(progress);
            editor.appendText(page, text, summary, sepa, self.maybeReloadFunc(title));
        }
        phase1();
    },

    //------------------------------------------------------------------------------
    //## helper

    /** creates a function to reload the current page, if it has the given title */
    maybeReloadFunc: function(title) {
        return function() {
            if (Page.title == title) {
                window.location.href    = Wiki.readURL(title);
            }
        }
    },

    /** returns the current date in the format the LKs are organized */
    currentDate: function() {
        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.cfg = {
    officialTalks:  [ "Hallo", "Test" ],
    personalTalks:  [], // below User:Name/
};
Template.msg = {
    qs: {
        label:  "QS",
        prompt: "QS - Begründung?",
    },
    la: {
        label:  "LA",
        prompt: "LA - Begründung?",
    },
    sla: {
        label:  "SLA",
        prompt: "SLA - Begründung?",
    },
};

//======================================================================
//## 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 */
    bankView: function(lemma) {
        return [ this.linkView(), this.linkMark(lemma) ];
    },

    /** return the absolute page link */
    linkView: function() {
        return Links.readLink(UserBookmarks.msg.view, this.pageTitle());
    },

    /** add a bookmark on a user's bookmark page. if the page is left out, the current is added */
    linkMark: function(lemma) {
        var self    = this;
        var msg     = UserBookmarks.msg;
        var cfg     = UserBookmarks.cfg;
        return Links.promptPopupLink(msg.add, msg.prompt, cfg.reasons, function(reason) {
            if (lemma)  self.arbitrary(reason, lemma);
            else        self.current(reason);
        });
    },

    //------------------------------------------------------------------------------
    //## private

    /** 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    = Markup.star + Markup.link_ + ":" + lemma + Markup._link;
        if (perma)  text    += " <small>[" + perma + " " + mode + "]</small>";
        if (remark) text    += " " + remark;
        text        += Markup.lf;
        this.prepend(text);
    },

    /** add text to the bookmarks page */
    prepend: function(text) {
        new Editor(new ProgressArea())
                .prependText(this.pageTitle(), text, "");
    },

    /** the title of the current user's bookmarks page */
    pageTitle: function() {
        return Wiki.userNS + ":" + Wiki.user + "/" + UserBookmarks.cfg.pageTitle;
    }
};
UserBookmarks.cfg = {
    pageTitle:  "bookmarks",
    reasons:    null,
};
UserBookmarks.msg = {
    view:       "Bookmarks",
    add:        "Merken",
    prompt:     "Bookmark - Kommentar?",
};

//======================================================================
//## portlet/Search.js 

/** #p-search */
Search = {
    /** remove the go button, i want to search */
    init: function() {
        var node    = document.forms['searchform'].elements['go'];
        DOM.removeNode(node);
    },
};

//======================================================================
//## portlet/Lang.js 

/** #p-lang */
Lang = {
    id: 'p-lang',

    /** insert a select box to replace the pLang portlet */
    init: function() {
        var pLang   = $(this.id);
        if (!pLang) return;

        var select  = document.createElement("select");
        select.id   = "langSelect";
        select.options[0]   = new Option(Lang.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;
        }

        SideBar.createPortlet(this.id, Lang.msg.title, select);
    },
};
Lang.msg = {
    title:  "Languages",

    select: "auswählen",
};

//======================================================================
//## portlet/Cactions.js 

/** #p-cactions */
Cactions = {
    id: "p-cactions",

    init: function() {
        this.unfix();

        SideBar.labelItems(Cactions.msg.labels);

        if (Page.namespace >= 0) {
            this.addTab('ca-logs',
                    ForPage.linkLogAbout(Page.title));
        }
        
    },

    /** move p-cactions out of column-one so it does not inherit its position:fixed */
    unfix: function() {
        var pCactions       = $(this.id);
        var columnContent   = $('column-content');  // belongs to the SideBar somehow..
        pCactions.parentNode.removeChild(pCactions);
        columnContent.insertBefore(pCactions, columnContent.firstChild);
    },

    /** adds a tab */
    addTab: function(id, content) {
        // ta[id] = ['g', 'Show logs for this page'];
        var li = document.createElement("li");
        li.id   = id;
        li.appendChild(content);
        var tabs    = DOM.fetch(this.id, "ul", null, 0);
        tabs.appendChild(li);
    },
};
Cactions.msg = {
    labels: {
        'ca-talk':          "Diskussion",
        'ca-edit':          "Bearbeiten",
        'ca-viewsource':    "Source",
        'ca-history':       "History",
        'ca-protect':       "Schützen",
        'ca-unprotect':     "Freigeben",
        'ca-delete':        "Löschen",
        'ca-undelete':      "Entlöschen",
        'ca-move':          "Verschieben",
        
    },
};

//======================================================================
//## portlet/Tools.js 

/** # p-tb */
Tools = {
    id: 'p-tb',

    init: function() {
        var tools1  = null;
        var tools2  = null;
        if (Page.editable) {
            tools1  = [];
            if (Page.deletable)
                tools1.push(FastDelete.linkDeletePopup(Page.title));
            tools2  = Template.bankAllPage(Page.title);
        }
        SideBar.createPortlet(this.id, Tools.msg.title, [
            tools1,
            tools2,
            UserBookmarks.bankView(),
        ]);
    },
};
Tools.msg = {
    title:  "Tools",
};

//======================================================================
//## portlet/Navigation.js 

/** #p-navigation */
Navigation = {
    id: 'p-navigation',

    init: function() {
        SideBar.createPortlet(this.id, Navigation.msg.title, [
            [   'n-recentchanges',
                'pt-watchlist',
            ],
            [   ForSite.linkNewusers(),
                ForSite.linkNewpages(),
            ],
            ForSite.bankProjectPages(),
            [   ForSite.linkAllSpecialsPopup(),
                ForSite.linkAllLogsPopup(),
            ],
            // 't-specialpages',
            // 't-permalink',
            [   't-recentchangeslinked',
                't-whatlinkshere',
            ],
        ]).labelStolen(Navigation.msg.labels);
    },
};
Navigation.msg = {
    title:  "Navigation",

    labels: {
        'n-recentchanges':          "Changes",
        'pt-watchlist':             "Watchlist",
        't-whatlinkshere':          "Hierher",
        't-recentchangeslinked':    "Umgebung",
    },
};

//======================================================================
//## portlet/Personal.js 

/** #p-personal */
Personal = {
    // cannot use p-personal which has way too much styling
    id: 'p-personal2',

    init: function() {
        SideBar.createPortlet(this.id, Personal.msg.title, [
            [   'pt-userpage',
                'pt-mytalk',
                ( Wiki.haveNews() ? ForUser.linkNews(Wiki.user) : null )
            ],
            [   ForUser.linkSubpages(Wiki.user),
                ForUser.linkLogsActor(Wiki.user),
                'pt-mycontris',
            ],
            [   'pt-preferences',
                'pt-logout'
            ],
        ]).labelStolen(Personal.msg.labels);
    },

};
Personal.msg = {
    title:      "Persönlich",

    labels: {
        'pt-mytalk':    "Diskussion",
        'pt-mycontris': "Beiträge",
    },
};

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

/** #p-communication: communication with Page.owner */
Communication = {
    id: 'p-communication',

    init: function() {
        if (!Page.owner)                return;
        if (Page.owner == Wiki.user)    return;
        if (!(this.hasRealOwner()
        || this.isLogForOwner()))       return;

        var ipOwner = isIP(Page.owner);

        
        SideBar.createPortlet(this.id, Communication.msg.title, [
            Template.bankTalks(Page.owner),
            [   ForUser.linkHome(Page.owner),
                ForUser.linkTalk(Page.owner),
            ],
            [   ForUser.linkSubpages(Page.owner),
                ForUser.linkLogsAbout(Page.owner),
                ForUser.linkContribs(Page.owner),
            ],
            !ipOwner ? null :
            [   ForUser.linkWhois(Page.owner),
                ForUser.linkSenderbase(Page.owner),
            ],
            ipOwner ? null :
            [   ForUser.linkEmail(Page.owner)
            ],
            [   ForUser.linkBlock(Page.owner),
            ],
        ]);
    },

    /** whether this page's owner really exists */
    hasRealOwner: function() {
        // only existing users have contributions and they have more links in Special:Contributions
        return (Page.namespace == 2 || Page.namespace == 3) &&  $('t-contributions') != null
            || Page.whichSpecial() == "Contributions" && DOM.fetch('contentSub', "a").length > 2;   // 2 or 4
    },

    /** if this page is a log for the owner */
    isLogForOwner: function() {
        return Page.whichSpecial() == "Blockip" && Page.params.ip
            || Page.whichSpecial() == "Log"     && Page.params.type == "block";
    },
};
Communication.msg = {
    title:  "Kommunikation",
};

//======================================================================
//## main.js 

/** onload hook */
function initialize() {
    // init features
    Wiki.init();
    Page.init();
    FastWatch.init();
    ActionHistory.init();
    ActionDiff.init();
    Special.init();

    // build new portlets
    Cactions.init();
    Search.init();
    Tools.init();
    Navigation.init();
    Communication.init();
    Personal.init();
    Lang.init();

    // display portlets created before
    SideBar.showPortlets();
}

doOnLoad(initialize);

/* </nowiki></pre> */