Benutzer:Notebook/monobook.js

/* <pre><nowiki> */
//======================================================================
//## core/prototypes.js 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

/** ajax functions for MediaWiki */
function Editor(progress) {
    if (progress)   this.indicator  = this.progressIndicator(progress);
    else            this.indicator  = this.quietIndicator;
}
Editor.prototype = {
    //------------------------------------------------------------------------------
    //## simple actions without form
    
    /** watch or unwatch a page. the doneFunc gets the new state and is optional */
    watchedPage: function(title, watch, doneFunc) {
        var self    = this;
        var action  = watch ? "watch" : "unwatch";
        self.indicator.header(action + "ing " + title);
        var url = Page.actionURL({
            title:  title,
            action: action,
        });
        self.indicator.getting(url);
        Ajax.call({
            method:     "GET",
            url:        url,
            doneState:  function(source) {
                if (source.status != 200) {
                    self.indicator.failed(source, 200);
                    return;
                }
                if (doneFunc)   doneFunc(watch);
                self.indicator.finished();
            },
        });
    },
    
    //------------------------------------------------------------------------------
    //## complex actions with form
    
    /** add text to the start of a page, the separator is optional */
    prependText: function(title, text, summary, separator, doneFunc) {
        this.indicator.header("prepending to " + title);
        this.action(
            // section=0 does _not_ insert the summary as a headline
            { title: title, action: "edit", section: 0 },
            200,
            "editform",
            function(f) { 
                var oldText = f.wpTextbox1.value.replace(/^[\r\n]+$/, "");
                var newText = concatSeparated(text, separator, oldText);
                return {
                    wpSection:      f.wpSection.value,
                    wpStarttime:    f.wpStarttime.value,
                    wpEdittime:     f.wpEdittime.value,
                    wpScrolltop:    f.wpScrolltop.value,
                    wpSummary:      summary,
                    wpWatchthis:    f.wpWatchthis.checked ? "1" : null,
                    wpMinoredit:    f.wpMinoredit.checked ? "1" : null,
                    wpSave:         f.wpSave.value,
                    wpEditToken:    f.wpEditToken.value,
                    wpTextbox1:     newText,
                };
            },
            200,
            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 " + title);
        this.action(
            // section=new inserts the summary as a headline
            { title: title, action: "edit" },
            200,
            "editform",
            function(f) {
                var oldText = f.wpTextbox1.value.replace(/^[\r\n]+$/, "");
                var newText = concatSeparated(oldText, separator, text);
                return {
                    wpSection:      f.wpSection.value,
                    wpStarttime:    f.wpStarttime.value,
                    wpEdittime:     f.wpEdittime.value,
                    wpScrolltop:    f.wpScrolltop.value,
                    wpSummary:      summary,
                    wpWatchthis:    f.wpWatchthis.checked ? "1" : null,
                    wpMinoredit:    f.wpMinoredit.checked ? "1" : null,
                    wpSave:         f.wpSave.value,
                    wpEditToken:    f.wpEditToken.value,
                    wpTextbox1:     newText,
                };
            },
            200,
            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);
        this.action(
            { title: title, action: "delete" },
            200,
            "deleteconfirm",
            function(f) {
                var separator   = " - ";    //### hardcoded
                var rs  = reason != null
                        ? concatSeparated(reason, separator, f.wpReason.value)
                        : "";
                return {
                    wpReason:       rs,
                    wpConfirmB:     f.wpConfirmB.value,
                    wpEditToken:    f.wpEditToken.value,
                };
            },
            200,
            doneFunc
        );
    },
    
    /** 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 title   = Page.specialNS + ":Blockip";
        this.action(
            { title: title, target: user },
            200,
            "blockip",
            function(f) { return {
                wpBlockAddress:     user,
                wpBlockReason:      reason,
                wpAnonOnly:         anonOnly        ? "1" : null,
                wpCreateAccount:    createAccount   ? "1" : null,
                wpBlockExpiry:      f.wpBlockExpiry.value,
                wpBlockOther:       duration,
                wpEditToken:        f.wpEditToken.value,
                wpBlock:            f.wpBlock.value,
            }},
            200,
            doneFunc
        );
    },
    
    /** move a page */
    movePage: function(oldTitle, newTitle, reason, withDiscussion, doneFunc) {
        this.indicator.header("moving " + oldTitle + " to " + newTitle);
        var title   = Page.specialNS + ":Movepage";
        this.action(
            { title: title, target: oldTitle },
            200,
            "movepage",
            function(f) { return {
                wpOldTitle:     oldTitle,
                wpNewTitle:     newTitle,
                wpReason:       reason,
                wpMovetalk:     withDiscussion ? "1" : null,
                wpEditToken:    f.wpEditToken.value,
                wpMove:         f.wpMove.value,
            }},
            200,
            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);
        this.action(
            { title: title, action: "protect" },
            200,
            0,  // this form does not have a name. arrgh.
            function(f) {
                return {
                    // nice names. second arrgh.
                    "mwProtect-level-edit": levelEdit,
                    "mwProtect-level-move": levelMove,
                    "mwProtect-reason":     reason, // f["mwProtect-reason"].value, 
                    wpEditToken:            f.wpEditToken.value,
                };
            },
            200,
            doneFunc
        );
    },
    
    //------------------------------------------------------------------------------
    //## action helper
    
    /** 
     * get a form, change it, post it.
     * makeData gets form.elements to create a Map
     * the doneFunc is called afterwards and may be left out
     */
    action: function(actionArgs, expectedGetStatus, 
            formName, makeData, expectedPostStatus,
            doneFunc) {
        function phase1() {
            var url = Page.actionURL(actionArgs);
            self.indicator.getting(url);
            Ajax.call({
                method:     "GET",
                url:        url,
                doneState:  phase2,
            });
        }
        function phase2(source) {
            if (expectedGetStatus && source.status != expectedGetStatus) {
                self.indicator.failed(source, expectedGetStatus);
                return;
            }   
            
            var doc     = Ajax.parseXML(source.responseText);
            var form    = self.findForm(doc, formName);
            if (form == null) { self.indicator.missingForm(source, formName); return; }
            
            var url     = form.action;
            var data    = makeData(form.elements);
            var headers = Ajax.urlEncoded("UTF-8"); // Ajax.multipartFormData(boundary, "UTF-8")
            var body    = Ajax.encodeArgs(data);    // Ajax.encodeFormData(boundary, data)
            
            // bug #246651 - is this still open?
            //headers["Connection"] = "close";
                                                
            //var   boundary    = "134j5hkvgnarw4t82raflfjl3aklsjdfhsdlkhflkqe";
            //var   headers = Ajax.multipartFormData(boundary, "UTF-8")
            //var   body    = Ajax.encodeFormData(boundary, data)
            
            self.indicator.posting(url);
            Ajax.call({
                method:     "POST",
                url:        url,
                headers:    headers,    
                body:       body,
                doneState:  phase3,
            });
        }
        function phase3(source) {
            if (expectedPostStatus && source.status != expectedPostStatus) {
                self.indicator.failed(source, expectedPostStatus);
                return;
            }
            self.indicator.finished();
            if (doneFunc)   doneFunc();
        }
        var self    = this;
        phase1();
    },
    
    
    /** finds a HTMLForm within an XMLDocument (!) */
    findForm: function(doc, nameOrIdOrIndex) {
        // firefox does _not_ provide document.forms,
        // but within the form we get HTMLInputElements (!)
        var forms   = doc.getElementsByTagName("form");
        if (typeof nameOrIdOrIndex == "number") {
            if (nameOrIdOrIndex >= 0 
            && nameOrIdOrIndex < forms.length)  return forms[nameOrIdOrIndex];
            else                                return null;
        }
        
        for (var i=0; i<forms.length; i++) {
            var form    = forms[i];
            if (this.elementNameOrId(form) == nameOrIdOrIndex)  return form;
        }
        return null;
    },
    
    /** finds the name or id of an element */
    elementNameOrId: function(element) {
        return  element.name    ? element.name
            :   element.id      ? element.id
            :   null;
    },

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

//======================================================================
//## FastBlock.js 

/** one-click user block, the hard way */
FastBlock = {
    /** returns a link which prompts a reason and then kills a user */
    doUserKill: function(user) {
        var self    = this;
        var msg     = FastBlock.msg;
        return Action.promptLink(msg.label, msg.prompt, function(reason) {
            self.killUser(user, reason);
        });
    },
    
    /** does everythin necessary to block a user indefinitely */
    killUser: function(user, reason) {
        this.blockIndefinite(user, reason);
        this.modifyPage(Page.userNS     + ":" + user, reason);
        this.modifyPage(Page.userTalkNS + ":" + user, reason);
    },
    
    /** blocks a user for an indefinite duration */
    blockIndefinite: function(user, reason) {
        var duration    = "indefinite";
        var editor      = new Editor(new ProgressArea());
        editor.blockUser(user, duration, reason, false, true);
    },
        
    /** inserts the template in a page and protects it */
    modifyPage: function(title, reason) {
        var self    = this;
        var msg     = FastBlock.msg;
        var editor  = new Editor(new ProgressArea());
        var text    = msg.template + " " + reason + msg.sigapp;
        editor.prependText(title, text, msg.template, msg.separator, function() {
            var level   = "sysop";
            var editor  = new Editor(new ProgressArea());
            editor.protectPage(title, level, level, msg.template);
        });
    },
};
FastBlock.msg = {
    label:          "Killen",
    prompt:         "warum killen?",
    template:       "{{gesperrter Benutzer}}",
    sigapp:         " -- ~~" + "~~\n",
    separator:      "\n----\n",
    //standardReason:   "vandalismus",
};

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    /** add an invert button for all checkboxes */
    toggleAll: function() {
        // damn, this form no longer has a name 
        // var form = document.forms["undelete"];
        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  = descendants('undelete', "ul", null, 1);
        var target  = descendants(form, "ul", null, 2);
        if (target == null) return; // no list if there is only one deleted version
        target.parentNode.insertBefore(button, target);
    },
};
SpecialUndelete.msg = {
    invert: "Invertieren",
};

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

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

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

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

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

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

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

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

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

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

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

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

    // cannot use p-personal which has too much styling
    SideBar.newPortlet('portlet-personal').setTitle("Persönlich").build([
        'pt-userpage',
        'pt-mytalk',
        'pt-mycontris',
        [   Action.urlLink("Adminlog", URLs.userAllLogs(Page.user)) ],  //TODO: wrap up
        'pt-preferences',
        'pt-logout',
        // 'pt-watchlist'
    ]).show();

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

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

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

Content Disclaimer

Informasi ini disarikan dari Wikipedia dan disajikan kembali untuk tujuan edukasi. Konten tersedia di bawah lisensi CC BY-SA 3.0. Kami tidak bertanggung jawab atas ketidakakuratan data yang bersumber dari kontribusi publik tersebut.

  1. The information displayed on this website is sourced in part or in whole from Wikipedia and has been adapted for the purpose of restating it. We strive to provide accurate and relevant information, however:
  2. There is no guarantee of absolute accuracy. Wikipedia is an open, collaborative project that can be edited by anyone, so information is subject to change.
  3. It is not intended to constitute professional advice. The content displayed is for informational and educational purposes only. For important decisions (e.g., medical, legal, or financial), please consult a professional.
  4. Content copyright. Wikipedia is licensed under the Creative Commons Attribution-ShareAlike License (CC BY-SA). This means that content may be reused with appropriate attribution and shared under a similar license.
  5. Responsible use. Any risk arising from the use of information from this website is entirely the responsibility of the user.