_ _    _ _____  ___   __                       
 __      _(_) | _(_)___ / ( _ ) / /_   ___ ___  _ __ ___  
 \ \ /\ / / | |/ / | |_ \ / _ \| '_ \ / __/ _ \| '_ ` _ \ 
  \ V  V /| |   <| |___) | (_) | (_) | (_| (_) | | | | | |
   \_/\_/ |_|_|\_\_|____/ \___/ \___(_)___\___/|_| |_| |_|

Benutzer:D/monobook/api.js

Das Thema Benutzer:D/monobook/api.js ist seit langem Gegenstand von Interesse und Debatten. Von seinen Anfängen bis heute hat Benutzer:D/monobook/api.js in verschiedenen Bereichen der Gesellschaft eine bedeutende Rolle gespielt. Um dieses Thema besser zu verstehen, ist es wichtig, sich mit seiner Geschichte, seinen Implikationen und seinen Auswirkungen in verschiedenen Kontexten zu befassen. In diesem Artikel werden verschiedene Perspektiven auf Benutzer:D/monobook/api.js angesprochen, mit dem Ziel, eine umfassende Vision zu bieten, die es den Lesern ermöglicht, ein umfassenderes und bereicherndes Verständnis dieses Themas zu erlangen.
/* ] ::: API ::: ] */

/* <pre><nowiki> */

//======================================================================
//## util/lang.js 

//------------------------------------------------------------------------------
//## Object

// NOTE: these do _not_ break for (foo in bar)

/** Object helper functions */
var Objects = {
    /** create an Object from a prototype */
    object: function(obj) {
        function F() {}
        F.prototype = obj;
        return new F();
    },
    
    /** copies an Object's properties into an new Object */
    copyOf: function(obj) {
        var out = {};
        for (var key in obj)
                if (obj.hasOwnProperty(key))    
                        out = obj;
        return out;
    },
    
    /** copies an object's properties into another object */
    copySlots: function(source, target) {
        for (var key in source)
                if (source.hasOwnProperty(key)) 
                        target = source;
    },
    
    /** an object's own property names as an Array */
    ownSlots: function(obj) {
        var out = ;
        for (key in obj)
                if (obj.hasOwnProperty(key))
                        out.push(obj);
    },
    
    /** returns an object's slots as an Array of Pairs */
    toPairs: function(obj) {
        var out = ;
        for (var key in obj)
                if (obj.hasOwnProperty(key))
                        out.push(]);
        return out;
    },
    
    /** creates an Object from an Array of key/value pairs, the last Pair for a key wins */
    fromPairs: function(pairs) {
        var out = {};
        for (var i=0; i<pairs.length; i++) {
            var pair    = pairs;
            out]    = pair;
        }
    }//,
};

//------------------------------------------------------------------------------
//## Array 

// NOTE: these _do_ break for each (foo in someArray)

/** can be used to copy a function's arguments into a real Array */
Array.make = function(args) {
    return Array.prototype.slice.apply(args);
};

/** removes an element */
Array.prototype.remove = function(element) {
    var index   = this.indexOf(element);
    if (index === -1)   return false;
    this.splice(index, 1);
    return true;
};

/** whether this array contains an element */
Array.prototype.contains = function(element) {
    return this.indexOf(element) !== -1;
};


/** flatten an Array of Arrays into a simple Array */
Array.prototype.flatten = function() {
    var out = ;
    this.forEach(function(element) { 
        out = out.concat(element); 
    });
    return out;
};


/** map every element to an Array and concat the resulting Arrays */
Array.prototype.flatMap = function(func, thisVal) {
    var out = ;
    this.forEach(function(element) {
        out = out.concat(func.call(thisVal, element));
    });
    return out;
};

/** return a new Array with a separator inserted between every element of the Array */
Array.prototype.infuse = function(separator) {
    var out = ;
    for (var i=0; i<this.length; i++) {
        out.push(this);
        out.push(separator);
    }
    out.pop();
    return out;
};

/** use a function to extract keys and build an Object */
Array.prototype.indexWith = function(keyFunc) {
    var out = {};
    for (var i=0; i<this.length; i++) {
        var item    = this;
        out  = item;
    }
    return out;
};

//------------------------------------------------------------------------------
//## Function

/** the unary identiy function */
Function.identity = function(x) { return x; }

/** create a constant Function */
Function.constant = function(c) { return function(v) { return c; } }

/** create a Function calling this Function with a fixed this */
/*Function.prototype.bind = function(thisObject) {
    var self    = this; // == arguments.callee
    return function() {
        return self.apply(thisObject, arguments);
    };
};*/

/** create a Function calling this Function with fixed first arguments */
Function.prototype.fix = function() {
    var self    = this; // == arguments.callee
    var args    = Array.prototype.slice.apply(arguments);
    return function() {
        return self.apply(this, args.concat(Array.prototype.slice.apply(arguments)));
    };
};

//------------------------------------------------------------------------------
//## String

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

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

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

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

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

/** escapes characters to make them usable as a literal in a RegExp */
String.prototype.escapeRE = function() {
    return this.replace(/(\\])/g, "\\$1");
};

/** parse a JSON String */
String.prototype.parseJSON = function() {
    var text    = this.replace(//g, function(a) { return '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); });
    if (!/^,:{}\s]*$/.test(text.replace(/\\(?:|u{4})/g, '@').replace(/"*"|true|false|null|-?\d+(?:\.\d*)?(?:?\d+)?/g, ']').replace(/(?:^|:|,)(?:\s*\[)+/g, '')))
            throw "invalid JSON string";
    return eval("(" + text + ")");
};

/** replace ${name} with the name property of the args object */
String.prototype.template = function(args) {
    return this.template2("${", "}", args);
};

/** replace prefix XXX suffix with the name property of the args object */
String.prototype.template2 = function(prefix, suffix, args) {
    // /\$\{(+?)\}/g
    var re  = new RegExp(prefix.escapeRE() + "(+?)" + suffix.escapeRE(), "g");
    return this.replace(re, function($0, $1) { 
        var arg = args; 
        return arg !== undefined ? arg : $0;
    });
};

//------------------------------------------------------------------------------
//## Number

/** create an array of number from inclusive to exclusive */
Number.range = function(from, to) {
    var out = ;
    for (var i=from; i<to; i++) out.push(i);
    return out;
};

//======================================================================
//## util/TextUtil.js 

/** text utilities */
var TextUtil = {
    /** 
     * gets an Array of search/replace-pairs (two Strings) and returns 
     * a function taking a String and replacing every search-String with
     * the corresponding replace-string
     */
    recoder: function(pairs) {
        var search  = ;
        var replace = {};
        for (var i=0; i<pairs.length; i++) {
            var pair    = pairs;
            search.push(pair.escapeRE());
            replace] = pair; 
        }
        var regexp  = new RegExp(search.join("|"), "gm");
        return function(s) { 
                return s.replace(regexp, function(dollar0) {  
                        return replace; }); };
    },
    
    /** concatenate all non-empty values in an array with a separator */
    joinPrintable: function(separator, values) {
        var filtered    = ;
        for (var i=0; i<values.length; i++) {
            var value   = values;
            if (value === null || value === "") continue; 
            filtered.push(value);
        }
        return filtered.join(separator ? separator : "");
    },
    
    /** make a function returning its argument */
    idFunc: function() {
        return function(s) {
            return s;
        };
    },
    
    /** make a function returning a constant value */
    constFunc: function(s) {
        return function() {
            return s;
        };
    },
    
    /** make a function returning its argument */
    replaceFunc: function(search, replace) {
        return function(s) {
            return s.replace(search, replace);
        };
    },
    
    /** make a function adding a given prefix */
    prefixFunc: function(separator, prefix) {
        return function(suffix) { 
            return TextUtil.joinPrintable(separator, ); 
        };
    },
    
    /** make a function adding a given suffix */
    suffixFunc: function(separator, suffix) {
        return function(prefix) { 
            return TextUtil.joinPrintable(separator, );
        };
    }//,
};

//======================================================================
//## util/XMLUtil.js 

/** XML utility functions */
var XMLUtil = {
    //------------------------------------------------------------------------------
    //## DOM
    
    /** parses a String into an XMLDocument */
    parseXML: function(text) {
        var doc     = new DOMParser().parseFromString(text, "text/xml");
        var root    = doc.documentElement;
        // root.namespaceURI === "http://www.mozilla.org/newlayout/xml/parsererror.xml"
        if (root.tagName === "parserError"  // ff 2
        || root.tagName === "parsererror")  // ff 3
                throw "XML parser error: " + root.textContent;
        return doc;
    },
    
    /** serialize an XML (e4x) or XMLDocument to a String */
    unparseXML: function(xml) {
        return new XMLSerializer().serializeToString(xml);
    },
    
    //------------------------------------------------------------------------------
    //## E4X
    
    /** parses a String into an e4x XML object */
    parseE4X: function(text) {
        
        return new XML(text.replace(/^<\?xml*>/, ""));
    },
    
    /** serialize an XML (e4x) to a String */
    unparseE4X: function(xml) {
        return xml.toXMLString();
    },
    
    //------------------------------------------------------------------------------
    //## escaping
    
    /** escapes XML metacharacters */
    encode: function(str) { 
        return str.replace(/&/g,    '&amp;')
                    .replace(/</g,  '&lt;')
                    .replace(/>/g,  '&gt;');
    },
    
    /** escapes XML metacharacters including double quotes */
    encodeDQ: function(str) {
        return str.replace(/&/g,    '&amp;')
                    .replace(/</g,  '&lt;')
                    .replace(/>/g,  '&gt;')
                    .replace(/\"/g, '&quot;');
    },
    
    /** escapes XML metacharacters including single quotes */
    encodeSQ: function(str) {
        return str.replace(/&/g,    '&amp;')
                    .replace(/</g,  '&lt;')
                    .replace(/>/g,  '&gt;')
                    .replace(/\'/g, '&apos;');
    },
    
    /** decodes results of encode, encodeDQ and encodeSQ */
    decode: function(code) {
        return code.replace(/&quot/g,   '"')
                    .replace(/&apos/g,  "'")
                    .replace(/&gt;/g,   ">")
                    .replace(/&lt;/g,   "<")
                    .replace(/&amp;/g,  "&");
    }//,
};

//======================================================================
//## util/Loc.js 

/**
 * tries to behave similar to a Location object
 * protocol includes everything before the //
 * host     is the plain hostname
 * port     is a number or null
 * pathname includes the first slash or is null
 * hash     includes the leading # or is null
 * search   includes the leading ? or is null
 */
function Loc(urlStr) {
    var m   = this.parser(urlStr);
    if (!m) throw "cannot parse URL: " + urlStr;
    this.local      = !m;
    this.protocol   = m ? m : null;                           // http:
    this.host       = m ? m : null;                           // de.wikipedia.org
    this.port       = m ? parseInt(m.substring(1)) : null;    // 80
    this.pathname   = m ? m : "";                             // https://wiki386.com/de/Test
    this.hash       = m ? m : "";                             // #Industry
    this.search     = m ? m : "";                             // ?action=edit
}
Loc.prototype = {
    /** matches a global or local URL */
    parser: /((.+?)\/\/(+)(:+)?)?(+)?(#*)?(\?.*)?/,

    /** returns the href which is the only usable string representationn of an URL */
    toString: function() {
        return this.hostPart() + this.pathPart();
    },

    /** returns everything before the pathPart */
    hostPart: function() {
        if (this.local) return "";
        return this.protocol + "//" + this.host
            + (this.port ? ":" + this.port  : "");
    },

    /**  returns everything local to the server */
    pathPart: function() {
        return this.pathname + this.hash + this.search;
    },

    /** converts the searchstring into an Array of name/value-pairs */
    args: function() {
        if (!this.search)   return ;
        var out     = ;
        var split   = this.search.substring(1).split("&");
        for (var i=0; i<split.length; i++) {
            var parts   = split.split("=");
            out.push([
                decodeURIComponent(parts), 
                decodeURIComponent(parts)
            ]);
        }
        return out;
    },
    
    /** converts the searchString into a hash. */
    argsMap: function() {
        return Objects.fromPairs(this.args());
        
    }//,
};

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

/** DOM helper functions */
var DOM = {
    //------------------------------------------------------------------------------
    //## events

    /** executes a function when the DOM is loaded */
    onLoad: function(func) {
        window.addEventListener("DOMContentLoaded", func, false);
    },

    //------------------------------------------------------------------------------
    //## find
    
    /** find an element in document by its id */
    get: function(id) {
        return document.getElementById(id);
    },

    /**
      * find descendants of an ancestor by tagName, className and index 
      * tagName, className and index are optional
      * returns a single element when index exists or an Array of elements if not
      */
    fetch: function(ancestor, tagName, className, index) {
        if (ancestor && ancestor.constructor === String) {
            ancestor    = document.getElementById(ancestor);
        }
        if (ancestor === null)  return null;
        var elements    = ancestor.getElementsByTagName(tagName ? tagName : "*");
        if (className) {
            var tmp = ;
            for (var i=0; i<elements.length; i++) {
                if (this.hasClass(elements, className)) {
                    tmp.push(elements);
                }
            }
            elements    = tmp;
        }
        if (typeof index === "undefined")   return elements;
        if (index >= elements.length)       return null;
        return elements;
    },

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

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

    /** whether an ancestor contains an element */
    contains: function(ancestor, element) {
        for (;;) {
            if (element === ancestor)   return true;    
            if (element === null)       return false;
            element = element.parentNode;
        }
    },
    
    //------------------------------------------------------------------------------
    //## add

    /** inserts text, a node or an Array of these before a target node */
    pasteBefore: function(target, additum) {
        if (additum.constructor !== Array)  additum = ;
        var parent  = target.parentNode;
        for (var i=0; i<additum.length; i++) {
            var node    = additum;
            if (node.constructor === String)    node    = document.createTextNode(node);
            parent.insertBefore(node, target);
        }
    },

    /** inserts text, a node or an Array of these after a target node */
    pasteAfter: function(target, additum) {
        if (target.nextSibling) this.pasteBefore(target.nextSibling, additum);
        else                    this.pasteEnd(target.parentNode, additum);
    },

    /** insert text, a node or an Array of these at the start of a target node */
    pasteBegin: function(parent, additum) {
        if (parent.firstChild)  this.pasteBefore(parent.firstChild, additum);
        else                    this.pasteEnd(parent, additum);
    },

    /** insert text, a node or an Array of these at the end of a target node */
    pasteEnd: function(parent, additum) {
        if (additum.constructor !== Array)  additum = ;
        for (var i=0; i<additum.length; i++) {
            var node    = additum;
            if (node.constructor === String)    node    = document.createTextNode(node);
            parent.appendChild(node);
        }
    },
    
    //------------------------------------------------------------------------------
    //## remove

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

    /** removes all children of a node */
    removeChildren: function(node) {
        //while (node.lastChild)    node.removeChild(node.lastChild);
        node.innerHTML  = "";
    },
    
    //------------------------------------------------------------------------------
    //## replace
    
    /** replace a node with another one */
    replaceNode: function(node, replacement) {
        node.parentNode.replaceChild(replacement, node); 
    },

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

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

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

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

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

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

    /** replaces a class in an element with another */
    replaceClass: function(element, oldClassName, newClassName) {
        this.removeClass(element, oldClassName);
        this.addClass(element, newClassName);
    },
    
    /** sets or unsets a class on an element */
    updateClass: function(element, className, active) {
        var has = this.hasClass(element, className);
        if (has === active) return;
        if (active) this.addClass(element, className);
        else        this.removeClass(element, className);
    },

    //------------------------------------------------------------------------------
    //## position

    /** mouse position in document base coordinates */
    mousePos: function(event) {
        return {
            x: window.pageXOffset + event.clientX,
            y: window.pageYOffset + event.clientY
        };
    },
    
    /** minimum visible position in document base coordinates */
    minPos: function() {
        return {
            x: window.scrollX,
            y: window.scrollY
        };
    },
    
    /** maximum visible position in document base coordinates */
    maxPos: function() {
        return {
            x: window.scrollX + window.innerWidth,
            y: window.scrollY + window.innerHeight
        };
    },
    
    /** position of an element in document base coordinates */
    elementPos: function(element) {
        var parent  = this.elementParentPos(element);
        return {
            x: element.offsetLeft   + parent.x,
            y: element.offsetTop    + parent.y
        };
    },

    /** size of an element */
    elementSize: function(element) {
        return {
            x: element.offsetWidth,
            y: element.offsetHeight
        };
    },

    /** document base coordinates for an elements parent */
    elementParentPos: function(element) {
        // TODO inline in elementPos?
        var pos = { x: 0, y: 0 };
        for (;;) {
            var mode = window.getComputedStyle(element, null).position;
            if (mode === "fixed") {
                pos.x   += window.pageXOffset;
                pos.y   += window.pageYOffset;
                return pos;
            }
            var parent  = element.offsetParent;
            if (!parent)    return pos;
            pos.x   += parent.offsetLeft;
            pos.y   += parent.offsetTop;
            // TODO add scrollTop and scrollLeft here?
            element = parent;
        }
    },
    
    /** moves an element to document base coordinates */
    moveElement: function(element, pos) {
        var container   = this.elementParentPos(element);
        element.style.left  = (pos.x - container.x) + "px";
        element.style.top   = (pos.y - container.y) + "px"; 
    }//,
};

//======================================================================
//## util/Cookie.js 

/** helper functions for cookies */
var Cookie = {
    TTL_DEFAULT:    1*31*24*60*60*1000, // in a month
    TTL_DELETE:       -3*24*60*60*1000, // 3 days before
    
    /** get a named cookie or returns null */
    get: function(key) {
        var point   = new RegExp("\\b" + encodeURIComponent(key).escapeRE() + "=");
        var s       = document.cookie.split(point);
        if (!s) return null;
        s   = s.split(";").replace(/ *$/, "");
        return decodeURIComponent(s);
    },

    /** set a named cookie */
    set: function(key, value, expires) {
        if (!expires)   expires = this.timeout(this.TTL_DEFAULT);
        document.cookie = encodeURIComponent(key) + "=" + encodeURIComponent(value) +
                        "; expires=" + expires.toGMTString() +
                        "; path=/";
    },

    /** delete a named cookie */
    del: function(key) {
        this.set(key, "", 
                this.timeout(this.TTL_DELETE));
    },

    /** calculate a date a given number of millis in the future */
    timeout: function(offset) {
        var expires     = new Date();
        expires.setTime(expires.getTime() + offset);
        return expires;
    }//,
};

//======================================================================
//## util/Ajax.js 

/** ajax helper */
var Ajax = {
    /** 
     * create and use an XMLHttpRequest with named parameters 
     *
     * data
     *      method      optional string, defaults to GET
     *      url         mandatory string, may contains parameters
     *      urlParams   optional map or Array of pairs, can be used together with params in url
     *      body        optional string
     *      bodyParams  optional map or Array of pairs, overwrites body
     *      charset     optional string for bodyParams
     *      headers     optional map
     *      timeout     optional number of milliseconds
     *
     * callbacks, all get the client as first parameter
     *      exceptionFunc       called when the client throws an exception
     *      completeFunc        called after the more specific functions
     *      noSuccessFunc       called in all cases when no success
     *
     *      successFunc         called for 200..300, gets the responseText
     *      intermediateFunc    called for 300..400
     *      failureFunc         called for 400..500
     *      errorFunc           called for 500..600
     */
    call: function(args) {
        if (!args.url)  throw "url argument missing";
        
        // create client
        var client  = new XMLHttpRequest();
        client.args = args;
        client.debug = function() {
            return client.status + " " + client.statusText + "\n"
                    + client.getAllResponseHeaders() + "\n\n"
                    + client.responseText;
        };
        
        // init client
        var method  = args.method || "GET";
        var url     = args.url;
        if (args.urlParams) {
            url += url.indexOf("?") === -1 ? "?" : "&";
            url += this.encodeUrlArgs(args.urlParams);
        }
        client.open(method, url, true);
        
        // state callback
        client.onreadystatechange = function() {
            if (client.readyState !== 4)    return;
            if (client.timer)   clearTimeout(client.timer);
            
            var status  = -1;
            try { status    = client.status; }
            catch (e) {
                if (args.exceptionFunc)     args.exceptionFunc(client, e);
                if (args.noSuccessFunc)     args.noSuccessFunc(client, e);
                return;
            }
                
            if (status >= 200 && status < 300) {
                if (args.successFunc)       args.successFunc(client, client.responseText);
            }
            else if (status >= 300 && status < 400) {
                // TODO location-header?
                if (args.intermediateFunc)  args.intermediateFunc(client);
            }
            else if (status >= 400 && status < 500) {
                if (args.failureFunc)       args.failureFunc(client);
            }
            else if (status >= 500 && status < 600) {
                if (args.errorFunc)         args.errorFunc(client);
            }
            
            if (args.completeFunc)  args.completeFunc(client);
            if (status < 200 || status >= 300) {
                if (args.noSuccessFunc)     args.noSuccessFunc(client);
            }
        };
        
        
        // init headers
        if (args.bodyParams) {
            var contentType = "application/x-www-form-urlencoded";
            if (args.charset)   contentType += "; charset=" + args.charset;
            client.setRequestHeader("Content-Type", contentType);
        }
        if (args.headers) {
            for (var key in args.headers) {
                client.setRequestHeader(key, args.headers);
            }
        }
        
        // init body
        var body;
        if (args.bodyParams) {
            body    = this.encodeFormArgs(args.bodyParams);
        }
        else {
            body    = args.body || null;
        }
        
        // send
        if (args.timeout) {
            client.timer    = setTimeout(client.abort.bind(client), args.timeout);
        }
        client.send(body);
        
        
    },
    
    //------------------------------------------------------------------------------
    //## private
    
    /** 
     * url-encode arguments
     * args may be an Array of Pair of String or a Map from String to String 
     */
    encodeUrlArgs: function(args) {
        if (args.constructor !== Array) args    = this.hashToPairs(args);
        return this.encodeArgPairs(args, encodeURIComponent);
    },
    
    /**
     * encode arguments into application/x-www-form-urlencoded 
     * args may be an Array of Pair of String or a Map from String to String 
     */
    encodeFormArgs: function(args) {
        if (args.constructor !== Array) args    = this.hashToPairs(args);
        return this.encodeArgPairs(args, this.encodeFormValue);
    },
    
    /** compile an Array of Pairs of Strings into the &name=value format */
    encodeArgPairs: function(args, encodeFunc) {
        var out = "";
        for (var i=0; i<args.length; i++) {                       
            var pair    = args;
            if (pair.constructor !== Array) throw "expected a Pair: " + pair;
            if (pair.length !== 2)          throw "expected a Pair: " + pair;
            if (pair === null)   continue;
            out += "&"  + encodeFunc(pair)
                +  "="  + encodeFunc(pair);
        }
        return out && out.substring(1);
    },
    
    /** encode a single form-value. this is a variation on url-encoding */
    encodeFormValue: function(value) {
        // use windows-linefeeds
        value   = value.replace(/\r\n|\n|\r/g, "\r\n");
        // escape funny characters
        value   = encodeURIComponent(value);
        // space is encoded as a plus sign instead of "%20"
        value   = value.replace(/(^|)(%%)*%20/g, "$1$2+");
        return value;
    },
    
    /** 
     * converts a hash into an Array of Pairs (2-element Arrays). 
     * null values generate no Pair, 
     * array values generate multiple Pairs, 
     * other values are toString()ed 
     */
    hashToPairs: function(map) {
        var out = ;
        for (var key in map) {
            var value   = map;
            if (value === null) continue;
            if (value.constructor === Array) {
                for (var i=0; i<value.length;i++) {
                    var subValue    = value;
                    if (subValue === null)  continue;
                    out.push();
                }
                continue;
            }
            out.push();
        }
        return out;
    }//,
};

//======================================================================
//## util/Form.js 

/** HTMLFormElement helper functions */
var Form = {
    //------------------------------------------------------------------------------
    ///## finder
    
    /** finds a HTMLForm or returns null */
    find: function(ancestor, nameOrIdOrIndex) {
        var forms   = ancestor.getElementsByTagName("form");
        if (typeof nameOrIdOrIndex === "number") {
            if (nameOrIdOrIndex >= 0
            && nameOrIdOrIndex < forms.length)  return forms;
            else                                return null;
        }
        for (var i=0; i<forms.length; i++) {
            var form    = forms;
            if (this.elementNameOrId(form) === nameOrIdOrIndex) return form;
        }
        return null;
    },
    
    /** returns the name or id of an element or null */
    elementNameOrId: function(element) {
        return  element.name    ? element.name
            :   element.id      ? element.id
            :   null;
    },
    
    //------------------------------------------------------------------------------
    //## serializer
    
    /**
     * parses HTMLFormElement and its HTML*Element children 
     * into an Array of name/value-pairs (2-element Arrays).
     * these pairs can be used as bodyArgs parameter for Ajax.call.
     *
     * returns an Array of Pairs, optionally with one of
     * the button/image/submit-elements activated
     */
    serialize: function(form, buttonName) {
        var out = ;
        for (var i=0; i<form.elements.length; i++) {
            var element = form.elements;
            
            if (!element.name)      continue;
            if (element.disabled)   continue;
        
            var handlingButton = element.type === "submit" 
                                || element.type === "image" 
                                || element.type === "button";
            if (handlingButton 
            && element.name !== buttonName) continue;
            
            var pairs   = this.elementPairs(element);
            out = out.concat(pairs);
        }
        return out;
    },
    
    /** 
     * returns an Array of Pairs for a single input element.
     * in most cases, it contains zero or one Pair. 
     * more than one are possible for select-multiple.
     */
    elementPairs: function(element) {
        var name    = element.name;
        var type    = element.type;
        var value   = element.value;
        
        if (type === "reset") {
            return ;
        }
        else if (type === "submit") {
            if (value)  return  ];
            else        return  ];
        }
        else if (type === "button" || type === "image") {
            if (value)  return  ];
            else        return  ];
        }
        else if (type === "checkbox" || type === "radio") {
                 if (!element.checked)  return ;
            else if (value !== null)    return  ];
            else                        return  ];    
        }
        else if (type === "select-one") {
            if (element.selectedIndex !== -1)   return  ];
            else                                return ;
        }
        else if (type === "select-multiple") {
            var pairs   = ;
            for (var i=0; i<element.options.length; i++) {
                var opt = element.options;
                if (!opt.selected)  continue;
                pairs.push();
            }
            return pairs;
        }
        else if (type === "text" || type === "password" || type === "hidden" || type === "textarea") {
            if (value)  return  ];
            else        return  ];
        }
        else if (type === "file") {
            // NOTE: can't do anything here :(
            return ;
        }
        else {
            throw "field: " + name + " has the unknown type: " + type;
        }
    }//,
};

//======================================================================
//## lib/core/Actions.js 

/** 
 * ajax functions for MediaWiki
 * uses wgScript, wgScriptPath and Titles.specialPage
 */
var Actions = {
    /** 
     * example feedback implementation, implement this interface
     * if you want to get notified about an Actions progress
     */
    NoFeedback: {
        job:        function(s) {},
        work:       function(s) {},
        success:    function(s) {},
        failure:    function(s) {}//,
    },

    //------------------------------------------------------------------------------
    //## change page content
    
    /** replace the text of a page with a replaceFunc. the replaceFunc can return null to abort. */
    replaceText: function(feedback, title, replaceFunc, summary, minorEdit, allowCreate, doneFunc) {
        this.editPage(feedback, title, null, null, summary, minorEdit, allowCreate, replaceFunc, doneFunc);
    },

    /** add text to the end of a spage, the separator is optional */
    appendText: function(feedback, title, text, summary, separator, allowCreate, doneFunc) {
        var changeFunc = TextUtil.suffixFunc(separator, text);
        this.editPage(feedback, title, null, null, summary, false, allowCreate, changeFunc, doneFunc);
    },

    /** add text to the start of a page, the separator is optional */
    prependText: function(feedback, title, text, summary, separator, allowCreate, doneFunc) {
        // could use section=0 if there wasn't the separator
        var changeFunc = TextUtil.prefixFunc(separator, text);
        this.editPage(feedback, title, null, null, summary, false, allowCreate, changeFunc, doneFunc);
    },

    /** restores a page to an older version */
    restoreVersion: function(feedback, title, oldid, summary, doneFunc) {
        var changeFunc  = TextUtil.idFunc();
        this.editPage(feedback, title, oldid, null, summary, false, false, changeFunc, doneFunc);
    },
    
    /**
     * edits a page's text
     * except feedback and title, all parameters can be null
     */
    editPage: function(feedback, title, oldid, section, summary, minor, allowCreate, textFunc, doneFunc) {
        feedback    = feedback || this.NoFeedback;
        feedback.job("editing page: " + title + " with oldid: " + oldid + " and section: " + section);
        var args = {
            title:      title,
            oldid:      oldid,          
            section:    section,
            action:     "edit"
        };
        var self    = this;
        function change(form, doc) {
            if (!allowCreate && doc.getElementById("newarticletext"))   return false;
            if (summary !== null)   form.elements.value        = summary;
            if (minor !== null)     form.elements.checked    = minor;
            var text    = form.elements.value;
            if (textFunc) {
                text    = text.replace(/^+$/, "");
                text    = textFunc(text);
                if (text === null) { feedback.failure("aborted"); return false; }
            }
            form.elements.value       = text
            return true;
        }
        var afterEdit   = this.afterEditFunc(feedback, doneFunc);
        this.action(feedback, args, "editform", change, 200, afterEdit);
    },
    
    /** undoes an edit */
    undoVersion: function(feedback, title, undo, undoafter, doneFunc) {
        feedback    = feedback || this.NoFeedback;
        feedback.job("undoing page: " + title + " with undo: " + undo + " and undoafter: " + undoafter);
        var args = {
            title:      title,
            undo:       undo,
            undoafter:  undoafter,
            action:     "edit"
        };
        function change(form, doc) {
            return true;
        }
        var afterEdit   = this.afterEditFunc(feedback, doneFunc);
        this.action(feedback, args, "editform", change, 200, afterEdit);
    },
    
    /** 
     * finds the newest edits of a user on a page 
     * the foundFunc is called with title, user, previousUser, revId and timestamp
     */
    newEdits: function(feedback, title, user, foundFunc) {
        feedback    = feedback || this.NoFeedback;
        var self    = this;
        
        function phase1() {
            feedback.work("fetching revision history");
            var apiArgs = {
                action: "query",
                prop:   "revisions",
                titles: title,
                rvprop: "ids|timestamp|flags|comment|user", 
                rvlimit: 50//,
            };
            self.apiCall(feedback, apiArgs, phase2);
        }
        function phase2(json) {
            feedback.work("parsing revision history");
            
            /** returns the first element of a map */
            function firstElement(map) {
                for (key in map) { return map; }
                return null;
            }
            var page    = firstElement(json.query.pages);
            if (page == null) {
                feedback.failure("no suitable revision found");
                return;
            }
            
            var rev = (function() {
                var revs    = page.revisions;
                for (var i=0; i<revs.length; i++) {
                    var rev = revs;
                    rev.index   = i;
                    if (rev.user !== user)  return rev;
                }
                return null;    // no version found;
            })();
                    
            if (rev === null) {
                feedback.failure("no suitable revision found");
                return;
            }
            if (rev.index === 0)    {
                feedback.failure("found conflicting revision by user " + rev.user);
                return;
            }
            
            feedback.success("found revision " + rev.revid);
            foundFunc(title, user, rev.user, rev.revid, rev.timestamp);
        }
        
        phase1();
    },

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

    /** watch or unwatch a page. the doneFunc is optional */
    watchedPage: function(feedback, title, watch, doneFunc) {
        feedback    = feedback || this.NoFeedback;
        var action  = watch ? "watch" : "unwatch";
        feedback.job(action + " page: " + title);
        var actionArgs  = {
            title:  title,
            action: action//,
        };
        feedback.work("GET " + mw.config.get('wgScript') + " with " + this.debugArgsString(actionArgs));
        function done(source) {
            if (source.status !== 200) {
                // source.args.method, source.args.url
                feedback.failure(source.status + " " + source.statusText);
                return;
            }
            feedback.success("done");
            if (doneFunc)   doneFunc();
        }
        Ajax.call({
            method:         "GET",
            url:            mw.config.get('wgScript'),
            urlParams:      actionArgs,
            successFunc:    done//,
        });
    },

    /** move a page */
    movePage: function(feedback, oldTitle, newTitle, reason, moveTalk, moveSub, watch, leaveRedirect, doneFunc) {
        feedback    = feedback || this.NoFeedback;
        feedback.job("move page: " + oldTitle + " to: " + newTitle);
        var args = {
            title:  this.specialTitle("MovePage"),
            target: oldTitle    // url-encoded, mandatory
        };
        function change(form, doc) {
            form.elements.value           = oldTitle;
            form.elements.value           = newTitle;
            form.elements.value             = reason;
            if (form.elements)
            form.elements.checked         = moveTalk;
            if (form.elements)
            form.elements.checked     = moveSub;
            if (form.elements)
            form.elements.checked    = leaveRedirect;
            form.elements.value              = watch;
            // TODO wpConfirm
            return true;
        }
        this.action(feedback, args, "movepage", change, 200, doneFunc);
    },
    
    /** rollback an edit, the summary may be null */
    rollbackEdit: function(feedback, title, from, token, summary, doneFunc) {
        feedback    = feedback || this.NoFeedback;
        feedback.job("rolling back page: " + title + " from: " + from);
        var actionArgs = {
            title:      title,
            from:       from,
            token:      token,
            summary:    summary,
            action:     "rollback"//,
        };
        feedback.work("GET " + mw.config.get('wgScript') + " with " + this.debugArgsString(actionArgs));
        function done(source) {
            if (source.status !== 200) {
                // source.args.method, source.args.url
                feedback.failure(source.status + " " + source.statusText);
                return;
            }
            feedback.success("done");
            if (doneFunc)   doneFunc();
        }
        Ajax.call({
            method:         "GET",
            url:            mw.config.get('wgScript'),
            urlParams:      actionArgs,
            successFunc:    done//,
        });
    },

    /** delete a page. if the reason is null, the original reason text is deleted */
    deletePage: function(feedback, title, reason, doneFunc) {
        feedback    = feedback || this.NoFeedback;
        feedback.job("delete page: " + title);
        var args = {
            title:  title,
            action: "delete"
        };
        function change(form, doc) {
            if (reason !== null) {
                reason  =  TextUtil.joinPrintable(" - ", [ 
                                reason,
                                form.elements.value ]);
            }
            else {
                reason  = "";
            }
            form.elements.value = reason;
            return true;
        }
        this.action(feedback, args, "deleteconfirm", change, 200, doneFunc);
    },
    
    /** delete a file. if the reason is null, the original reason text is deleted */
    deleteFile: function(feedback, title, reason, doneFunc) {
        feedback    = feedback || this.NoFeedback;
        feedback.job("delete file: " + title);
        var args = {
            title:  title,
            action: "delete"
        };
        function change(form, doc) {
            if (reason !== null)     {
                reason  = TextUtil.joinPrintable(" - ", [ 
                                reason,
                                form.elements.value ]);
            }
            else {
                reason  = "";
            }
            form.elements.value = reason;
            // mw-filedelete-submit
            return true;
        }
        this.action(feedback, args, 0, change, 200, doneFunc);
    },

    /**
     * change a page's protection state
     * allowed values for the levels are "", "autoconfirmed" and "sysop"
     * cascade should be false in most cases
     * expiry may be empty for indefinite, "indefinite", 
     * or a number followed by a space and 
     * "years", "months", "days", "hours" or "minutes"
     */
    protectPage: function(feedback, title, 
            levelEdit, expiryEdit, 
            levelMove, expiryMove,
            levelCreate, expiryCreate, 
            reason, cascade, watch, doneFunc) {
        feedback    = feedback || this.NoFeedback;
        feedback.job("protect page: " + title);
        var args    = {
            title:  title,
            action: "protect"
        };
        function change(form, doc) {
            // for existing pages
            if (form.elements)
            form.elements.value     = levelEdit;    // plus mwProtectExpirySelection-edit named wpProtectExpirySelection-edit
            if (form.elements)
            form.elements.value   = expiryEdit;   // named mwProtect-expiry-edit
            
            // for existing pages
            if (form.elements)
            form.elements.value     = levelMove;    // plus mwProtectExpirySelection-move named wpProtectExpirySelection-move
            if(form.elements)
            form.elements.value   = expiryMove;   // named mwProtect-expiry-move
            
            // for deleted pages
            if (form.elements)
            form.elements.value   = levelCreate;  // plus mwProtectExpirySelection-create named wpProtectExpirySelection-create
            if (form.elements)
            form.elements.value = expiryMove;   // named mwProtect-expiry-create
        
         
            // for both deleted and existing pages
            form.elements.checked  = cascade;
            form.elements.value         = reason;   // plus wpProtectReasonSelection    
            form.elements.value           = watch;
             
            
            return true;
        }
        // this form does not have a name
        this.action(feedback, args, 0, change, 200, doneFunc);
    },

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

    /** 
     * block a user. 
     * anonOnly, createAccounts, enableAutoblock and allowUserTalk default to true
     * expiry may be "indefinite", 
     * or a number followed by a space and 
     * "years", "months", "days", "hours" or "minutes"
     */
    blockUser: function(feedback, user, expiry, reason, anonOnly, createAccount, enableAutoblock, emailBan, allowUserTalk, watchUser, allowChange, doneFunc) {
        feedback    = feedback || this.NoFeedback;
        feedback.job("block user: " + user + " for: " + expiry);
        var args = {
            title:  this.specialTitle("BlockIP"),
            ip:     user    // url-encoded, optional
        };
        function change(form, doc) {
            if (!allowChange && form.elements) return false;
            form.elements.value       = user;
            form.elements.value        = reason;
            form.elements.checked         = anonOnly;
            form.elements.checked    = createAccount;
            form.elements.checked  = enableAutoblock;
            form.elements.checked         = emailBan;
            form.elements.checked    = allowUserTalk;
            form.elements.checked        = watchUser;
            form.elements.value         = expiry;
            
            return true;
        }
        this.action(feedback, args, "blockip", change,  200, doneFunc);
    },

    /** send an email to a user. */
    sendEmail: function(feedback, user, subject, body, ccSelf, doneFunc) {
        feedback    = feedback || this.NoFeedback;
        feedback.job("sending email to user: " + user + " with subject: " + subject);
        var args = {
            title:  this.specialTitle("EmailUser"),
            target: user
        };
        function change(form, doc) {
            form.elements.value    = subject;
            form.elements.value       = body;
            form.elements.value       = ccSelf;
            return true;
        }
        this.action(feedback, args, "emailuser", change,  200, doneFunc);
    },
    
    //------------------------------------------------------------------------------
    //## private
    
    /** returns a doneFunc displaying an error if an edit was not successful or calls the (optional) doneFunc */
    afterEditFunc: function(feedback, doneFunc) {
        feedback    = feedback || this.NoFeedback;
        return function(text) {
            var doc;
            try { 
                doc = XMLUtil.parseXML(text);
            }
            catch (e) {
                feedback.failure("cannot parse XML: " + e);
                return;
            }
            if (doc.getElementById('wikiPreview')) {
                feedback.failure("cannot save, preview detected");
                return;
            }
            var form    = Form.find(doc, "editform");
            if (form) {
                feedback.failure("cannot save, editform detected");
                return;
            }
            if (doneFunc)   doneFunc(text);
        };
    },

    /**
     * get a form, change it, post it.
     * the changeFunc gets the form as its first, the complete document as its second parameter
     * and modifies this form in-place. it may return false to abort.
     * the doneFunc is called  after modification with the document text and may be left out
     */
    action: function(feedback, actionArgs, formName, changeFunc, expectedPostStatus, doneFunc) {
        feedback    = feedback || this.NoFeedback;
        var self    = this;
        function phase1() {
            // get the form
            feedback.work("GET " + mw.config.get('wgScript') + " with " + self.debugArgsString(actionArgs));
            Ajax.call({
                method:         "GET",
                url:            mw.config.get('wgScript'),
                urlParams:      actionArgs,
                successFunc:    phase2,
                noSuccessFunc:  failure//,
            });
        }
        function phase2(source) {
            // check status
            var expectedGetStatus   = 200;
            if (expectedGetStatus && source.status !== expectedGetStatus) {
                feedback.failure(source.status + " " + source.statusText);
                return;
            }

            // get document
            var doc;
            try {
                doc = XMLUtil.parseXML(source.responseText);
            }
            catch (e) {
                feedback.failure("cannot parse XML: " + e);
                return;
            }
            
            // get form
            var form    = Form.find(doc, formName);
            if (form === null) { 
                feedback.failure("missing form: " + formName); 
                return; 
            }
            
            // modify form
            var ok;
            try {
                ok  = changeFunc(form, doc);
            }
            catch(e) {
                feedback.failure("cannot change form: " + e);
                return;
            }
            if (!ok) {
                feedback.failure("aborted");
                return;
            }
            
            // post the form
            var url     = form.action;
            var data    = Form.serialize(form);
            feedback.work("POST " + url);
            Ajax.call({
                method:         "POST",
                url:            url,
                bodyParams:     data,
                successFunc:    phase3,
                noSuccessFunc:  failure//,
            });
        }
        function phase3(source) {
            // check status
            if (expectedPostStatus && source.status !== expectedPostStatus) {
                feedback.failure(source.status + " " + source.statusText);
                return;
            }
            
            // done
            feedback.success("done");
            if (doneFunc)   doneFunc(source.responseText);
        }
        function failure(source) {
            feedback.failure(source.status + ": " + self.debugArgsString(source.args));
        }
        phase1();
    },
    
    /** call the api, calls doneFunc with the JSON result (and the response text) if successful */
    apiCall: function(feedback, args, doneFunc) {
        feedback    = feedback || this.NoFeedback;
        //var   self    = this;
        function phase1() {
            var apiPath     = mw.config.get('wgScriptPath') + "/api.php";
            var bodyParams  = {
                format: "json"//,
            };
            Objects.copySlots(args, bodyParams);
            feedback.work("POST " + apiPath);
            Ajax.call({
                url:            apiPath,
                method:         "POST",
                bodyParams:     bodyParams,
                successFunc:    phase2,
                noSuccessFunc:  failure//,
            });
        }
        function phase2(source) {
            var text    = source.responseText;
            var json;
            try {
                json    = text.parseJSON();
            }
            catch (e) {
                feedback.failure("cannot parse JSON: " + e);
                return;
            }
            feedback.success("done");
            if (doneFunc)   doneFunc(json, text);
        }
        function failure(source) {
            feedback.failure("api status: " + source.status);
        }
        phase1();
    },
    
    /** bring a map into a human readable form */
    debugArgsString: function(args) {
        var out = "";
        for (key in args) {
            var arg = args;
            if (arg !== null && arg.constructor === Function) continue;
            out += ", " + key + ": " + arg;
        }
        return out.substring(2); 
    },
    
    /** SpecialPage access, uses Titles.specialPage if Titles exists */
    specialTitle: function(specialName) {
        // HACK for standalone operation
        if (!window.Titles)  return "Special:" + specialName;
        return Titles.specialPage(specialName);
    }//,
};

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