Difference between revisions of "Дольник:Pill/monobook.js"

С Сибирска Википеддя
Айдать на коробушку Айдать на сыскальник
Content deleted Content added
*>Pill
mNo edit summary
*>Pill
No edit summary
Line 1: Line 1:
// from http://de.wikipedia.org/w/index.php?title=Benutzer:D/monobook/user.js by [[:w:de:User:D]] -
document.write('<script type="text/javascript" src="'
// for implementing it on your monobook, please use the document.write function as described on
+ 'http://de.wikipedia.org/w/index.php?title=Benutzer:D/monobook/user.js'
// http://de.wikipedia.org/wiki/Benutzer:D/monobook#einbinden, otherwise bug fixes an changes of the
+ '&action=raw&ctype=text/javascript&dontcountme=s"></script>');
// source code don't work.
/* <pre><nowiki> */

//======================================================================
//## lib/util/prototypes.js

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

/** returns the index of an element or -1 */
if (!Array.prototype.indexOf)
Array.prototype.indexOf = function(element) {
for (var i=0; i<this.length; i++) {
if (this[i] === element) return i;
}
return -1;
}

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

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

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

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

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

/** copies an object */
function copyOf(obj) {
var out = {};
copy(obj, out);
return out;
}

/** copies an object's properties */
function copy(source, target) {
for (var key in source)
target[key] = source[key];
}

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

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

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

/** find the next element from el which has a given nodeName or is non-text */
nextElement: function(el, nodeName) {
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; }
}
},

/** finds a HTMLForm or returns null */
findForm: function(ancestor, nameOrIdOrIndex) {
var forms = ancestor.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;
},

/** returns the name or id of an element or null */
elementNameOrId: function(element) {
return element.name ? element.name
: element.id ? element.id
: null;
},
/** whether an ancestor contains an element */
contains: function(ancestor, element) {
for (;;) {
if (element == ancestor) return true;
if (element == null) return false;
element = element.parentNode;
}
},
//------------------------------------------------------------------------------
//## 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);
},

//------------------------------------------------------------------------------
//## add

/** inserts text, a node or an Array of these before a target node */
pasteBefore: function(target, additum) {
if (additum.constructor != Array) additum = [ additum ];
var parent = target.parentNode;
for (var i=0; i<additum.length; i++) {
var node = additum[i];
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 = [ additum ];
for (var i=0; i<additum.length; i++) {
var node = additum[i];
if (node.constructor == String) node = document.createTextNode(node);
parent.appendChild(node);
}
},

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

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

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

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

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

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

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

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

/** mouse position in document 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.parentPos(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 objects coordinates */
parentPos: function(element) {
var pos = { x: 0, y: 0 };
for (;;) {
var mode = window.getComputedStyle(element, null).position;
if (mode == "fixed") {
pos.x += window.pageXOffset;
pos.y += window.pageYOffset;
return pos;
}
var parent = element.offsetParent;
if (!parent) return pos;
pos.x += parent.offsetLeft;
pos.y += parent.offsetTop;
element = parent;
}
},
/** moves an element to document base coordinates */
moveElement: function(element, pos) {
var container = this.parentPos(element);
element.style.left = (pos.x - container.x) + "px";
element.style.top = (pos.y - container.y) + "px";
},
};

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

/**
* tries to behave similar to a Location object
* protocol includes everything before the //
* host is the plain hostname
* port is a number or null
* pathname includes the first slash or is null
* hash includes the leading # or is null
* search includes the leading ? or is null
*/
function Loc(urlStr) {
var m = this.parser(urlStr);
if (!m) throw "cannot parse URL: " + urlStr;
this.local = !m[1];
this.protocol = m[2] ? m[2] : null; // http:
this.host = m[3] ? m[3] : null; // ru-sib.wikipedia.org
this.port = m[4] ? parseInt(m[4].substring(1)) : null; // 80
this.pathname = m[5] ? m[5] : ""; // /wiki/Test
this.hash = m[6] ? m[6] : ""; // #Industry
this.search = m[7] ? m[7] : ""; // ?action=edit
}
Loc.prototype = {
/** matches a global or local URL */
parser: /((.+?)\/\/([^:\/]+)(:[0-9]+)?)?([^#?]+)?(#[^?]*)?(\?.*)?/,

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

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

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

/** converts the searchstring into an associative array */
args: function() {
if (!this.search) return {};
var out = {};
var split = this.search.substring(1).split("&");
for (i=0; i<split.length; i++) {
var parts = split[i].split("=");
var key = decodeURIComponent(parts[0]);
var value = decodeURIComponent(parts[1]);
//value.raw = parts[1];
out[key] = value;
}
return out;
},
};

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

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

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

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

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

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

/** ajax helper functions */
Ajax = {
/** headers preset for POSTs */
urlEncoded: function(charset) { return {
"Content-Type": "application/x-www-form-urlencoded; charset=" + charset
}},

/** headers preset for POSTs */
multipartFormData: function(boundary, charset) { return {
"Content-Type": "multipart/form-data; boundary=" + boundary + "; charset=" + charset
}},

/** encode an Object or Array into URL parameters. */
encodeArgs: function(args) {
if (!args) return "";
var query = "";
for (var arg in args) {
var key = encodeURIComponent(arg);
var raw = args[arg];
if (raw === null) continue;
var value = encodeURIComponent(raw.toString());
query += "&" + key + "=" + value;
}
if (query == "") return "";
return query.substring(1);
},

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

/** create and use an XMLHttpRequest with named parameters */
call: function(args) {
// create client
var client = new XMLHttpRequest();
// extensions
var self = this;
client.args = args;
client.getXML = function() { return self.parseXML(client.responseText); };
client.getE4X = function() { return self.parseE4X(client.responseText); };
// open
client.open(
args.method ? args.method : "GET",
args.url,
args.async ? args.async == true : true
);
// set headers
if (args.headers) {
for (var name in args.headers) {
client.setRequestHeader(name, args.headers[name]);
}
}
// handle state changes
client.onreadystatechange = function() {
if (args.state) args.state(client, args);
if (client.readyState != 4) return;
if (args.doneFunc) args.doneFunc(client, args);
}
// debug status
client.debug = function() {
return client.status + " " + client.statusText + "\n"
+ client.getAllResponseHeaders() + "\n\n"
+ client.responseText;
}
// and start
client.send(args.body ? args.body : null);
return client;
},

/** 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")
throw "XML parser error: " + root.textContent;
return doc;
},

/** parses a String into an e4x XML object */
parseE4X: function(text) {
return new XML(text.replace(/^<\?xml[^>]*>/, ""));
},

/** serialize an XML (e4x) or XMLDocument to a String */
unparseXML: function(xml) {
if (xml instanceof XMLDocument) return new XMLSerializer().serializeToString(xml);
else if (xml instanceof XML) return xml.toXMLString();
else throw "not an XML document";
},
};

//======================================================================
//## lib/util/IP.js

IP = {
/** matches IPv4-like strings */
v4RE: /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/,

/** whether thw string denotes an IPv4-address */
isV4: function(s) {
var m = this.v4RE(s);
if (!m) return false;
for (var i=1; i<=4; i++) {
var byt = parseInt(m[i]);
if (byt < 0 || byt > 255) return false;
}
return true;
},
}

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

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

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

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

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

/** User namespace */
userNS: null,

/** User_talk namespace */
userTalkNS: null,

/** name of the logged in user */
user: wgUserName,
/** maps canonical special page names to localized ones */
localSpecialPages: null,
/** maps localized special page names to canonical ones */
canonicalSpecialPages: null,

/** whether user has news */
haveNews: function() {
return DOM.fetch('bodyContent', "div", "usermessage", 0) != null;
},
/** create a localized page title from the canonical special page name and an optional parameter */
specialTitle: function(name, param) {
var localName = this.localSpecialPages[name];
if (!localName) throw "cannot localize specialpage: " + name;
return this.specialNS + ":" + localName
+ ( param ? "/" + param : "" );
},

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

/** encode parameters into an URL */
encodeURL: function(args, shorten) {
// TODO: localized specialpage names
args = copyOf(args);

// Special:Randompage _requires_ smushing!
var specialPage = this.specialPageInfo(args.title);
if (specialPage) {
if (shorten
|| specialPage.name == "Randompage" && specialPage.param)
this.smush(args, specialPage);
else
this.desmush(args, specialPage);
}

// create start path
var path;
if (shorten) {
path = this.readPath
+ this.fixTitle(encodeURIComponent(this.normalizeTitle(args.title)))
.replace(/%2b/gi, "+"); // short URLs use "+" literally
delete args.title; // not needed any more
}
else {
path = this.actionPath;
}
path += "?";

// normalize title-type parameters
var normalizeParams = this.normalizeParams(specialPage);
for (var key in args) {
if (key == "_smushed") continue;

var value = args[key];
if (value === null) continue;

var code = encodeURIComponent(value.toString());
if (normalizeParams[key]) {
code = this.fixTitle(code);
}
path += encodeURIComponent(key)
+ "=" + code + "&";
}

return this.site + path.replace(/[?&]$/, "");
},

/**
* decode an URL or path into a map of parameters. all titles are normalized.
* if a specialpage has a smushed parameter, it is removed from the title
* and handled like any other parameter. additionally, a _smushed parameter
* is added providing key and value if the smushed parameter.
*/
decodeURL: function(url) {
// TODO: localized specialpage names
var args = {};
var loc = new Loc(url);

// readPath has the title directly attached
if (loc.pathname != this.actionPath) {
var read = loc.pathname.scan(this.readPath);
if (!read) throw "cannot decode: " + url;
args.title = decodeURIComponent(read);
}

// decode all parameters, "+" means " "
if (loc.search) {
var split = loc.search.substring(1).split("&");
for (i=0; i<split.length; i++) {
var parts = split[i].split("=");
var key = decodeURIComponent(parts[0]);
var code = parts[1].replace(/\+/g, "%20");
args[key] = decodeURIComponent(code)
}
}

if (!args.title) throw "decode: missing page title in: " + loc;

// normalize title-type parameters and desmush
var specialPage = this.specialPageInfo(args.title);
var normalizeParams = this.normalizeParams(specialPage);
for (var key in normalizeParams) {
if (args[key]) {
args[key] = this.normalizeTitle(args[key]);
}
}
if (specialPage) {
this.desmush(args, specialPage);
}
return args;
},
//------------------------------------------------------------------------------
//## private

/**
* replaces Special:Page?key=value with Special:Page/value
* the name of the specialPage must be in canonical form
*/
smush: function(args, specialPage) {
delete args._smushed;
// Watchlist smushes irregularly
if (specialPage.name == "Watchlist") {
if (args.edit) { args.title += "/edit"; delete args.edit; }
else if (args.clear) { args.title += "/clear"; delete args.clear; }
return;
}

var smushed = this.specialSmush[specialPage.name];
if (!smushed) return;

var value = args[smushed];
if (value || value == "") {
args.title += "/" + value;
delete args[smushed];
}
},

/**
* replaces Special:Page/value with Special:Page?key=value
* the name of the specialPage must be in canonical form
*/
desmush: function(args, specialPage) {
// Watchlist smushes irregularly
if (specialPage.name == "Watchlist") {
var param = specialPage.param;
if (!param) return;
args[param] = "yes";
args.title = this.specialNS + ":" + specialPage.name;
args._smushed = {
key: param,
value: param
};
return;
}
var smushed = this.specialSmush[specialPage.name];
if (smushed && specialPage.param) {
args.title = this.specialNS + ":" + specialPage.localName;
args[smushed] = specialPage.param;
args._smushed = {
key: smushed,
value: specialPage.param,
};
}
},

/** returns which parameters need to be normalized */
normalizeParams: function(specialPage) {
var out = { title: true };
if (!specialPage) return out;

var params = this.specialTitleParam[specialPage.name];
if (!params) return out;

for (var i=0; i<params.length; i++) {
out[params[i]] = true;
}
return out;
},

/** to the user, all titles use " " instead of "_" */
normalizeTitle: function(title) {
return title.replace(/_/g, " ");
},

/** some characters are encoded differently in titles */
fixTitle: function(code) {
return code.replace(/%3a/gi, ":")
.replace(/%2f/gi, "/")
.replace(/%20/gi, "_")
.replace(/%5f/gi, "_");
},
/** returns canonical name and optional param if title is Special:Name or Special:Name/param, else null */
specialPageInfo: function(title) {
title = this.normalizeTitle(title).scan(this.specialNS + ":");
if (title == null) return null;
var m = /(.*?)\/(.*)/(title);
var name;
var param;
var localName;
if (m) {
name = m[1];
param = m[2];
}
else {
name = title;
param = null;
}
var canonicalName = this.canonicalSpecialPages[name];
if (canonicalName) {
localName = name;
name = canonicalName;
}
else {
localName = this.localSpecialPages[name];
if (!localName) throw "could not localize special page: " + name;
}
return {
name: name,
param: param,
localName: localName,
};
},

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

/** to be called onload */
init: function() {
// init localized namespaces
var nss = this.namespaces[this.language];
if (!nss) throw "unconfigured namespaces language: " + language;
this.specialNS = nss.special; // -1
this.userNS = nss.user; // 2
this.userTalkNS = nss.userTalk; // 3
// init localized specialpage names
this.localSpecialPages = this.specialPages[this.language];
if (!this.localSpecialPages) throw "unconfigured specialPages language: " + language;
// init inverse mapping
// TODO: this is slow!
this.canonicalSpecialPages = {};
for (key in this.localSpecialPages) {
var value = this.localSpecialPages[key];
this.canonicalSpecialPages[value] = key;
}
},

//------------------------------------------------------------------------------
//## tables
/** indexed by language */
namespaces: {
de: {
special: "Spezial",
user: "Benutzer",
userTalk: "Benutzer Diskussion",
},
en: {
special: "Special",
user: "User",
userTalk: "User talk",
},
},
/** indexed by language */
specialPages: {
de: {
"Contributions": "Beiträge",
"Specialpages": "Spezialseiten",
"Emailuser": "E-Mail",
"Verweisliste": "Whatlinkshere",
"Move": "Verschieben",

"Allpages": "Alle Seiten",
"Userlogin": "Anmelden",
"CrossNamespaceLinks": "CrossNamespaceLinks",
"Mostrevisions": "Meistbearbeitete Seiten",
"Disambiguations": "Begriffsklärungsverweise",
"Listusers": "Benutzer",
"Wantedcategories": "Gewünschte Kategorien",
"Watchlist": "Beobachtungsliste",
"Imagelist": "Dateien",
"Filepath": "Filepath",
"DoubleRedirects": "Doppelte Weiterleitungen",
"Preferences": "Einstellungen",
"Wantedpages": "Gewünschte Seiten",
"Upload": "Hochladen",
"Mostlinked": "Meistverlinkte Seiten",
"Booksources": "ISBN-Suche",
"BrokenRedirects": "Kaputte Weiterleitungen",
"Categories": "Kategorien",
"CategoryTree": "CategoryTree",
"Shortpages": "Kürzeste Seiten",
"Longpages": "Längste Seiten",
"Recentchanges": "Letzte Änderungen",
"Ipblocklist": "Gesperrte IPs",
"SiteMatrix": "SiteMatrix",
"Log": "Logbuch",
"Mostcategories": "Meistkategorisierte Seiten",
"Mostimages": "Meistbenutzte Dateien",
"Mostlinkedcategories": "Meistbenutzte Kategorien",
"Newpages": "Neue Seiten",
"Newimages": "Neue Dateien",
"Unusedtemplates": "Unbenutzte Vorlagen",
"Uncategorizedpages": "Nicht kategorisierte Seiten",
"Uncategorizedimages": "Nicht kategorisierte Dateien",
"Uncategorizedcategories": "Nicht kategorisierte Kategorien",
"Prefixindex": "Präfixindex",
"Deadendpages": "Sackgassenseiten",
"Ancientpages": "Älteste Seiten",
"Export": "Exportieren",
"Allmessages": "MediaWiki-Systemnachrichten",
"Statistics": "Statistik",
"Search": "Suche",
"MIMEsearch": "MIME-Typ-Suche",
"Version": "Version",
"Unusedimages": "Unbenutzte Dateien",
"Unusedcategories": "Unbenutzte Kategorien",
"Lonelypages": "Verwaiste Seiten",
"ExpandTemplates": "ExpandTemplates",
"Boardvote": "Boardvote",
"Linksearch": "Linksearch",
"Listredirects": "Weiterleitungen",
"Cite": "Cite",
"Randomredirect": "Zufällige Weiterleitung",
"Random": "Zufällige Seite",
"Undelete": "Wiederherstellen",
"Blockip": "Sperren",
"Unwatchedpages": "Ignorierte Seiten",
"Import": "Importieren",
},
// en is canonical, so this is an identity mapping
en: {
"Contributions": "Contributions",
"Specialpages": "Specialpages",
"Emailuser": "Emailuser",
"Whatlinkshere": "Whatlinkshere",
"Move": "Move",
"Allpages": "Allpages",
"Userlogin": "Userlogin",
"CrossNamespaceLinks": "CrossNamespaceLinks",
"Mostrevisions": "Mostrevisions",
"Disambiguations": "Disambiguations",
"Listusers": "Listusers",
"Wantedcategories": "Wantedcategories",
"Watchlist": "Watchlist",
"Imagelist": "Imagelist",
"Filepath": "Filepath",
"DoubleRedirects": "DoubleRedirects",
"Preferences": "Preferences",
"Wantedpages": "Wantedpages",
"Upload": "Upload",
"Mostlinked": "Mostlinked",
"Booksources": "Booksources",
"BrokenRedirects": "BrokenRedirects",
"Categories": "Categories",
"CategoryTree": "CategoryTree",
"Shortpages": "Shortpages",
"Longpages": "Longpages",
"Recentchanges": "Recentchanges",
"Ipblocklist": "Ipblocklist",
"SiteMatrix": "SiteMatrix",
"Log": "Log",
"Mostcategories": "Mostcategories",
"Mostimages": "Mostimages",
"Mostlinkedcategories": "Mostlinkedcategories",
"Newpages": "Newpages",
"Newimages": "Newimages",
"Unusedtemplates": "Unusedtemplates",
"Uncategorizedpages": "Uncategorizedpages",
"Uncategorizedimages": "Uncategorizedimages",
"Uncategorizedcategories": "Uncategorizedcategories",
"Prefixindex": "Prefixindex",
"Deadendpages": "Deadendpages",
"Ancientpages": "Ancientpages",
"Export": "Export",
"Allmessages": "Allmessages",
"Statistics": "Statistics",
"Search": "Search",
"MIMEsearch": "MIMEsearch",
"Version": "Version",
"Unusedimages": "Unusedimages",
"Unusedcategories": "Unusedcategories",
"Lonelypages": "Lonelypages",
"ExpandTemplates": "ExpandTemplates",
"Boardvote": "Boardvote",
"Linksearch": "Linksearch",
"Listredirects": "Listredirects",
"Cite": "Cite",
"Randomredirect": "Randomredirect",
"Random": "Random",
"Undelete": "Undelete",
"Blockip": "Blockip",
"Unwatchedpages": "Unwatchedpages",
"Import": "Import",
},
},

/** some special pages can smush one parameter to the page title */
specialSmush: {
"Emailuser": "target",
"Contributions": "target",
"Whatlinkshere": "target",
"Recentchangeslinked": "target",
"Undelete": "target",
"Linksearch": "target",
"Newpages": "limit",
"Newimages": "limit",
"Wantedpages": "limit",
"Recentchanges": "limit",
"Allpages": "from",
"Prefixindex": "from",
"Log": "type",
"Blockip": "ip",
"Listusers": "group",
"Filepath": "file",
"Randompage": "namespace",
// Contributions
// a smushed /newbies does not mean a user named "newbies"!
// Randompage
// the namespace parameter is fake, it exists only in smushed form
// Watchlist
// a smushed /edit means ?edit=yes
// a smushed /clear means ?clear=yes
},

/** some parameters of special pages point to pages, in this case space and underscore mean the same */
specialTitleParam: {
"Emailuser": [ "target" ],
"Contributions": [ "target" ],
"Whatlinkshere": [ "target" ],
"Recentchangeslinked": [ "target" ],
"Undelete": [ "target" ],
"Allpages": [ "from", ],
"Prefixindex": [ "from" ],
"Blockip": [ "ip" ],
"Log": [ "page" ],
"Filepath": [ "file" ],
"Randompage": [ "namespace", ],
},
};

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

/** represents the current Page */
Page = {
/** returns the canonical name of the current Specialpage or null */
whichSpecial: function() {
// if it contains a slash, unsmushing failed
var name = this.title.scan(Wiki.specialNS + ":");
if (!name) return null;
// TODO: is this a good idea?
var canonicalName = Wiki.canonicalSpecialPages[name];
// if (!canonicalName) throw "could not map special page to canonical name: " + localName;
return canonicalName ? canonicalName : name;
},

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

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

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

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

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

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

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

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

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

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

// wgPageName / wgTitle
this.title = this.params.title;

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

var self = this;
(function() {
// try User namespace
var tmp = self.title.scan(Wiki.userNS + ":");
if (tmp) self.owner = tmp.replace(/\/.*/, "");
if (self.owner) return;

// try User_talk namespace
var tmp = self.title.scan(Wiki.userTalkNS + ":");
if (tmp) self.owner = tmp.replace(/\/.*/, "");
if (self.owner) return;

// try some special pages
var special = self.whichSpecial();
if (special == "Contributions" || special == "Emailuser") {
self.owner = self.params.target;
}
else if (special == "Blockip") {
self.owner = self.params.ip;
}
else if (special == "Log" && self.params.page) { // && self.params.type == "block"
self.owner = self.params.page.scan(Wiki.userNS + ":");
}
if (self.owner) return;

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

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

/** ajax functions for MediaWiki */
Editor = {
//------------------------------------------------------------------------------
//## change page content
/** replace the text of a page with a replaceFunc */
replaceText: function(feedback, title, replaceFunc, summary, minorEdit, allowCreate, doneFunc) {
feedback.job("change page: " + title);
var args = {
title: title,
action: "edit"
};
function change(v, doc) {
if (!allowCreate && doc.getElementById('newarticletext')) return null;
return {
wpSummary: summary,
wpMinoredit: minorEdit,
wpTextbox1: replaceFunc(v.wpTextbox1.replace(/^[\r\n]+$/, "")),
};
}
this.action(feedback, args, "editform", change, 200, doneFunc);
},

/** add text to the end of a spage, the separator is optional */
appendText: function(feedback, title, text, summary, separator, allowCreate, doneFunc) {
function change(s) { return concatSeparated(s, separator, text); }
this.replaceText(feedback, title, change, summary, false, allowCreate, doneFunc);
},

/** add text to the start of a page, the separator is optional */
prependText: function(feedback, title, text, summary, separator, allowCreate, doneFunc) {
feedback.job("change page: " + title);
var args = {
title: title,
action: "edit",
section: 0
};
function change(v, doc) {
if (!allowCreate && doc.getElementById("newarticletext")) return null;
return {
wpSummary: summary,
wpMinoredit: false,
wpTextbox1: concatSeparated(text, separator, v.wpTextbox1.replace(/^[\r\n]+$/, "")),
};
}
this.action(feedback, args, "editform", change, 200, doneFunc);
},

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

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

/** watch or unwatch a page. the doneFunc is optional */
watchedPage: function(feedback, title, watch, doneFunc) {
var action = watch ? "watch" : "unwatch";
feedback.job(action + " page: " + title);
var url = Wiki.encodeURL({
title: title,
action: action,
});
feedback.work("GET " + url);
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: url,
doneFunc: done,
});
},

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

//------------------------------------------------------------------------------
//## private
/**
* get a form, change it, post it.
* the changeFunc gets the form as its first,
* the complete document as its second parameter
* and returns a map of changed form-fields or null to abort
* the doneFunc is called afterwards and may be left out
*/
action: function(feedback, actionArgs, formName, changeFunc, expectedPostStatus, doneFunc) {
function phase1() {
var url = Wiki.encodeURL(actionArgs);
feedback.work("GET " + url);
Ajax.call({
method: "GET",
url: url,
doneFunc: phase2,
});
}
function phase2(source) {
var expectedGetStatus = 200;
if (expectedGetStatus && source.status != expectedGetStatus) {
feedback.failure(source.status + " " + source.statusText);
return;
}

var doc = source.getXML();
var form = DOM.findForm(doc, formName);
if (form === null) {
feedback.failure("missing form: " + formName);
return;
}

var url = form.action;
var data = self.changedForm(doc, form, changeFunc);
if (data == null) {
feedback.failure("aborted");
return;
}
var headers = Ajax.urlEncoded("UTF-8");
var body = Ajax.encodeArgs(data);
feedback.work("POST " + url);
Ajax.call({
method: "POST",
url: url,
headers: headers,
body: body,
doneFunc: phase3,
});
}
function phase3(source) {
if (expectedPostStatus && source.status != expectedPostStatus) {
feedback.failure(source.status + " " + source.statusText);
return;
}
feedback.success("done");
if (doneFunc) doneFunc();
}
var self = this;
phase1();
},

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

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

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

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

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

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

//======================================================================
//## lib/core/WikiLink.js

/** the label is optional */
function WikiLink(title, label) {
this.title = title;
this.label = label;
}
WikiLink.prototype = {
/** omits the label if it equals the title */
toString: function() {
return this.label && this.label != this.title
? "[[" + this.title + "|" + this.label + "]]"
: "[[" + this.title + "]]";
},
};

/** returns an Array of all WikiLinks contained in a String */
WikiLink.parseAll = function(s) {
// constructed here with /g so e can use exec multiple times
var re = /\[\[[ \t]*([^\]|]+?)[ \t]*(?:\|[ \t]*([^\]]+?)[ \t]*)?\]\]/g;
var out = [];
for (;;) {
var m = re.exec(s);
if (!m) break;
var link = new WikiLink(m[1], m[2]);
out.push(link);
}
return out;
};

//======================================================================
//## lib/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;
}

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

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

/** contains a number of on/off-switches */
function SwitchBoard() {
this.knobs = [];
this.board = document.createElement("span");
this.board.className = "switch-board";

// public
this.component = this.board;
}
SwitchBoard.prototype = {
/** add a knob and set its className */
add: function(knob) {
DOM.addClass(knob, "switch-knob");
DOM.addClass(knob, "switch-off");
this.knobs.push(knob);
this.board.appendChild(knob);
},

/** selects a single knob */
select: function(knob) {
this.changeAll(false);
this.change(knob, true);
},

/** changes selection state of one knob */
change: function(knob, selected) {
if (selected) DOM.replaceClass(knob, "switch-off", "switch-on");
else DOM.replaceClass(knob, "switch-on", "switch-off");
},

/** changes selection state of all knobs */
changeAll: function(selected) {
for (var i=0; i<this.knobs.length; i++) {
this.change(this.knobs[i], selected);
}
},
};

//======================================================================
//## lib/ui/Floater.js

/** a Floater is a small area floating over the document */
function Floater(id, limited) {
this.limited = limited;
// public
this.canvas = document.createElement("div");
this.canvas.id = id;
this.canvas.className = "floater";
// shortcut
this.style = this.canvas.style;
// attaching to a node below body leads to clipping: overflow:visible maybe?
//this.source.appendChild(this.canvas);
document.body.appendChild(this.canvas);
Floater.instances.push(this);
this.style.zIndex = this.minimumZ + Floater.instances.length - 1;
}
Floater.prototype = {
/** z-index for the lowest Floater */
minimumZ: 1000,

/** removes this Floater from the view */
destroy: function() {
Floater.instances.remove(this);
// TODO: change all other fields like it was removed
document.body.removeChild(this.canvas);
},
/** locates the div near a mouse position */
locate: function(pos) {
// helps with https://bugzilla.mozilla.org/show_bug.cgi?id=324819
// display is necessary for position, visibility is not
this.style.display = "block";
if (this.limited) pos = this.limit(pos);
DOM.moveElement(this.canvas, pos);
},
/** limits canvas position to the window */
limit: function(pos) {
var min = DOM.minPos();
var max = DOM.maxPos();
var size = DOM.elementSize(this.canvas);
// HACK: why does the menu go too far to the right without this?
size.x += 16;
pos = { x: pos.x, y: pos.y };
if (pos.x < min.x) pos.x = min.x;
if (pos.y < min.y) pos.y = min.y;
if (pos.x + size.x > max.x) pos.x = max.x - size.x;
if (pos.y + size.y > max.y) pos.y = max.y - size.y;
return pos;
},
/** returns the current location */
location: function() {
// display is necessary for position, visibility is not
this.style.display = "block";
return DOM.elementPos(this.canvas);
},
/** displays the div */
show: function() {
this.style.display = "block";
this.style.visibility = "visible";
},
/** hides the div */
hide: function() {
this.style.display = "none";
this.style.visibility = "hidden";
},
/** raises the div above all other Floaters */
raise: function() {
var all = Floater.instances;
var idx = all.indexOf(this);
if (idx == -1) return;
all.splice(idx, 1);
all.push(this);
for (var i=idx; i<all.length; i++) {
all[i].style.zIndex = i + this.minimumZ;
}
},
/** lower the div blow all other Floaters */
lower: function() {
var all = Floater.instances;
var idx = all.indexOf(this);
if (idx == -1) return;
all.splice(idx, 1);
all.unshift(this);
for (var i=idx; i>= 0; i++) {
all[i].style.zIndex = i + this.minimumZ;
}
},
};

/** all instances z-ordered starting with the lowest */
Floater.instances = [];

//======================================================================
//## lib/ui/PopupMenu.js

/** a PopupMenu display a number of items and call a selectFunc when one of the items is selected */
function PopupMenu(selectFunc) {
this.selectFunc = selectFunc;
this.floater = new Floater(null, true);
this.canvas = this.floater.canvas;
DOM.addClass(this.canvas, "popup-menu-window");
this.canvas.onmouseup = this.maybeSelectItem.bind(this);
}
PopupMenu.prototype = {
/** removes this menu */
destroy: function() {
this.canvas.onmouseup = null;
this.floater.destroy();
},
/** opens at a given position */
showAt: function(pos) {
this.floater.locate(pos);
this.floater.raise();
this.floater.show();
},
/** closes the menu */
hide: function() {
this.floater.hide();
},
/** adds an item, its userdata will be supplied to the selectFunc */
item: function(label, userdata) {
var item = document.createElement("div");
item.className = "popup-menu-item";
item.textContent = label;
item.userdata = userdata;
this.canvas.appendChild(item);
},

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

/** calls the selectFunc with the userData of the selected item */
maybeSelectItem: function(ev) {
var target = ev.target;
for (;;) {
if (DOM.hasClass(target, "popup-menu-item")) {
if (this.selectFunc) {
this.selectFunc(target.userdata);
}
return;
}
target = target.parentNode;
if (!target) return;
}
},
};

//======================================================================
//## lib/ui/PopupSource.js

/** makes a source open a Floater as context-menu */
function PopupSource(source, menu) {
this.source = source;
this.menu = menu;
DOM.addClass(source, "popup-source");
source.oncontextmenu = this.contextMenu.bind(this);
this.boundMouseUp = this.mouseUp.bind(this);
document.addEventListener("mouseup", this.boundMouseUp, false);
}
PopupSource.prototype = {
mouseupCloseDelay: 250,
/** removes all listeners */
destroy: function() {
DOM.removeClass(this.source, "popup-source");
this.source.oncontextmenu = null;
document.removeEventListener("mouseup", this.boundMouseUp, false);
},
/** opens the Floater near the mouse cursor */
contextMenu: function(ev) {
if (ev.target != this.source) return;
var mouse = DOM.mousePos(ev);
// so the document does not get a mouseup shortly after
mouse.x ++;
this.menu.showAt(mouse);

// delay closing so the popup stays open after a short klick
this.abortable = false;
var self = this;
window.setTimeout(
function() { self.abortable = true; },
this.mouseupCloseDelay);
// old-style, stop propagation
return false;
},
/** closes the Floater except within a short time after opening */
mouseUp: function(ev) {
if (this.abortable) {
this.menu.hide();
}
},
};

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

/** creates links to action functions and pages */
Links = {
/**
* create an action link which
* - onclick queries a text or
* - oncontextmenu opens a popup with default texts
* and calls a single-argument function with it.
*
* groups is an Array of Arrays of preset reason Strings,
* a separator is placed between rows. null is allowed to
* disable the popup.
*
* the popupFunc is optional, when it's given it's called instead
* of the func for a popup reason, but not for manual input
*/
promptPopupLink: function(label, query, groups, func, popupFunc) {
// the main link calls back with a prompted reason
var mainLink = this.promptLink(label, query, func);
if (!groups) return mainLink;

// optional parameter
if (!popupFunc) popupFunc = func;
var popup = new PopupMenu(popupFunc);

// setup groups of items
for (var i=0; i<groups.length; i++) {
var group = groups[i];
if (i != 0) popup.separator();
for (var j=0; j<group.length; j++) {
var preset = group[j];
popup.item(preset, preset);
}
}

new PopupSource(mainLink, popup);
return mainLink;
},

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

/** create an action link calling a function on click */
functionLink: function(label, func) {
var a = document.createElement("a");
a.className = "link-function";
a.onclick = func;
a.textContent = label;
return a;
},

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

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

/** create a link to an URL within the current list item */
urlLink: function(label, url) {
var a = document.createElement("a");
a.href = url;
a.textContent = label;
return a;
},
};

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

/** uses a ProgressArea 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);

// the mainDiv is a singleton
var mainDiv = $('progress-global');
if (mainDiv === null) {
mainDiv = document.createElement("div");
mainDiv.id = 'progress-global';
mainDiv.className = "progress-global";
DOM.pasteBefore($('bodyContent'), mainDiv);
}
mainDiv.appendChild(outerDiv);

this.headerDiv = headerDiv;
this.bodyDiv = bodyDiv;
this.outerDiv = outerDiv;

this.timeout = null;
}
ProgressArea.prototype = {
/** display a header text */
header: function(content) {
this.unfade();
DOM.removeChildren(this.headerDiv);
DOM.pasteEnd(this.headerDiv, content);
},

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

/** destructor, called by fade */
destroy: function() {
DOM.removeNode(this.outerDiv);
},

/** fade out */
fade: function() {
this.timeout = setTimeout(this.destroy.bind(this), ProgressArea.cfg.fadeTime);
},

/** inihibit fade */
unfade: function() {
if (this.timeout != null) {
clearTimeout(this.timeout);
this.timeout = null;
}
}
};
ProgressArea.cfg = {
fadeTime: 750, // fade delay in millis
};

//======================================================================
//## lib/ui/FeedbackLink.js

/** implements Feedback to change an ActionLink's link-running class */
function FeedbackLink(link) {
this.link = link;
}
FeedbackLink.prototype = {
job: function(s) { },
work: function(s) { DOM.addClass(this.link, "link-running"); },
success: function(s) { DOM.removeClass(this.link, "link-running"); },
failure: function(s) { },
};

//======================================================================
//## lib/ui/FeedbackArea.js

/** implements Progress delegating to a ProgressArea */
function FeedbackArea() {
this.area = new ProgressArea();
}
FeedbackArea.prototype = {
// HACK: used when the ProgressArea should be used after success
//fade: function() { this.area.fade(); },
unfade: function() { this.area.unfade(); },

job: function(s) { this.area.header(s); },
work: function(s) { this.area.body(s); },
success: function(s) { this.area.body(s);
this.area.fade(); },
failure: function(s) { this.area.body(s); },
};

//======================================================================
//## lib/ui/Background.js

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

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

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

/** create a portlet which has to be initialized with either createNew or useExisting */
function Portlet(id, title, rows, withoutPBody) {
this.outer = document.createElement("div");
this.outer.id = id;
this.outer.className = "portlet";

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

this.body = document.createElement("div");
this.body.className = "pBody";

this.outer.appendChild(this.header);
this.outer.appendChild(this.body);
}

this.ul = null;
this.li = null;
this.canLabel = {};
this.render(rows);

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

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

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

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

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

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

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

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

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

/**
* render an array of arrays of links.
* the outer array may contains strings to steal list items
* null items in the outer array or inner are legal and skipped
* withoutPBody is optional
*/
createPortlet: function(id, title, rows, withoutPBody) {
var portlet = new Portlet(id, title, rows, withoutPBody);
this.preparedPortlets.push(portlet);
return portlet;
},

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

/** adds a div with the site name at the top of the sidebar */
insertSiteName: function() {
var a = this.siteNameLink();
var heading = DOM.fetch('p-search', "h5", null, 0);
DOM.removeChildren(heading);
heading.appendChild(a);
},
/** creates a link displaying the site name and linking to the main page */
siteNameLink: function() {
var name = document.getElementsByTagName("link")[1].title;
var a = document.createElement("a");
a.id = "siteName";
a.textContent = name;
a.href = Wiki.site;
return a;
},
};
SideBar.msg = {
show: "show",
hide: "hide",
};

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

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

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

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

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

var msg = ActionHistory.msg;

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

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

var lis = DOM.fetch('pagehistory', "li");
if (!lis) return;
for (var i=0; i<lis.length; i++) {
addLink(lis[i]);
}
},
};
ActionHistory.msg = {
edit: "edit",
restored: "revert to revision by ",
};

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

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

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

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

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

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

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

// get version date
var dateP = ActionDiff.cfg.versionExtractRE(a0.textContent);
var date = dateP ? dateP[1] : null;

// get version user
var user = a2.parentNode.nodeName != "STRONG"
? a2.textContent // not a1!
: a3.textContent;

// add restore version link
function done() {
window.location.href = Wiki.encodeURL({
title: Page.title,
action: "history",
});
}
var summary = msg.restored + " " + user + " " + date;
var restore = FastRestore.linkRestore(Page.title, oldid, summary, done);
DOM.pasteBefore(a1, [ restore, " | "]);
}
extend("diff-ntitle");
extend("diff-otitle");
},
};
ActionDiff.msg = {
restored: "restored to revision by ",
};
ActionDiff.cfg = {
// TODO: hardcoded lang_de OR lang_en
versionExtractRE: /(?:Version vom|Revision as of) (.*)/,
};

//======================================================================
//## app/extend/Special.js

/** dispatcher for Specialpages */
Special = {
/** dispatches calls to Special* objects */
init: function() {
var name = Page.whichSpecial();
if (!name) return;

var feature = window["Special" + name];
if (feature && feature.init) {
feature.init();
}

var elements = Special.cfg.autoSubmitElements[name];
if (elements) {
// TODO: HACK: we need the button here -- why?
var withButton = name == "Watchlist";
this.autoSubmit(document.forms[0], elements, withButton);
}
},

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

function change() { form.submit(); }
for (var i=0; i<elementNames.length; i++) {
var element = elements[elementNames[i]];
if (!element) continue;
element.onchange = change;
}

if (leaveSubmitAlone) return;
var todo = [];
for (var i=0; i<elements.length; i++) {
var element = elements[i];
if (element.type == "submit") todo.push(element);
}
for (var i=0; i<todo.length; i++) {
DOM.removeNode(todo[i]);
}
},
};
Special.cfg = {
/** maps Specialpage names to the autosubmitting form elements */
autoSubmitElements: {
Allpages: [ "namespace", "nsfrom" ],
Contributions: [ "namespace" ],
Ipblocklist: [ // default action
"title",
// action=unblock
"wpUnblockAddress", "wpUnblockReason"
],
Linksearch: [ "title" ],
Listusers: [ "group", "username" ],
Log: [ "type", "user", "page" ],
Newimages: [ "wpIlMatch" ],
Newpages: [ "namespace", "username" ],
Prefixindex: [ "namespace", "nsfrom" ],
Recentchanges: [ "namespace", "invert" ],
Watchlist: [ "namespace" ],
Booksources: [ "isbn" ],
CategoryTree: [ "mode", "target" ],
Cite: [ "page" ],
Filepath: [ "file" ],
Imagelist: [ "limit" ],
MIMEsearch: [ "mime" ],
Search: [ "lsearchbox" ],
},
};

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

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

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

/** extend Special:Newpages with the content of the articles */
displayInline: function() {
var openCount = 0;

/** parse one list item, then add folding and the inline view to it */
function extendItem(li) {
// fetch data
var a = li.getElementsByTagName("a")[0];
var title = a.title;

var byteStr = li.innerHTML
.replace(SpecialNewpages.cfg.bytesExtractRE, "$1")
.replace(SpecialNewpages.cfg.bytesStripRE, "");
var bytes = parseInt(byteStr);

// make header
var header = document.createElement("div");
header.className = "folding-header";
header.innerHTML = li.innerHTML;

// make body
var body = document.createElement("div");
body.className = "folding-body";

// a FoldButton for the header
var foldButton = new FoldButton(true, function(open) {
body.style.display = open ? null : "none";
if (open && foldButton.needsLoad) {
loadContent(li);
foldButton.needsLoad = false;
}
});
foldButton.needsLoad = false;
DOM.pasteBegin(header, foldButton.button);

// add action links
DOM.pasteBegin(header, UserBookmarks.linkMark(title));
var templateTools = TemplatePage.bankAllPage(title);
if (templateTools) DOM.pasteBegin(header, templateTools);
// change listitem
li.pageTitle = title;
li.contentBytes = bytes;
li.headerDiv = header;
li.bodyDiv = body;
li.className = "folding-container";
li.innerHTML = "";
li.appendChild(header);
li.appendChild(body);

if (li.contentBytes <= SpecialNewpages.cfg.sizeLimit
&& openCount < SpecialNewpages.cfg.maxArticles) {
loadContent(li);
openCount++;
}
else {
foldButton.change(false);
foldButton.needsLoad = true;
}
}

// uses the monobook start content marker
var extractRE = /<!-- start content -->([^]*)<div class="printfooter">/;

/** load the article content and display it inline */
function loadContent(li) {
li.bodyDiv.textContent = SpecialNewpages.msg.loading;
Ajax.call({
url: Wiki.readURL(li.pageTitle, { redirect: "no" }),
doneFunc: function(source) {
var content = extractRE(source.responseText);
if (!content) throw "could not extract article content";
li.bodyDiv.innerHTML = content[1] + '<div class="visualClear" />';
// <div class="noarticletext">
}
});
}

// find article list
var ol = DOM.fetch('bodyContent', "ol", null, 0);
if (!ol) return;
ol.className = "specialNewPages";

// find article list items
var lis = DOM.fetch(ol, "li");
for (var i=0; i<lis.length; i++) {
extendItem(lis[i], i);
}
},
};
SpecialNewpages.cfg = {
maxArticles: 100,
sizeLimit: 2048,

// TODO: hardcoded lang_de OR lang_en
bytesExtractRE: /.*\[([0-9.,]+) [Bb]ytes\].*/,
bytesStripRE: /[.,]/g,
};
SpecialNewpages.msg = {
loading: "lade seite..",
};

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

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

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

/** make a sorted tables from the links */
extendLinks: function() {
var uls = DOM.fetch('bodyContent', "ul", null);
for (var i=uls.length-1; i>=0; i--) {
var ul = uls[i];
this.extendGroup(ul);
}
},

/** make a sorted table from the links of one group */
extendGroup: function(ul) {
var lis = DOM.fetch(ul, "li", null);
var lines = [];
for (var i=0; i<lis.length; i++) {
var li = lis[i];
var a = li.firstChild;
lines.push({
href: a.href,
title: a.title,
text: a.textContent,
});
}
lines.sort(function(a,b) {
return a.title < b.title ? -1
: a.title > b.title ? 1
: 0;
});
var table = document.createElement("table");
for (var i=0; i<lines.length; i++) {
var line = lines[i];
var tr = document.createElement("tr");
var td1 = document.createElement("td");
var a = document.createElement("a");
a.href = line.href;
a.title = line.title;
a.textContent = line.title.scan(Wiki.specialNS + ":");
td1.appendChild(a);
var td2 = document.createElement("td");
var text = document.createTextNode(line.text);
td2.appendChild(text);
tr.appendChild(td1);
tr.appendChild(td2);
table.appendChild(tr);
}
DOM.pasteBefore(ul, table);
DOM.removeNode(ul);
},
};

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

/** extensions for Special:Recentchanges */
SpecialRecentchanges = {
/** onload initializer */
init: function() {
FilteredEditList.filterLinks("FilteredEditList_SpecialRecentchanges");
},
};

//======================================================================
//## app/extend/SpecialRecentchangeslinked.js

/** extensions for Special:Recentchangeslinked */
SpecialRecentchangeslinked = {
/** onload initializer */
init: function() {
FilteredEditList.filterLinks("FilteredEditList_SpecialRecentchangeslinked");
},
};

//======================================================================
//## app/extend/SpecialWatchlist.js

/** extensions for Special:Watchlist */
SpecialWatchlist = {
/** onload initializer */
init: function() {
if (Page.params["edit"]) {
var spaces = this.parseNamespaces();
this.exportLink(spaces);
this.toggleLinks(spaces);
}
else if (Page.params["clear"]) {}
else {
FilteredEditList.filterLinks("FilteredEditList_SpecialWatchlist");
}
},

//------------------------------------------------------------------------------
//## edit mode

/** extend Special:Watchlist?edit=yes with a links to a wikitext and a csv version */
exportLink: function(spaces) {
var self = this;
var link = Links.functionLink("watchlist.wkp", function() {
window.location.href = "data:text/plain;charset=utf-8,"
+ encodeURIComponent(self.renderWikiText(spaces));
});
var target = DOM.fetch(document, "form", null, 0);
if (!target) return;
DOM.pasteBefore(target, [ SpecialWatchlist.msg.export1, link ]);
},

/** render lists of wikilinks */
renderWikiText: function(spaces) {
var wiki = "";
for (var i=0; i<spaces.length; i++) {
var space = spaces[i];
wiki += "== " + space.ns + " ==\n";
var links = this.parseLinks(space);
for (var j=0; j<links.length; j++) {
var link = links[j];
wiki += '*[[' + link.title + ']]'
+ (link.exists ? "" : " (new)")
+ '\n';
}
wiki += "\n";
}
return wiki;
},

//------------------------------------------------------------------------------
//## toggle-links

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

// add a header for the article namespace
var space0 = spaces[0];
space0.h2 = document.createElement("h2");
space0.h2.textContent = SpecialWatchlist.msg.article;
DOM.pasteBefore(space0.ul, space0.h2);

// add invert buttons for single namespaces
for (var i=0; i<spaces.length; i++) {
var space = spaces[i];
var button = this.toggleButton(space.ul);
//DOM.pasteAfter(space.h2.lastChild, [ ' ', button ]);
DOM.pasteAfter(space.h2, button );
}

// add gobal invert button with header
var globalHdr = document.createElement("h2");
globalHdr.textContent = SpecialWatchlist.msg.global;
var button = this.toggleButton(form);
var target = form.elements["remove"];
DOM.pasteBefore(target, [
globalHdr, button,
// TODO: ugly HACK
document.createElement("br"),
document.createElement("br"),
]);
},

/** creates a toggle button for all input children of an element */
toggleButton: function(container) {
return Links.functionLink(SpecialWatchlist.msg.invert, function() {
var inputs = container.getElementsByTagName("input");
for (var i=0; i<inputs.length; i++) {
var el = inputs[i];
if (el.type == "checkbox")
el.checked = !el.checked;
}
});
},

//------------------------------------------------------------------------------
//## list parser

parseNamespaces: function() {
var out = [];
var form = DOM.fetch(document, "form", null, 0);
var uls = DOM.fetch(form, "ul");
for (var i=0; i<uls.length; i++) {
var ul = uls[i];
var h2 = DOM.previousElement(ul);
var ns = h2 ? h2.textContent : "";
out.push({ ul: ul, h2: h2, ns: ns });
}
return out;
},

parseLinks: function(space) {
var out = [];
var lis = DOM.fetch(space.ul, "li");
for (var j=0; j<lis.length; j++) {
var li = lis[j];
var a = DOM.fetch(li, "a", null, 0);
var title = a.title;
var exists = a.className != "new"; // TODO: use hasClass
out.push({ title: title, exists: exists });
}
return out;
},
};
SpecialWatchlist.msg = {
export1: "export as WikiText: ",

invert: "Invertieren",
article: "Artikel",
global: "Alle",
};

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

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

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

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

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

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

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

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

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

/** return a link for fast logfiles access */
linkAllSpecialsPopup: function() {
function selected(userdata) {
window.location.href = Wiki.readURL(Wiki.specialTitle(userdata));
}
return this.linkAllPopup(
ForSite.msg.specialLabel,
Wiki.specialTitle("Specialpages"),
ForSite.cfg.specials,
selected);
},

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

/** returns a linkPopup */
linkAllPopup: function(linkLabel, mainPage, pages, selectFunc) {
var mainLink = Links.readLink(linkLabel, mainPage);
var popup = new PopupMenu(selectFunc);
for (var i=0; i<pages.length; i++) {
var page = pages[i];
popup.item(page, page); // the page is the userdata
}
new PopupSource(mainLink, popup);
return mainLink;
},
}
ForSite.cfg = {
/** maps sites to an Array of interesting pages */
projectPages: {
"http://ru-sib.wikipedia.org": [
[ "Vip", "Wiktionary:Vandalism in progress" ],
[ "Rfd", "Wiktionary:Requests for deletion" ],
],
"http://de.wikiversity.org": [
[ "Löschen", "Kategorie:Wikiversity:Löschen" ],
],
},

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

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

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

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

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

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

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

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

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

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


/** returns a link to a users emailpage */
linkEmail: function(user) {
return Links.readLink(ForUser.msg.email, Wiki.specialTitle("Emailuser", user));
},

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

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

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

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

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

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

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

//======================================================================
//## app/feature/FilteredEditList.js

/** filters edit lists by name/ip */
FilteredEditList = {
/** onload initializer */
filterLinks: function(cookieName) {
var bodyContent = $('bodyContent');

// tag list items with a CSS class "is-ip" or "is-named"
var uls = DOM.fetch(bodyContent, "ul", "special");
for (var i=0; i<uls.length; i++) {
var ul = uls[i];
var lis = DOM.fetch(ul, "li");
for (var j=0; j<lis.length; j++) {
var li = lis[j];
var as = DOM.fetch(li, "a", null);
// new articles do not have a previous version link
var a = as[0].previousSibling.textContent.length == 1
? as[3] : as[2];
if (IP.isV4(a.textContent)) DOM.addClass(li, "is-ip");
else DOM.addClass(li, "is-named");
}
}

/** changes the filter state */
function update(link, state) {
board.select(link);
if (state == "named") DOM.addClass( bodyContent, "hide-ip");
else DOM.removeClass(bodyContent, "hide-ip");
if (state == "ip") DOM.addClass( bodyContent, "hide-named");
else DOM.removeClass(bodyContent, "hide-named");
Cookie.set(cookieName, state);
}

/** adds a filter-change link to the switchBoard */
function action(state) {
var link = Links.functionLink(
FilteredEditList.msg.state[state],
function() { update(link, state); }
);
board.add(link);
if (state == initial) update(link, state);
}

// create state switchboard
var initial = Cookie.get(cookieName);
if (!initial) initial = "all";
var states = [ "all", "named", "ip" ];
var board = new SwitchBoard();
for (var i=0; i<states.length; i++) action(states[i]);

var target = DOM.fetch(document, "form", null, 0);
if (!target) return;
// TODO: HACK for SpecialRecentchangeslinked which does not have a form
if (target.id == "searchform") {
target = DOM.fetch($('bodyContent'), "h4", null, 0);
if (!target) return;
target = target.previousSibling;
}

// TODO: HACK to get some space
var br = document.createElement("br");
br.style.lineHeight = "30%";

DOM.pasteAfter(target, [
br,
FilteredEditList.msg.intro,
board.component
]);
},
};
FilteredEditList.msg = {
intro: "Filter: ",
state: {
all: "Alle Edits",
ip: "nur von Ips",
named: "nur von Angemeldeten",
},
};

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

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

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

/** create a li with a link in it */
function exchangeItem(target, watchable) {
var li = document.createElement("li");
li.id = watchable ? "ca-watch" : "ca-unwatch";
var label = watchable ? FastWatch.msg.watch : FastWatch.msg.unwatch;
var a = Links.functionLink(label, function() {
changeState(a, watchable);
});
DOM.addClass(a, "link-immediate");
li.appendChild(a);
target.parentNode.replaceChild(li, target);
}

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

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

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

//======================================================================
//## app/feature/TemplatePage.js

/** puts templates into the current page */
TemplatePage = {
/** return an Array of links to actions for normal pages */
bankAllPage: function(title) {
// TODO: does not make sense on other wikis
if (Wiki.site != "http://ru-sib.wikipedia.org") return null;

var msg = TemplatePage.msg;
var self = this;
return [
Links.promptLink(msg.rfv.label, msg.rfv.prompt, function(reason) { self.rfv(title, reason); }),
Links.promptLink(msg.rfd.label, msg.rfd.prompt, function(reason) { self.rfd(title, reason); }),
Links.promptLink(msg.delete.label, msg.delete.prompt, function(reason) { self.delete(title, reason); }),
];
},

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

/** puts a delete template into an article */
delete: function(title, reason) {
this.simple(title, "delete", reason);
},

/** puts an QS template into an article */
rfv: function(title, reason) {
this.enlist(title, "rfv", reason);
},

/** puts an LA template into an article */
rfd: function(title, reason) {
this.enlist(title, "rfd", reason);
},

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

/** list page on a list page */
enlist: function(title, template, listPage, reason) {
var r = Markup;
var self = this;
var feedback = new FeedbackArea();
// insert template
function phase1() {
var summary = r.template_ + template + r._template_ + reason + r._template;
var text = r.template_ + template + r._template_ + reason + r.sigapp + r._template;
var sepa = r.lf;
Editor.prependText(feedback, title, text, summary, sepa, false, phase2);
}
// add to list page
function phase2() {
var page = listPage + "/" + self.currentDate();
var text = r.h2_ + r.link_ + title + r._link + r._h2 + r.lf + reason + r.sigapp;
var summary = r.link_ + title + r._link + r.sp + r.dash + r.sp + reason;
var sepa = r.lf;
Editor.appendText(feedback, page, text, summary, sepa, true, self.maybeReloadFunc(title));
}
phase1();
},

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

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

/** returns the current date in the format the LKs are organized */
currentDate: function() {
var months = [ "Januar", "Februar", "März", "April", "Mai", "Juni", "Juli",
"August", "September", "Oktober", "November", "Dezember" ];
var now = new Date();
var year = now.getYear();
if (year < 999) year += 1900;
return now.getDate() + ". " + months[now.getMonth()] + " " + year;
},
};
TemplatePage.msg = {
rfv: {
label: "rfv",
prompt: "rfv - reason?",
},
rfd: {
label: "rfd",
prompt: "rfd - reason?",
},
delete: {
label: "delete",
prompt: "delete - reason?",
},
};

//======================================================================
//## app/feature/TemplateTalk.js

/** puts templates into user talkpages */
TemplateTalk = {
/** return an Array of links for userTalkPages or null if none exist */
bankOfficial: function(user) {
var talks = TemplateTalk.cfg.officialTalks;
return talks && talks.length != 0 && Wiki.site == "http://ru-sib.wikipedia.org"
? this.talksArray(user, talks, false, false)
: null;
},

/** return an Array of links for userTalkPages or null if none exist */
bankPersonal: function(user) {
var talks = TemplateTalk.cfg.personalTalks;
return talks && talks.length != 0
? this.talksArray(user, talks, true, true)
: null;
},

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

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

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

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

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

/** creates a function to reload the current page, if it has the given title */
maybeReloadFunc: function(title) {
return function() {
if (Page.title == title) {
window.location.href = Wiki.readURL(title);
}
}
},
};
TemplateTalk.cfg = {
// TODO: hardcoded wiktionary_en
officialTalks: [ "Welcome", "test", "test2" ],
personalTalks: [], // below User:Name/
};

//======================================================================
//## app/feature/UserPage.js

/** cares for pages in the user namespace */
UserPage = {
/** create bank of readLinks to private pages */
bankGoto: function() {
function addLink(name) {
var link = Links.readLink(name, Wiki.userNS + ":" + Wiki.user + "/" + name);
out.push(link);
}
var out = [];
var names = UserPage.cfg.pages;
if (names == null
|| names.length == 0) return null;
for (var i=0; i<names.length; i++) {
addLink(names[i]);
}
return out;
},
};
UserPage.cfg = {
pages: null, // [ "tmp", ... ]
};

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

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

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

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

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

/** add a bookmark for an arbitrary page */
arbitrary: function(remark, lemma) {
var text = "*\[\[:" + lemma + "\]\]";
if (remark) text += " " + remark;
text += "\n";
this.prepend(text);
},

/** add a bookmark on a user's bookmark page */
current: function(remark) {
var text = Markup.star;
var lemma = Page.title;
if (Page.whichSpecial()) {
// HACK: ensure the title is smushed
var temp = copyOf(Page.params);
Wiki.smush(temp, Wiki.specialPageInfo(temp.title));
lemma = temp.title;
if (temp._smushed) {
// TODO: should add smushable values, not only really smushed values
lemma += "/" + temp._smushed.value;
}
var params = {
title: lemma,
};
for (var key in temp) {
if (key == "title") continue;
if (key == "_smushed") continue;
if (temp._smushed
&& key == temp._smushed.key) continue;
params[key] = temp[key];
}
// check whether any unsmushed parameters are left
var leftUnsmushed = false;
for (var key in params) {
if (key == "title") continue;
leftUnsmushed = true;
break;
}
if (leftUnsmushed) {
text += Markup.web_ +Wiki.encodeURL(params) + Markup._web_ + lemma + Markup._web;
}
else {
text += Markup.link_ + ":" + lemma + Markup._link;
}
}
else {
var mode = "perma";
var perma = Page.perma;
if (!perma) {
var params = Page.params;
var oldid = params["oldid"];
if (oldid) {
var diff = params["diff"];
if (diff) {
mode = "diff";
if (diff == "prev"
|| diff == "next"
|| diff == "next"
|| diff == "cur") mode = diff;
else
if (diff == "cur"
|| diff == "0") mode = "cur";
perma = Wiki.encodeURL({
title: lemma,
oldid: oldid,
diff: diff,
});
}
else {
mode = "old";
perma = Wiki.encodeURL({
title: lemma,
oldid: oldid,
});
}
}
}
text += Markup.link_ + ":" + lemma + Markup._link;
if (perma) text += " <small>[" + perma + " " + mode + "]</small>";
}
if (remark) text += " " + remark;
text += Markup.lf;
this.prepend(text);
},

/** add text to the bookmarks page */
prepend: function(text) {
var feedback = new FeedbackArea();
Editor.prependText(feedback, this.pageTitle(), text, "", null, true);
},

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

//======================================================================
//## app/feature/EditWarning.js

/** displays a stop hand behind the edit link on other people's user page */
EditWarning = {
init: function() {
var name = Page.title.scan(Wiki.userNS + ":");
if (!name) return;
if (name.indexOf("/") != -1) return;
if (name == Wiki.user) return;
var ed = $('ca-edit');
if (!ed) return;
var a = DOM.fetch(ed, "a", null, 0);
if (!a) return;
a.style.background = "left url(http://upload.wikimedia.org/wikipedia/commons/thumb/f/ff/Stop_hand.png/32px-Stop_hand.png);";
},
};

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

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

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

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

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

var select = document.createElement("select");
select.id = "langSelect";
select.options[0] = new Option(Lang.msg.select, "");

var list = pLang.getElementsByTagName("a");
for (var i=0; i<list.length; i++) {
var a = list[i];
var label = a.textContent
.replace(/\s*\/.*/, "");
select.options[i+1] = new Option(label, a.href);
}

select.onchange = function() {
var selected = this.options[this.selectedIndex].value;
if (selected == "") return;
window.location.href = selected;
}

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

select: "auswählen",
};

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

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

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

SideBar.labelItems(Cactions.msg.labels);

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

// bugfix: diskussion pages lick to action=edit without a local description page
if (Wiki.site != "http://commons.wikimedia.org") {
var tab = $('ca-nstab-image');
if (tab) {
var a = tab.firstChild;
a.href = a.href.replace(/&action=edit$/, "");
}
}
},

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

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

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

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

init: function() {
var tools1 = [];
if (Page.editable) {
}
if (tools1.length == 0) tools1 = null;

var tools2 = null;
if (Page.editable) {
tools2 = TemplatePage.bankAllPage(Page.title);
}

SideBar.createPortlet(this.id, Tools.msg.title, [
tools1,
tools2,
UserBookmarks.bankView(),
UserPage.bankGoto(),
]);
},
};
Tools.msg = {
title: "Tools",
};

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

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

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

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

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

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

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

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

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

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

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

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

var ipOwner = IP.isV4(Page.owner);

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

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

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

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

/** onload hook */
function initialize() {
// user configuration
if (typeof configure == "function") configure();

// init features
Wiki.init();
Page.init();
FastWatch.init();
ActionHistory.init();
ActionDiff.init();
Special.init();
EditWarning.init();

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

// display portlets created before
SideBar.showPortlets();
// insert sitename header
SideBar.insertSiteName();
}

// loads when the DOM is complete, but in contrast to the onload event,
// this happens before any images have been loaded. this lessens GUI flicker.
document.addEventListener("DOMContentLoaded", initialize, false);

/* </nowiki></pre> */
function configure() {
Search.cfg.goNotSearch = true;
}
document.write('<link rel="stylesheet" type="text/css" href="'
document.write('<link rel="stylesheet" type="text/css" href="'
+ 'http://de.wikipedia.org/w/index.php?title=Benutzer:D/monobook/user.css'
+ 'http://de.wikipedia.org/w/index.php?title=Benutzer:D/monobook/user.css'
+ '&action=raw&ctype=text/css&dontcountme=s"></link>');
+ '&action=raw&ctype=text/css&dontcountme=s"></link>');

Revision as of 11:38, 2 Сечня 2007

// from http://de.wikipedia.org/w/index.php?title=Benutzer:D/monobook/user.js by [[:w:de:User:D]] - 
// for implementing it on your monobook, please use the document.write function as described on 
// http://de.wikipedia.org/wiki/Benutzer:D/monobook#einbinden, otherwise bug fixes an changes of the 
// source code don't work.
/* <pre><nowiki> */

//======================================================================
//## lib/util/prototypes.js 

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

/** returns the index of an element or -1 */
if (!Array.prototype.indexOf)
Array.prototype.indexOf = function(element) {
    for (var i=0; i<this.length; i++) {
        if (this[i] === element)    return i;
    }
    return -1;
}

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

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

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

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

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

/** copies an object */
function copyOf(obj) {
    var out = {};
    copy(obj, out);
    return out;
}

/** copies an object's properties */
function copy(source, target) {
    for (var key in source)
            target[key] = source[key];
}

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

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

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

    /** find the next element from el which has a given nodeName or is non-text */
    nextElement: function(el, nodeName) {
        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; }
        }
    },

    /** finds a HTMLForm or returns null */
    findForm: function(ancestor, nameOrIdOrIndex) {
        var forms   = ancestor.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;
    },

    /** returns the name or id of an element or null */
    elementNameOrId: function(element) {
        return  element.name    ? element.name
            :   element.id      ? element.id
            :   null;
    },
    
    /** whether an ancestor contains an element */
    contains: function(ancestor, element) {
        for (;;) {
            if (element == ancestor)    return true;    
            if (element == null)        return false;
            element = element.parentNode;
        }
    },
    
    //------------------------------------------------------------------------------
    //##  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);
    },

    //------------------------------------------------------------------------------
    //##  add

    /** inserts text, a node or an Array of these before a target node */
    pasteBefore: function(target, additum) {
        if (additum.constructor != Array)   additum = [ additum ];
        var parent  = target.parentNode;
        for (var i=0; i<additum.length; i++) {
            var node    = additum[i];
            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 = [ additum ];
        for (var i=0; i<additum.length; i++) {
            var node    = additum[i];
            if (node.constructor == String) node    = document.createTextNode(node);
            parent.appendChild(node);
        }
    },

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

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

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

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

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

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

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

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

    /** mouse position in document 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.parentPos(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 objects coordinates */
    parentPos: function(element) {
        var pos = { x: 0, y: 0 };
        for (;;) {
            var mode = window.getComputedStyle(element, null).position;
            if (mode == "fixed") {
                pos.x   += window.pageXOffset;
                pos.y   += window.pageYOffset;
                return pos;
            }
            var parent  = element.offsetParent;
            if (!parent)    return pos;
            pos.x   += parent.offsetLeft;
            pos.y   += parent.offsetTop;
            element = parent;
        }
    },
    
    /** moves an element to document base coordinates */
    moveElement: function(element, pos) {
        var container   = this.parentPos(element);
        element.style.left  = (pos.x - container.x) + "px";
        element.style.top   = (pos.y - container.y) + "px"; 
    },
};

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

/**
 * tries to behave similar to a Location object
 * protocol includes everything before the //
 * host     is the plain hostname
 * port     is a number or null
 * pathname includes the first slash or is null
 * hash     includes the leading # or is null
 * search   includes the leading ? or is null
 */
function Loc(urlStr) {
    var m   = this.parser(urlStr);
    if (!m) throw "cannot parse URL: " + urlStr;
    this.local      = !m[1];
    this.protocol   = m[2] ? m[2] : null;                           // http:
    this.host       = m[3] ? m[3] : null;                           // ru-sib.wikipedia.org
    this.port       = m[4] ? parseInt(m[4].substring(1)) : null;    // 80
    this.pathname   = m[5] ? m[5] : "";                             // /wiki/Test
    this.hash       = m[6] ? m[6] : "";                             // #Industry
    this.search     = m[7] ? m[7] : "";                             // ?action=edit
}
Loc.prototype = {
    /** matches a global or local URL */
    parser: /((.+?)\/\/([^:\/]+)(:[0-9]+)?)?([^#?]+)?(#[^?]*)?(\?.*)?/,

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

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

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

    /** converts the searchstring into an associative array */
    args: function() {
        if (!this.search)   return {};
        var out     = {};
        var split   = this.search.substring(1).split("&");
        for (i=0; i<split.length; i++) {
            var parts   = split[i].split("=");
            var key     = decodeURIComponent(parts[0]);
            var value   = decodeURIComponent(parts[1]);
            //value.raw = parts[1];
            out[key]    = value;
        }
        return out;
    },
};

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

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

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

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

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

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

/** ajax helper functions */
Ajax = {
    /** headers preset for POSTs */
    urlEncoded: function(charset) { return {
        "Content-Type": "application/x-www-form-urlencoded; charset=" + charset
    }},

    /** headers preset for POSTs */
    multipartFormData: function(boundary, charset) { return {
        "Content-Type": "multipart/form-data; boundary=" + boundary + "; charset=" + charset
    }},

    /** encode an Object or Array into URL parameters. */
    encodeArgs: function(args) {
        if (!args)  return "";
        var query   = "";
        for (var arg in args) {
            var key     = encodeURIComponent(arg);
            var raw     = args[arg];
            if (raw === null) continue;
            var value   = encodeURIComponent(raw.toString());
            query   += "&" + key +  "=" + value;
        }
        if (query == "")    return "";
        return query.substring(1);
    },

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

    /** create and use an XMLHttpRequest with named parameters */
    call: function(args) {
        // create client
        var client  = new XMLHttpRequest();
        // extensions
        var self    = this;
        client.args = args;
        client.getXML = function() { return self.parseXML(client.responseText); };
        client.getE4X = function() { return self.parseE4X(client.responseText); };
        // open
        client.open(
            args.method ? args.method        : "GET",
            args.url,
            args.async  ? args.async == true : true
        );
        // set headers
        if (args.headers) {
            for (var name in args.headers) {
                client.setRequestHeader(name, args.headers[name]);
            }
        }
        // handle state changes
        client.onreadystatechange = function() {
            if (args.state)     args.state(client, args);
            if (client.readyState != 4) return;
            if (args.doneFunc)  args.doneFunc(client, args);
        }
        // debug status
        client.debug = function() {
            return client.status + " " + client.statusText + "\n"
                    + client.getAllResponseHeaders() + "\n\n"
                    + client.responseText;
        }
        // and start
        client.send(args.body ? args.body : null);
        return client;
    },

    /** 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")
                throw "XML parser error: " + root.textContent;
        return doc;
    },

    /** parses a String into an e4x XML object */
    parseE4X: function(text) {
        return new XML(text.replace(/^<\?xml[^>]*>/, ""));
    },

    /** serialize an XML (e4x) or XMLDocument to a String */
    unparseXML: function(xml) {
             if (xml instanceof XMLDocument)    return new XMLSerializer().serializeToString(xml);
        else if (xml instanceof XML)            return xml.toXMLString();
        else throw "not an XML document";
    },
};

//======================================================================
//## lib/util/IP.js 

IP = {
    /** matches IPv4-like strings */
    v4RE: /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/,

    /** whether thw string denotes an IPv4-address */
    isV4: function(s) {
        var m   = this.v4RE(s);
        if (!m) return false;
        for (var i=1; i<=4; i++) {
            var byt = parseInt(m[i]);
            if (byt < 0 || byt > 255)   return false;
        }
        return true;
    },
}

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

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

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

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

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

    /** User namespace */
    userNS:     null,

    /** User_talk namespace */
    userTalkNS: null,

    /** name of the logged in user */
    user:       wgUserName,
    
    /** maps canonical special page names to localized ones */
    localSpecialPages:      null,
    
    /** maps localized special page names to canonical ones */
    canonicalSpecialPages:  null,

    /** whether user has news */
    haveNews: function() {
        return DOM.fetch('bodyContent', "div", "usermessage", 0) != null;
    },
    
    /** create a localized page title from the canonical special page name and an optional parameter */
    specialTitle: function(name, param) {
        var localName   = this.localSpecialPages[name];
        if (!localName) throw "cannot localize specialpage: " + name;
        return this.specialNS + ":" + localName
                + ( param ? "/" + param : "" );
    },

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

    /** encode parameters into an URL */
    encodeURL: function(args, shorten) {
        // TODO: localized specialpage names
        
        args    = copyOf(args);

        // Special:Randompage _requires_ smushing!
        var specialPage = this.specialPageInfo(args.title);
        if (specialPage) {
            if (shorten
            || specialPage.name == "Randompage" && specialPage.param)
                    this.smush(args, specialPage);
            else
                    this.desmush(args, specialPage);
        }

        // create start path
        var path;
        if (shorten) {
            path    = this.readPath
                    + this.fixTitle(encodeURIComponent(this.normalizeTitle(args.title)))
                            .replace(/%2b/gi, "+"); // short URLs use "+" literally
            delete args.title;                      // not needed any more
        }
        else {
            path    = this.actionPath;
        }
        path    += "?";

        // normalize title-type parameters
        var normalizeParams = this.normalizeParams(specialPage);
        for (var key in args) {
            if (key == "_smushed")  continue;

            var value   = args[key];
            if (value === null)     continue;

            var code    = encodeURIComponent(value.toString());
            if (normalizeParams[key]) {
                code    = this.fixTitle(code);
            }
            path    += encodeURIComponent(key)
                    + "=" + code + "&";
        }

        return this.site + path.replace(/[?&]$/, "");
    },

    /**
      * decode an URL or path into a map of parameters. all titles are normalized.
      * if a specialpage has a smushed parameter, it is removed from the title
      * and handled like any other parameter. additionally, a _smushed parameter
      * is added providing key and value if the smushed parameter.
      */
    decodeURL: function(url) {
        // TODO: localized specialpage names
        
        var args    = {};
        var loc     = new Loc(url);

        // readPath has the title directly attached
        if (loc.pathname != this.actionPath) {
            var read    = loc.pathname.scan(this.readPath);
            if (!read)  throw "cannot decode: " + url;
            args.title  = decodeURIComponent(read);
        }

        // decode all parameters, "+" means " "
        if (loc.search) {
            var split   = loc.search.substring(1).split("&");
            for (i=0; i<split.length; i++) {
                var parts   = split[i].split("=");
                var key     = decodeURIComponent(parts[0]);
                var code    = parts[1].replace(/\+/g, "%20");
                args[key]   = decodeURIComponent(code)
            }
        }

        if (!args.title)    throw "decode: missing page title in: " + loc;

        // normalize title-type parameters and desmush
        var specialPage     = this.specialPageInfo(args.title);
        var normalizeParams = this.normalizeParams(specialPage);
        for (var key in normalizeParams) {
            if (args[key]) {
                args[key]   = this.normalizeTitle(args[key]);
            }
        }
        if (specialPage) {
            this.desmush(args, specialPage);
        }
        return args;
    },
    
    //------------------------------------------------------------------------------
    //## private

    /**
     * replaces Special:Page?key=value with Special:Page/value 
     * the name of the specialPage must be in canonical form 
     */
    smush: function(args, specialPage) {
        delete args._smushed;
        
        // Watchlist smushes irregularly
        if (specialPage.name == "Watchlist") {
                 if (args.edit)  { args.title   += "/edit";  delete args.edit;  }
            else if (args.clear) { args.title   += "/clear"; delete args.clear; }
            return;
        }

        var smushed = this.specialSmush[specialPage.name];
        if (!smushed)   return;

        var value   = args[smushed];
        if (value || value == "") {
            args.title  += "/" + value;
            delete args[smushed];
        }
    },

    /** 
     * replaces Special:Page/value with Special:Page?key=value
     * the name of the specialPage must be in canonical form 
     */
    desmush: function(args, specialPage) {
        // Watchlist smushes irregularly
        if (specialPage.name == "Watchlist") {
            var param   = specialPage.param;
            if (!param) return;
            args[param] = "yes";
            args.title  = this.specialNS + ":" + specialPage.name;
            args._smushed = {
                key:    param,
                value:  param
            };
            return;
        }
        
        var smushed = this.specialSmush[specialPage.name];
        if (smushed && specialPage.param) {
            args.title      = this.specialNS + ":" + specialPage.localName;
            args[smushed]   = specialPage.param;
            args._smushed   = {
                key:    smushed,
                value:  specialPage.param,
            };
        }
    },

    /** returns which parameters need to be normalized */
    normalizeParams: function(specialPage) {
        var out = { title: true };
        if (!specialPage)   return out;

        var params      = this.specialTitleParam[specialPage.name];
        if (!params)    return out;

        for (var i=0; i<params.length; i++) {
            out[params[i]]  = true;
        }
        return out;
    },

    /** to the user, all titles use " " instead of "_" */
    normalizeTitle: function(title) {
        return title.replace(/_/g, " ");
    },

    /** some characters are encoded differently in titles */
    fixTitle: function(code) {
        return code.replace(/%3a/gi, ":")
                    .replace(/%2f/gi, "/")
                    .replace(/%20/gi, "_")
                    .replace(/%5f/gi, "_");
    },
    
    /** returns canonical name and optional param if title is Special:Name or Special:Name/param, else null */
    specialPageInfo: function(title) {
        title   = this.normalizeTitle(title).scan(this.specialNS + ":");
        if (title == null)  return null;
        
        var m = /(.*?)\/(.*)/(title);
        var name;
        var param;
        var localName;
        if (m) {
            name    = m[1];
            param   = m[2];
        }
        else {
            name    = title;
            param   = null;
        }
        
        var canonicalName   = this.canonicalSpecialPages[name];
        if (canonicalName) {
            localName   = name;
            name        = canonicalName;
        }
        else {
            localName   = this.localSpecialPages[name];
            if (!localName) throw "could not localize special page: " + name;
        }
        return {
            name:   name,
            param:  param,
            localName:  localName,
        };
    },

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

    /** to be called onload */
    init: function() {
        // init localized namespaces
        var nss = this.namespaces[this.language];
        if (!nss)   throw "unconfigured namespaces language: " + language;
        this.specialNS  = nss.special;      // -1
        this.userNS     = nss.user;     // 2
        this.userTalkNS = nss.userTalk; // 3
        
        // init localized specialpage names
        this.localSpecialPages  = this.specialPages[this.language];
        if (!this.localSpecialPages)    throw "unconfigured specialPages language: " + language;
        
        // init inverse mapping
        // TODO: this is slow!
        this.canonicalSpecialPages  = {};
        for (key in this.localSpecialPages) {
            var value   = this.localSpecialPages[key];
            this.canonicalSpecialPages[value]   = key;
        }
    },

    //------------------------------------------------------------------------------
    //## tables
    
    /** indexed by language */
    namespaces: {
        de: {
            special:    "Spezial",
            user:       "Benutzer",
            userTalk:   "Benutzer Diskussion",
        },
        en: {
            special:    "Special",
            user:       "User",
            userTalk:   "User talk",
        },
    },
    
    /** indexed by language */
    specialPages: {
        de: {
            "Contributions":        "Beiträge",
            "Specialpages":         "Spezialseiten",
            "Emailuser":            "E-Mail",
            "Verweisliste":         "Whatlinkshere",
            "Move":                 "Verschieben",

            "Allpages":             "Alle Seiten",
            "Userlogin":            "Anmelden",
            "CrossNamespaceLinks":  "CrossNamespaceLinks",
            
            "Mostrevisions":        "Meistbearbeitete Seiten",
            "Disambiguations":      "Begriffsklärungsverweise",
            "Listusers":            "Benutzer",
            "Wantedcategories":     "Gewünschte Kategorien",
            "Watchlist":            "Beobachtungsliste",
            "Imagelist":            "Dateien",
            "Filepath":             "Filepath",
            
            "DoubleRedirects":      "Doppelte Weiterleitungen",
            "Preferences":          "Einstellungen",
            "Wantedpages":          "Gewünschte Seiten",
            "Upload":               "Hochladen",
            "Mostlinked":           "Meistverlinkte Seiten",
            "Booksources":          "ISBN-Suche",
            "BrokenRedirects":      "Kaputte Weiterleitungen",
            "Categories":           "Kategorien",
            "CategoryTree":         "CategoryTree",
            
            "Shortpages":           "Kürzeste Seiten",
            "Longpages":            "Längste Seiten",
            "Recentchanges":        "Letzte Änderungen",
            "Ipblocklist":          "Gesperrte IPs",
            "SiteMatrix":           "SiteMatrix",
            "Log":                  "Logbuch",
            "Mostcategories":       "Meistkategorisierte Seiten",
            "Mostimages":           "Meistbenutzte Dateien",
            "Mostlinkedcategories": "Meistbenutzte Kategorien",
            
            "Newpages":             "Neue Seiten",
            "Newimages":            "Neue Dateien",
            "Unusedtemplates":      "Unbenutzte Vorlagen",
            "Uncategorizedpages":   "Nicht kategorisierte Seiten",
            "Uncategorizedimages":  "Nicht kategorisierte Dateien",
            "Uncategorizedcategories":  "Nicht kategorisierte Kategorien",
            "Prefixindex":          "Präfixindex",
            "Deadendpages":         "Sackgassenseiten",
            "Ancientpages":         "Älteste Seiten",
            
            "Export":               "Exportieren",
            "Allmessages":          "MediaWiki-Systemnachrichten",
            "Statistics":           "Statistik",
            "Search":               "Suche",
            "MIMEsearch":           "MIME-Typ-Suche",
            "Version":              "Version",
            "Unusedimages":         "Unbenutzte Dateien",
            "Unusedcategories":     "Unbenutzte Kategorien",
            "Lonelypages":          "Verwaiste Seiten",
            
            "ExpandTemplates":      "ExpandTemplates",
            "Boardvote":            "Boardvote",
            "Linksearch":           "Linksearch",
            "Listredirects":        "Weiterleitungen",
            "Cite":                 "Cite",
            "Randomredirect":       "Zufällige Weiterleitung",
            "Random":               "Zufällige Seite",
            
            "Undelete":             "Wiederherstellen",
            "Blockip":              "Sperren",
            "Unwatchedpages":       "Ignorierte Seiten",
            "Import":               "Importieren",
        },
        // en is canonical, so this is an identity mapping
        en: {
            "Contributions":        "Contributions",
            "Specialpages":         "Specialpages",
            "Emailuser":            "Emailuser",
            "Whatlinkshere":        "Whatlinkshere",
            "Move":                 "Move",
            
            "Allpages":             "Allpages",
            "Userlogin":            "Userlogin",
            "CrossNamespaceLinks":  "CrossNamespaceLinks",
            
            "Mostrevisions":        "Mostrevisions",
            "Disambiguations":      "Disambiguations",
            "Listusers":            "Listusers",
            "Wantedcategories":     "Wantedcategories",
            "Watchlist":            "Watchlist",
            "Imagelist":            "Imagelist",
            "Filepath":             "Filepath",
            
            "DoubleRedirects":      "DoubleRedirects",
            "Preferences":          "Preferences",
            "Wantedpages":          "Wantedpages",
            "Upload":               "Upload",
            "Mostlinked":           "Mostlinked",
            "Booksources":          "Booksources",
            "BrokenRedirects":      "BrokenRedirects",
            "Categories":           "Categories",
            "CategoryTree":         "CategoryTree",
            
            "Shortpages":           "Shortpages",
            "Longpages":            "Longpages",
            "Recentchanges":        "Recentchanges",
            "Ipblocklist":          "Ipblocklist",
            "SiteMatrix":           "SiteMatrix",
            "Log":                  "Log",
            "Mostcategories":       "Mostcategories",
            "Mostimages":           "Mostimages",
            "Mostlinkedcategories": "Mostlinkedcategories",
            
            "Newpages":             "Newpages",
            "Newimages":            "Newimages",
            "Unusedtemplates":      "Unusedtemplates",
            "Uncategorizedpages":   "Uncategorizedpages",
            "Uncategorizedimages":  "Uncategorizedimages",
            "Uncategorizedcategories":  "Uncategorizedcategories",
            "Prefixindex":          "Prefixindex",
            "Deadendpages":         "Deadendpages",
            "Ancientpages":         "Ancientpages",
            
            "Export":               "Export",
            "Allmessages":          "Allmessages",
            "Statistics":           "Statistics",
            "Search":               "Search",
            "MIMEsearch":           "MIMEsearch",
            "Version":              "Version",
            "Unusedimages":         "Unusedimages",
            "Unusedcategories":     "Unusedcategories",
            "Lonelypages":          "Lonelypages",
            
            "ExpandTemplates":      "ExpandTemplates",
            "Boardvote":            "Boardvote",
            "Linksearch":           "Linksearch",
            "Listredirects":        "Listredirects",
            "Cite":                 "Cite",
            "Randomredirect":       "Randomredirect",
            "Random":               "Random",
            
            "Undelete":             "Undelete",
            "Blockip":              "Blockip",
            "Unwatchedpages":       "Unwatchedpages",
            "Import":               "Import",
        },
    },

    /** some special pages can smush one parameter to the page title */
    specialSmush: {
        "Emailuser":            "target",
        "Contributions":        "target",
        "Whatlinkshere":        "target",
        "Recentchangeslinked":  "target",
        "Undelete":             "target",
        "Linksearch":           "target",
        "Newpages":             "limit",
        "Newimages":            "limit",
        "Wantedpages":          "limit",
        "Recentchanges":        "limit",
        "Allpages":             "from",
        "Prefixindex":          "from",
        "Log":                  "type",
        "Blockip":              "ip",
        "Listusers":            "group",
        "Filepath":             "file",
        "Randompage":           "namespace",
        // Contributions
        //      a smushed /newbies does not mean a user named "newbies"!
        // Randompage
        //      the namespace parameter is fake, it exists only in smushed form
        // Watchlist
        //      a smushed /edit  means ?edit=yes
        //      a smushed /clear means ?clear=yes
    },

    /** some parameters of special pages point to pages, in this case space and underscore mean the same */
    specialTitleParam: {
        "Emailuser":            [ "target"      ],
        "Contributions":        [ "target"      ],
        "Whatlinkshere":        [ "target"      ],
        "Recentchangeslinked":  [ "target"      ],
        "Undelete":             [ "target"      ],
        "Allpages":             [ "from",       ],
        "Prefixindex":          [ "from"        ],
        "Blockip":              [ "ip"          ],
        "Log":                  [ "page"        ],
        "Filepath":             [ "file"        ],
        "Randompage":           [ "namespace",  ],
    },
};

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

/** represents the current Page */
Page = {
    /** returns the canonical name of the current Specialpage or null */
    whichSpecial: function() {
        // if it contains a slash, unsmushing failed
        var name    = this.title.scan(Wiki.specialNS + ":");
        if (!name)  return null;
        // TODO: is this a good idea?
        var canonicalName   = Wiki.canonicalSpecialPages[name];
        // if (!canonicalName)  throw "could not map special page to canonical name: " + localName;
        return canonicalName ? canonicalName : name;
    },

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

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

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

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

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

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

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

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

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

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

        // wgPageName / wgTitle
        this.title      = this.params.title;

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

        var self    = this;
        (function() {
            // try User namespace
            var tmp =  self.title.scan(Wiki.userNS + ":");
            if (tmp)    self.owner  = tmp.replace(/\/.*/, "");
            if (self.owner) return;

            // try User_talk namespace
            var tmp =  self.title.scan(Wiki.userTalkNS + ":");
            if (tmp)    self.owner  = tmp.replace(/\/.*/, "");
            if (self.owner) return;

            // try some special pages
            var special = self.whichSpecial();
            if (special == "Contributions" || special == "Emailuser") {
                self.owner  = self.params.target;
            }
            else if (special == "Blockip") {
                self.owner  = self.params.ip;
            }
            else if (special == "Log" && self.params.page) {    // && self.params.type == "block"
                self.owner  = self.params.page.scan(Wiki.userNS + ":");
            }
            if (self.owner) return;

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

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

/** ajax functions for MediaWiki */
Editor = {
    //------------------------------------------------------------------------------
    //## change page content
    
    /** replace the text of a page with a replaceFunc  */
    replaceText: function(feedback, title, replaceFunc, summary, minorEdit, allowCreate, doneFunc) {
        feedback.job("change page: " + title);
        var args = {
            title:  title,
            action: "edit"
        };
        function change(v, doc) {
            if (!allowCreate && doc.getElementById('newarticletext'))   return null;
            return {
                wpSummary:      summary,
                wpMinoredit:    minorEdit,
                wpTextbox1:     replaceFunc(v.wpTextbox1.replace(/^[\r\n]+$/, "")),
            };
        }
        this.action(feedback, args, "editform", change, 200, doneFunc);
    },

    /** add text to the end of a spage, the separator is optional */
    appendText: function(feedback, title, text, summary, separator, allowCreate, doneFunc) {
        function change(s) { return concatSeparated(s, separator, text); }
        this.replaceText(feedback, title, change, summary, false, allowCreate, doneFunc);
    },

    /** add text to the start of a page, the separator is optional */
    prependText: function(feedback, title, text, summary, separator, allowCreate, doneFunc) {
        feedback.job("change page: " + title);
        var args = {
            title:  title,
            action: "edit",
            section: 0
        };
        function change(v, doc) {
            if (!allowCreate && doc.getElementById("newarticletext"))   return null;
            return {
                wpSummary:      summary,
                wpMinoredit:    false,
                wpTextbox1:     concatSeparated(text, separator, v.wpTextbox1.replace(/^[\r\n]+$/, "")),
            };
        }
        this.action(feedback, args, "editform", change, 200, doneFunc);
    },

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

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

    /** watch or unwatch a page. the doneFunc is optional */
    watchedPage: function(feedback, title, watch, doneFunc) {
        var action  = watch ? "watch" : "unwatch";
        feedback.job(action + " page: " + title);
        var url = Wiki.encodeURL({
            title:  title,
            action: action,
        });
        feedback.work("GET " + url);
        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:        url,
            doneFunc:   done,
        });
    },

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

    //------------------------------------------------------------------------------
    //## private
    
    /**
     * get a form, change it, post it.
     * the changeFunc gets the form as its first,
     * the complete document as its second parameter
     * and returns a map of changed form-fields or null to abort
     * the doneFunc is called afterwards and may be left out
     */
    action: function(feedback, actionArgs, formName, changeFunc, expectedPostStatus, doneFunc) {
        function phase1() {
            var url = Wiki.encodeURL(actionArgs);
            feedback.work("GET " + url);
            Ajax.call({
                method:     "GET",
                url:        url,
                doneFunc:   phase2,
            });
        }
        function phase2(source) {
            var expectedGetStatus   = 200;
            if (expectedGetStatus && source.status != expectedGetStatus) {
                feedback.failure(source.status + " " + source.statusText);
                return;
            }

            var doc     = source.getXML();
            
            var form    = DOM.findForm(doc, formName);
            if (form === null) { 
                feedback.failure("missing form: " + formName); 
                return; 
            }

            var url     = form.action;
            var data    = self.changedForm(doc, form, changeFunc);
            if (data == null) {
                feedback.failure("aborted");
                return;
            }
            
            var headers = Ajax.urlEncoded("UTF-8");
            var body    = Ajax.encodeArgs(data);
            
            feedback.work("POST " + url);
            Ajax.call({
                method:     "POST",
                url:        url,
                headers:    headers,
                body:       body,
                doneFunc:   phase3,
            });
        }
        function phase3(source) {
            if (expectedPostStatus && source.status != expectedPostStatus) {
                feedback.failure(source.status + " " + source.statusText);
                return;
            }
            feedback.success("done");
            if (doneFunc)   doneFunc();
        }
        var self    = this;
        phase1();
    },

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

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

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

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

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

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

//======================================================================
//## lib/core/WikiLink.js 

/** the label is optional */
function WikiLink(title, label) {
    this.title  = title;
    this.label  = label;
}
WikiLink.prototype = {
    /** omits the label if it equals the title */
    toString: function() {
        return this.label && this.label != this.title
                ? "[[" + this.title + "|" + this.label + "]]"
                : "[[" + this.title + "]]";
    },
};

/** returns an Array of all WikiLinks contained in a String */
WikiLink.parseAll = function(s) {
    // constructed here with /g so e can use exec multiple times
    var re  = /\[\[[ \t]*([^\]|]+?)[ \t]*(?:\|[ \t]*([^\]]+?)[ \t]*)?\]\]/g;
    var out = [];
    for (;;) {
        var m   = re.exec(s);
        if (!m) break;
        var link    = new WikiLink(m[1], m[2]);
        
        out.push(link);
    }
    return out;
};

//======================================================================
//## lib/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;
}

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

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

/** contains a number of on/off-switches */
function SwitchBoard() {
    this.knobs  = [];
    this.board  = document.createElement("span");
    this.board.className    = "switch-board";

    // public
    this.component  = this.board;
}
SwitchBoard.prototype = {
    /** add a knob and set its className */
    add: function(knob) {
        DOM.addClass(knob, "switch-knob");
        DOM.addClass(knob, "switch-off");
        this.knobs.push(knob);
        this.board.appendChild(knob);
    },

    /** selects a single knob */
    select: function(knob) {
        this.changeAll(false);
        this.change(knob, true);
    },

    /** changes selection state of one knob */
    change: function(knob, selected) {
        if (selected)   DOM.replaceClass(knob, "switch-off", "switch-on");
        else            DOM.replaceClass(knob, "switch-on", "switch-off");
    },

    /** changes selection state of all knobs */
    changeAll: function(selected) {
        for (var i=0; i<this.knobs.length; i++) {
            this.change(this.knobs[i], selected);
        }
    },
};

//======================================================================
//## lib/ui/Floater.js 

/** a Floater is a small area floating over the document */
function Floater(id, limited) {
    this.limited    = limited;
    
    // public
    this.canvas = document.createElement("div");
    this.canvas.id          = id;
    this.canvas.className   = "floater";
    
    // shortcut
    this.style  = this.canvas.style;
    
    // attaching to a node below body leads to clipping: overflow:visible maybe?
    //this.source.appendChild(this.canvas);
    document.body.appendChild(this.canvas);
    Floater.instances.push(this);
    this.style.zIndex   = this.minimumZ + Floater.instances.length - 1;
}
Floater.prototype = {
    /** z-index for the lowest Floater */
    minimumZ: 1000,

    /** removes this Floater from the view */
    destroy: function() {
        Floater.instances.remove(this);
        // TODO: change all other fields like it was removed
        document.body.removeChild(this.canvas);
    },
    
    /** locates the div near a mouse position */
    locate: function(pos) {
        // helps with https://bugzilla.mozilla.org/show_bug.cgi?id=324819
        // display is necessary for position, visibility is not
        this.style.display  = "block";
        if (this.limited)   pos = this.limit(pos);
        DOM.moveElement(this.canvas, pos);
    },
    
    /** limits canvas position to the window */
    limit: function(pos) {
        var min     = DOM.minPos();
        var max     = DOM.maxPos();
        var size    = DOM.elementSize(this.canvas);
        
        // HACK: why does the menu go too far to the right without this?
        size.x  += 16;
        pos = { x: pos.x, y: pos.y };
        
        if (pos.x < min.x)          pos.x   = min.x;
        if (pos.y < min.y)          pos.y   = min.y;
        if (pos.x + size.x > max.x) pos.x = max.x - size.x;
        if (pos.y + size.y > max.y) pos.y = max.y - size.y;
        return pos;
    },
    
    /** returns the current location */
    location: function() {
        // display is necessary for position, visibility is not
        this.style.display  = "block";
        return DOM.elementPos(this.canvas);
    },
    
    /** displays the div */
    show: function() {
        this.style.display      = "block";
        this.style.visibility   = "visible";
    },
    
    /** hides the div */
    hide: function() {
        this.style.display      = "none";
        this.style.visibility   = "hidden";
    },
    
    /** raises the div above all other Floaters */
    raise: function() {
        var all = Floater.instances;
        var idx = all.indexOf(this);
        if (idx == -1)  return;
        all.splice(idx, 1);
        all.push(this);
        for (var i=idx; i<all.length; i++) {
            all[i].style.zIndex = i + this.minimumZ;
        }
    },
    
    /** lower the div blow all other Floaters */
    lower: function() {
        var all = Floater.instances;
        var idx = all.indexOf(this);
        if (idx == -1)  return;
        all.splice(idx, 1);
        all.unshift(this);
        for (var i=idx; i>= 0; i++) {
            all[i].style.zIndex = i + this.minimumZ;
        }
    },
};

/** all instances z-ordered starting with the lowest */
Floater.instances   = [];

//======================================================================
//## lib/ui/PopupMenu.js 

/** a PopupMenu display a number of items and call a selectFunc when one of the items is selected */ 
function PopupMenu(selectFunc) {
    this.selectFunc = selectFunc;
    this.floater    = new Floater(null, true);
    this.canvas     = this.floater.canvas;
    DOM.addClass(this.canvas, "popup-menu-window");
    this.canvas.onmouseup   = this.maybeSelectItem.bind(this);
}
PopupMenu.prototype = {
    /** removes this menu */
    destroy: function() {
        this.canvas.onmouseup   = null;
        this.floater.destroy();
    },
    
    /** opens at a given position */
    showAt: function(pos) {
        this.floater.locate(pos);
        this.floater.raise();
        this.floater.show();
    },
    
    /** closes the menu */
    hide: function() {
        this.floater.hide();
    },
    
    /** adds an item, its userdata will be supplied to the selectFunc */
    item: function(label, userdata) {
        var item    = document.createElement("div");
        item.className      = "popup-menu-item";
        item.textContent    = label;
        item.userdata       = userdata;
        this.canvas.appendChild(item);
    },

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

    /** calls the selectFunc with the userData of the selected item */
    maybeSelectItem: function(ev) {
        var target  = ev.target;
        for (;;) {
            if (DOM.hasClass(target, "popup-menu-item")) {
                if (this.selectFunc) {
                    this.selectFunc(target.userdata);
                }
                return;
            }
            target  = target.parentNode;
            if (!target)    return;
        }
    },
};

//======================================================================
//## lib/ui/PopupSource.js 

/** makes a source open a Floater as context-menu */
function PopupSource(source, menu) {
    this.source = source;
    this.menu   = menu;
    DOM.addClass(source, "popup-source");
    source.oncontextmenu    = this.contextMenu.bind(this);
    this.boundMouseUp       = this.mouseUp.bind(this);
    document.addEventListener("mouseup", this.boundMouseUp, false);
}
PopupSource.prototype = {
    mouseupCloseDelay: 250,
    
    /** removes all listeners */
    destroy: function() {
        DOM.removeClass(this.source, "popup-source");
        this.source.oncontextmenu   = null;
        document.removeEventListener("mouseup", this.boundMouseUp, false);
    },
    
    /** opens the Floater near the mouse cursor */
    contextMenu: function(ev) {
        if (ev.target != this.source)   return;
        
        var mouse   = DOM.mousePos(ev);
        // so the document does not get a mouseup shortly after
        mouse.x ++;
        this.menu.showAt(mouse);

        // delay closing so the popup stays open after a short klick
        this.abortable  = false;
        var self        = this;
        window.setTimeout(
                function() { self.abortable = true; }, 
                this.mouseupCloseDelay);
        
        // old-style, stop propagation
        return false;
    },
    
    /** closes the Floater except within a short time after opening */
    mouseUp: function(ev) {
        if (this.abortable) {
            this.menu.hide();
        }
    },
};

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

/** creates links to action functions and pages */
Links = {
    /**
     * create an action link which
     * - onclick queries a text or
     * - oncontextmenu opens a popup with default texts
     * and calls a single-argument function with it.
     *
     * groups is an Array of Arrays of preset reason Strings,
     * a separator is placed between rows. null is allowed to
     * disable the popup.
     *
     * the popupFunc is optional, when it's given it's called instead
     * of the func for a popup reason, but not for manual input
     */
    promptPopupLink: function(label, query, groups, func, popupFunc) {
        // the main link calls back with a prompted reason
        var mainLink    = this.promptLink(label, query, func);
        if (!groups)    return mainLink;

        // optional parameter
        if (!popupFunc) popupFunc   = func;
        var popup       = new PopupMenu(popupFunc);

        // setup groups of items
        for (var i=0; i<groups.length; i++) {
            var group   = groups[i];
            if (i != 0) popup.separator();
            for (var j=0; j<group.length; j++) {
                var preset  = group[j];
                popup.item(preset, preset);
            }
        }

        new PopupSource(mainLink, popup);
        return mainLink;
    },

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

    /** create an action link calling a function on click */
    functionLink: function(label, func) {
        var a   = document.createElement("a");
        a.className     = "link-function";
        a.onclick       = func;
        a.textContent   = label;
        return a;
    },

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

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

    /** create a link to an URL within the current list item */
    urlLink: function(label, url) {
        var a   = document.createElement("a");
        a.href          = url;
        a.textContent   = label;
        return a;
    },
};

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

/** uses a ProgressArea 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);

    // the mainDiv is a singleton
    var mainDiv     = $('progress-global');
    if (mainDiv === null) {
        mainDiv = document.createElement("div");
        mainDiv.id          = 'progress-global';
        mainDiv.className   = "progress-global";
        DOM.pasteBefore($('bodyContent'), mainDiv);
    }
    mainDiv.appendChild(outerDiv);

    this.headerDiv  = headerDiv;
    this.bodyDiv    = bodyDiv;
    this.outerDiv   = outerDiv;

    this.timeout    = null;
}
ProgressArea.prototype = {
    /** display a header text */
    header: function(content) {
        this.unfade();
        DOM.removeChildren(this.headerDiv);
        DOM.pasteEnd(this.headerDiv, content);
    },

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

    /** destructor, called by fade */
    destroy: function() {
        DOM.removeNode(this.outerDiv);
    },

    /** fade out */
    fade: function() {
        this.timeout    = setTimeout(this.destroy.bind(this), ProgressArea.cfg.fadeTime);
    },

    /** inihibit fade */
    unfade: function() {
        if (this.timeout != null) {
            clearTimeout(this.timeout);
            this.timeout    = null;
        }
    }
};
ProgressArea.cfg = {
    fadeTime:   750,    // fade delay in millis
};

//======================================================================
//## lib/ui/FeedbackLink.js 

/** implements Feedback to change an ActionLink's link-running class */
function FeedbackLink(link) {
    this.link   = link;
}
FeedbackLink.prototype = {
    job:        function(s) { },
    work:       function(s) { DOM.addClass(this.link, "link-running");      },
    success:    function(s) { DOM.removeClass(this.link, "link-running");   },
    failure:    function(s) { },
};

//======================================================================
//## lib/ui/FeedbackArea.js 

/** implements Progress delegating to a ProgressArea */
function FeedbackArea() {
    this.area   = new ProgressArea();
}
FeedbackArea.prototype = {
    // HACK: used when the ProgressArea should be used after success
    //fade:     function() { this.area.fade();      },
    unfade:     function() { this.area.unfade();    },

    job:        function(s) { this.area.header(s);  },
    work:       function(s) { this.area.body(s);    },
    success:    function(s) { this.area.body(s);
                                this.area.fade();   },
    failure:    function(s) { this.area.body(s);    },
};

//======================================================================
//## lib/ui/Background.js 

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

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

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

/** create a portlet which has to be initialized with either createNew or useExisting */
function Portlet(id, title, rows, withoutPBody) {
    this.outer  = document.createElement("div");
    this.outer.id           = id;
    this.outer.className    = "portlet";

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

        this.body   = document.createElement("div");
        this.body.className = "pBody";

        this.outer.appendChild(this.header);
        this.outer.appendChild(this.body);
    }

    this.ul     = null;
    this.li     = null;
    this.canLabel   = {};
    this.render(rows);

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

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

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

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

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

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

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

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

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

    /**
     * render an array of arrays of links.
     * the outer array may contains strings to steal list items
     * null items in the outer array or inner are legal and skipped
     * withoutPBody is optional
     */
    createPortlet: function(id, title, rows, withoutPBody) {
        var portlet = new Portlet(id, title, rows, withoutPBody);
        this.preparedPortlets.push(portlet);
        return portlet;
    },

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

    /** adds a div with the site name at the top of the sidebar */
    insertSiteName: function() {
        var a       = this.siteNameLink();
        var heading = DOM.fetch('p-search', "h5", null, 0);
        DOM.removeChildren(heading);
        heading.appendChild(a);
    },
    
    /** creates a link displaying the site name and linking to the main page */
    siteNameLink: function() {
        var name        = document.getElementsByTagName("link")[1].title;
        var a           = document.createElement("a");
        a.id            = "siteName";
        a.textContent   = name;
        a.href          = Wiki.site;
        return a;
    },
};
SideBar.msg = {
    show:   "show",
    hide:   "hide",
};

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

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

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

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

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

            var msg = ActionHistory.msg;

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

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

        var lis = DOM.fetch('pagehistory', "li");
        if (!lis)   return;
        for (var i=0; i<lis.length; i++) {
            addLink(lis[i]);
        }
    },
};
ActionHistory.msg = {
    edit:       "edit",
    restored:   "revert to revision by ",
};

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

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

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

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

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

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

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

            // get version date
            var dateP       = ActionDiff.cfg.versionExtractRE(a0.textContent);
            var date        = dateP  ? dateP[1]  : null;

            // get version user
            var user    = a2.parentNode.nodeName != "STRONG"
                        ? a2.textContent    // not a1!
                        : a3.textContent;

            // add restore version link
            function done() {
                window.location.href    = Wiki.encodeURL({
                    title:  Page.title,
                    action: "history",
                });
            }
            var summary = msg.restored + " " + user + " " + date;
            var restore = FastRestore.linkRestore(Page.title, oldid, summary, done);
            DOM.pasteBefore(a1, [ restore, " | "]);
        }
        extend("diff-ntitle");
        extend("diff-otitle");
    },
};
ActionDiff.msg = {
    restored:   "restored to revision by ",
};
ActionDiff.cfg = {
    // TODO: hardcoded lang_de OR lang_en
    versionExtractRE:   /(?:Version vom|Revision as of) (.*)/,
};

//======================================================================
//## app/extend/Special.js 

/** dispatcher for Specialpages */
Special = {
    /** dispatches calls to Special* objects */
    init: function() {
        var name    = Page.whichSpecial();
        if (!name)      return;

        var feature = window["Special" + name];
        if (feature && feature.init) {
            feature.init();
        }

        var elements    = Special.cfg.autoSubmitElements[name];
        if (elements) {
            // TODO: HACK: we need the button here -- why?
            var withButton  = name == "Watchlist";
            this.autoSubmit(document.forms[0], elements, withButton);
        }
    },

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

        function change() { form.submit(); }
        for (var i=0; i<elementNames.length; i++) {
            var element = elements[elementNames[i]];
            if (!element)   continue;
            element.onchange    = change;
        }

        if (leaveSubmitAlone)   return;
        var todo    = [];
        for (var i=0; i<elements.length; i++) {
            var element = elements[i];
            if (element.type == "submit") todo.push(element);
        }
        for (var i=0; i<todo.length; i++) {
            DOM.removeNode(todo[i]);
        }
    },
};
Special.cfg = {
    /** maps Specialpage names to the autosubmitting form elements */
    autoSubmitElements: {
        Allpages:       [ "namespace", "nsfrom"     ],
        Contributions:  [ "namespace"               ],
        Ipblocklist:    [   // default action
                            "title",
                            // action=unblock
                            "wpUnblockAddress", "wpUnblockReason"
                        ],
        Linksearch:     [ "title"                   ],
        Listusers:      [ "group", "username"       ],
        Log:            [ "type", "user", "page"    ],
        Newimages:      [ "wpIlMatch"               ],
        Newpages:       [ "namespace", "username"   ],
        Prefixindex:    [ "namespace", "nsfrom"     ],
        Recentchanges:  [ "namespace", "invert"     ],
        Watchlist:      [ "namespace"               ],
        
        Booksources:    [ "isbn"                    ],
        CategoryTree:   [ "mode",   "target"        ],
        Cite:           [ "page"                    ],
        Filepath:       [ "file"                    ],
        Imagelist:      [ "limit"                   ],
        MIMEsearch:     [ "mime"                    ],
        Search:         [ "lsearchbox"              ],
    },
};

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

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

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

    /** extend Special:Newpages with the content of the articles */
    displayInline: function() {
        var openCount = 0;

        /** parse one list item, then add folding and the inline view to it */
        function extendItem(li) {
            // fetch data
            var a       = li.getElementsByTagName("a")[0];
            var title   = a.title;

            var byteStr = li.innerHTML
                    .replace(SpecialNewpages.cfg.bytesExtractRE, "$1")
                    .replace(SpecialNewpages.cfg.bytesStripRE,   "");
            var bytes   = parseInt(byteStr);

            // make header
            var header  =  document.createElement("div");
            header.className    = "folding-header";
            header.innerHTML    = li.innerHTML;

            // make body
            var body    = document.createElement("div");
            body.className      = "folding-body";

            // a FoldButton for the header
            var foldButton  = new FoldButton(true, function(open) {
                body.style.display  = open ? null : "none";
                if (open && foldButton.needsLoad) {
                    loadContent(li);
                    foldButton.needsLoad    = false;
                }
            });
            foldButton.needsLoad    = false;
            DOM.pasteBegin(header, foldButton.button);

            // add action links
            DOM.pasteBegin(header, UserBookmarks.linkMark(title));
            var templateTools   = TemplatePage.bankAllPage(title);
            if (templateTools)  DOM.pasteBegin(header, templateTools);
            // change listitem
            li.pageTitle    = title;
            li.contentBytes = bytes;
            li.headerDiv    = header;
            li.bodyDiv      = body;
            li.className    = "folding-container";
            li.innerHTML    = "";
            li.appendChild(header);
            li.appendChild(body);

            if (li.contentBytes <= SpecialNewpages.cfg.sizeLimit
            && openCount < SpecialNewpages.cfg.maxArticles) {
                loadContent(li);
                openCount++;
            }
            else {
                foldButton.change(false);
                foldButton.needsLoad    = true;
            }
        }

        // uses the monobook start content marker
        var extractRE   =  /<!-- start content -->([^]*)<div class="printfooter">/;

        /** load the article content and display it inline */
        function loadContent(li) {
            li.bodyDiv.textContent  = SpecialNewpages.msg.loading;
            Ajax.call({
                url:        Wiki.readURL(li.pageTitle, { redirect: "no" }),
                doneFunc:   function(source) {
                    var content = extractRE(source.responseText);
                    if (!content)   throw "could not extract article content";
                    li.bodyDiv.innerHTML    = content[1] + '<div class="visualClear" />';
                    // <div class="noarticletext">
                }
            });
        }

        // find article list
        var ol  = DOM.fetch('bodyContent', "ol", null, 0);
        if (!ol)    return;
        ol.className    = "specialNewPages";

        // find article list items
        var lis = DOM.fetch(ol, "li");
        for (var i=0; i<lis.length; i++) {
            extendItem(lis[i], i);
        }
    },
};
SpecialNewpages.cfg = {
    maxArticles:    100,
    sizeLimit:      2048,

    // TODO: hardcoded lang_de OR lang_en
    bytesExtractRE:     /.*\[([0-9.,]+) [Bb]ytes\].*/,
    bytesStripRE:       /[.,]/g,
};
SpecialNewpages.msg = {
    loading:    "lade seite..",
};

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

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

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

    /** make a sorted tables from the links */
    extendLinks: function() {
        var uls = DOM.fetch('bodyContent', "ul", null);
        for (var i=uls.length-1; i>=0; i--) {
            var ul  = uls[i];
            this.extendGroup(ul);
        }
    },

    /** make a sorted table from the links of one group */
    extendGroup: function(ul) {
        var lis     = DOM.fetch(ul, "li", null);
        var lines   = [];
        for (var i=0; i<lis.length; i++) {
            var li  = lis[i];
            var a   = li.firstChild;
            lines.push({
                href:   a.href,
                title:  a.title,
                text:   a.textContent,
            });
        }
        lines.sort(function(a,b) {
            return  a.title < b.title ? -1
                :   a.title > b.title ? 1
                :   0;
        });
        var table   = document.createElement("table");
        for (var i=0; i<lines.length; i++) {
            var line    = lines[i];
            var tr      = document.createElement("tr");
            var td1     = document.createElement("td");
            var a       = document.createElement("a");
            a.href          = line.href;
            a.title         = line.title;
            a.textContent   = line.title.scan(Wiki.specialNS + ":");
            td1.appendChild(a);
            var td2     = document.createElement("td");
            var text    = document.createTextNode(line.text);
            td2.appendChild(text);
            tr.appendChild(td1);
            tr.appendChild(td2);
            table.appendChild(tr);
        }
        DOM.pasteBefore(ul, table);
        DOM.removeNode(ul);
    },
};

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

/** extensions for Special:Recentchanges */
SpecialRecentchanges = {
    /** onload initializer */
    init: function() {
        FilteredEditList.filterLinks("FilteredEditList_SpecialRecentchanges");
    },
};

//======================================================================
//## app/extend/SpecialRecentchangeslinked.js 

/** extensions for Special:Recentchangeslinked */
SpecialRecentchangeslinked = {
    /** onload initializer */
    init: function() {
        FilteredEditList.filterLinks("FilteredEditList_SpecialRecentchangeslinked");
    },
};

//======================================================================
//## app/extend/SpecialWatchlist.js 

/** extensions for Special:Watchlist */
SpecialWatchlist = {
    /** onload initializer */
    init: function() {
        if (Page.params["edit"]) {
            var spaces  = this.parseNamespaces();
            this.exportLink(spaces);
            this.toggleLinks(spaces);
        }
        else if (Page.params["clear"]) {}
        else {
            FilteredEditList.filterLinks("FilteredEditList_SpecialWatchlist");
        }
    },

    //------------------------------------------------------------------------------
    //## edit mode

    /** extend Special:Watchlist?edit=yes with a links to a wikitext and a csv version */
    exportLink: function(spaces) {
        var self    = this;
        var link    = Links.functionLink("watchlist.wkp", function() {
            window.location.href    = "data:text/plain;charset=utf-8,"
                                    + encodeURIComponent(self.renderWikiText(spaces));
        });
        var target  = DOM.fetch(document, "form", null, 0);
        if (!target)    return;
        DOM.pasteBefore(target, [ SpecialWatchlist.msg.export1, link ]);
    },

    /** render lists of wikilinks */
    renderWikiText: function(spaces) {
        var wiki    = "";
        for (var i=0; i<spaces.length; i++) {
            var space   = spaces[i];
            wiki    += "== " + space.ns + " ==\n";
            var links   = this.parseLinks(space);
            for (var j=0; j<links.length; j++) {
                var link    = links[j];
                wiki    += '*[[' + link.title + ']]'
                        +  (link.exists ? "" : " (new)")
                        +  '\n';
            }
            wiki    += "\n";
        }
        return wiki;
    },

    //------------------------------------------------------------------------------
    //## toggle-links

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

        // add a header for the article namespace
        var space0  = spaces[0];
        space0.h2   = document.createElement("h2");
        space0.h2.textContent   = SpecialWatchlist.msg.article;
        DOM.pasteBefore(space0.ul, space0.h2);

        // add invert buttons for single namespaces
        for (var i=0; i<spaces.length; i++) {
            var space   = spaces[i];
            var button  = this.toggleButton(space.ul);
            //DOM.pasteAfter(space.h2.lastChild, [ ' ', button ]);
            DOM.pasteAfter(space.h2, button );
        }

        // add  gobal invert button with header
        var globalHdr   = document.createElement("h2");
        globalHdr.textContent   = SpecialWatchlist.msg.global;
        var button  = this.toggleButton(form);
        var target  = form.elements["remove"];
        DOM.pasteBefore(target,  [
            globalHdr, button,
            // TODO: ugly HACK
            document.createElement("br"),
            document.createElement("br"),
        ]);
    },

    /** creates a toggle button for all input children of an element */
    toggleButton: function(container) {
        return Links.functionLink(SpecialWatchlist.msg.invert, function() {
            var inputs  = container.getElementsByTagName("input");
            for (var i=0; i<inputs.length; i++) {
                var el  = inputs[i];
                if (el.type == "checkbox")
                    el.checked  = !el.checked;
            }
        });
    },

    //------------------------------------------------------------------------------
    //## list parser

    parseNamespaces: function() {
        var out     = [];
        var form    = DOM.fetch(document, "form", null, 0);
        var uls     = DOM.fetch(form, "ul");
        for (var i=0; i<uls.length; i++) {
            var ul  = uls[i];
            var h2  = DOM.previousElement(ul);
            var ns  = h2 ? h2.textContent : "";
            out.push({ ul: ul, h2: h2, ns: ns });
        }
        return out;
    },

    parseLinks: function(space) {
        var out = [];
        var lis = DOM.fetch(space.ul, "li");
        for (var j=0; j<lis.length; j++) {
            var li      = lis[j];
            var a       = DOM.fetch(li, "a", null, 0);
            var title   = a.title;
            var exists  = a.className != "new"; // TODO: use hasClass
            out.push({ title: title, exists: exists });
        }
        return out;
    },
};
SpecialWatchlist.msg = {
    export1:    "export as WikiText: ",

    invert:     "Invertieren",
    article:    "Artikel",
    global:     "Alle",
};

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

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

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

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

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

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

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

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

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

    /** return a link for fast logfiles access */
    linkAllSpecialsPopup: function() {
        function selected(userdata) {
            window.location.href    = Wiki.readURL(Wiki.specialTitle(userdata));
        }
        return this.linkAllPopup(
            ForSite.msg.specialLabel,
            Wiki.specialTitle("Specialpages"),
            ForSite.cfg.specials,
            selected);
    },

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

    /** returns a linkPopup */
    linkAllPopup: function(linkLabel, mainPage, pages, selectFunc) {
        var mainLink    = Links.readLink(linkLabel, mainPage);
        var popup       = new PopupMenu(selectFunc);
        for (var i=0; i<pages.length; i++) {
            var page    = pages[i];
            popup.item(page, page); // the page is the userdata
        }
        new PopupSource(mainLink, popup);
        return mainLink;
    },
}
ForSite.cfg = {
    /** maps sites to an Array of interesting pages */
    projectPages: {
        "http://ru-sib.wikipedia.org": [
            [   "Vip",   "Wiktionary:Vandalism in progress"            ],
            [   "Rfd",   "Wiktionary:Requests for deletion"             ],
            
        ],
        "http://de.wikiversity.org": [
            [   "Löschen",  "Kategorie:Wikiversity:Löschen" ],
        ],
    },

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

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

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

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

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

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

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

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

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

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


    /** returns a link to a users emailpage */
    linkEmail: function(user) {
        return Links.readLink(ForUser.msg.email,    Wiki.specialTitle("Emailuser", user));
    },

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

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

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

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

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

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

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

//======================================================================
//## app/feature/FilteredEditList.js 

/** filters edit lists by name/ip */
FilteredEditList = {
    /** onload initializer */
    filterLinks: function(cookieName) {
        var bodyContent = $('bodyContent');

        // tag list items with a CSS class "is-ip" or "is-named"
        var uls = DOM.fetch(bodyContent, "ul", "special");
        for (var i=0; i<uls.length; i++) {
            var ul  = uls[i];
            var lis = DOM.fetch(ul, "li");
            for (var j=0; j<lis.length; j++) {
                var li  = lis[j];
                var as  = DOM.fetch(li, "a", null);
                // new articles do not have a previous version link
                var a   = as[0].previousSibling.textContent.length == 1 
                        ? as[3] : as[2];
                if (IP.isV4(a.textContent)) DOM.addClass(li, "is-ip");
                else                        DOM.addClass(li, "is-named");
            }
        }

        /** changes the filter state */
        function update(link, state) {
            board.select(link);
            if (state == "named")   DOM.addClass(   bodyContent, "hide-ip");
            else                    DOM.removeClass(bodyContent, "hide-ip");
            if (state == "ip")      DOM.addClass(   bodyContent, "hide-named");
            else                    DOM.removeClass(bodyContent, "hide-named");
            Cookie.set(cookieName, state);
        }

        /** adds a filter-change link to the switchBoard */
        function action(state) {
            var link    = Links.functionLink(
                FilteredEditList.msg.state[state],
                function() { update(link, state); }
            );
            board.add(link);
            if (state == initial)   update(link, state);
        }

        // create state switchboard
        var initial = Cookie.get(cookieName);
        if (!initial)   initial = "all";
        var states  = [ "all", "named", "ip" ];
        var board   = new SwitchBoard();
        for (var i=0; i<states.length; i++) action(states[i]);

        var target  = DOM.fetch(document, "form", null, 0);
        if (!target)    return;
        // TODO:  HACK for SpecialRecentchangeslinked which does not have a form
        if (target.id == "searchform") {
            target  = DOM.fetch($('bodyContent'), "h4", null, 0);
            if (!target)    return;
            target  = target.previousSibling;
        }

        // TODO:  HACK to get some space
        var br  = document.createElement("br");
        br.style.lineHeight = "30%";

        DOM.pasteAfter(target, [
            br,
            FilteredEditList.msg.intro,
            board.component
        ]);
    },
};
FilteredEditList.msg = {
    intro:  "Filter: ",
    state: {
        all:    "Alle Edits",
        ip:     "nur von Ips",
        named:  "nur von Angemeldeten",
    },
};

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

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

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

        /** create a li with a link in it */
        function exchangeItem(target, watchable) {
            var li      = document.createElement("li");
            li.id       = watchable ? "ca-watch"            : "ca-unwatch";
            var label   = watchable ? FastWatch.msg.watch   : FastWatch.msg.unwatch;
            var a       = Links.functionLink(label, function() {
                changeState(a, watchable);
            });
            DOM.addClass(a, "link-immediate");
            li.appendChild(a);
            target.parentNode.replaceChild(li, target);
        }

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

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

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

//======================================================================
//## app/feature/TemplatePage.js 

/** puts templates into the current page */
TemplatePage = {
    /** return an Array of links to actions for normal pages */
    bankAllPage: function(title) {
        // TODO: does not make sense on other wikis
        if (Wiki.site != "http://ru-sib.wikipedia.org") return null;

        var msg     = TemplatePage.msg;
        var self    = this;
        return [
            Links.promptLink(msg.rfv.label,  msg.rfv.prompt,  function(reason) { self.rfv(title, reason);  }),
            Links.promptLink(msg.rfd.label,  msg.rfd.prompt,  function(reason) { self.rfd(title, reason);  }),
            Links.promptLink(msg.delete.label, msg.delete.prompt, function(reason) { self.delete(title, reason); }),
        ];
    },

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

    /** puts a delete template into an article */
    delete: function(title, reason) {
        this.simple(title, "delete", reason);
    },

    /** puts an QS template into an article */
    rfv: function(title, reason) {
        this.enlist(title, "rfv", reason);
    },

    /** puts an LA template into an article */
    rfd: function(title, reason) {
        this.enlist(title, "rfd", reason);
    },

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

    /** list page on a list page */
    enlist: function(title, template, listPage, reason) {
        var r           = Markup;
        var self        = this;
        var feedback    = new FeedbackArea();
        // insert template
        function phase1() {
            var summary = r.template_ + template + r._template_ + reason + r._template;
            var text    = r.template_ + template + r._template_ + reason + r.sigapp + r._template;
            var sepa    = r.lf;
            Editor.prependText(feedback, title,  text, summary, sepa, false, phase2);
        }
        // add to list page
        function phase2() {
            var page    = listPage + "/" + self.currentDate();
            var text    = r.h2_ + r.link_ + title + r._link + r._h2 + r.lf + reason + r.sigapp;
            var summary = r.link_ + title + r._link + r.sp + r.dash + r.sp + reason;
            var sepa    = r.lf;
            Editor.appendText(feedback, page, text, summary, sepa, true, self.maybeReloadFunc(title));
        }
        phase1();
    },

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

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

    /** returns the current date in the format the LKs are organized */
    currentDate: function() {
        var months  = [ "Januar", "Februar", "März", "April", "Mai", "Juni", "Juli",
                        "August", "September", "Oktober", "November", "Dezember" ];
        var now     = new Date();
        var year    = now.getYear();
        if (year < 999) year    += 1900;
        return now.getDate() + ". " + months[now.getMonth()] + " " + year;
    },
};
TemplatePage.msg = {
    rfv: {
        label:  "rfv",
        prompt: "rfv - reason?",
    },
    rfd: {
        label:  "rfd",
        prompt: "rfd - reason?",
    },
    delete: {
        label:  "delete",
        prompt: "delete - reason?",
    },
};

//======================================================================
//## app/feature/TemplateTalk.js 

/** puts templates into user talkpages */
TemplateTalk = {
    /** return an Array of links for userTalkPages or null if none exist */
    bankOfficial: function(user) {
        var talks   = TemplateTalk.cfg.officialTalks;
        return talks && talks.length != 0 && Wiki.site == "http://ru-sib.wikipedia.org"
                ? this.talksArray(user, talks, false, false)
                : null;
    },

    /** return an Array of links for userTalkPages or null if none exist */
    bankPersonal: function(user) {
        var talks   = TemplateTalk.cfg.personalTalks;
        return talks && talks.length != 0
                ? this.talksArray(user, talks, true, true)
                : null;
    },

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

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

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

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

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

    /** creates a function to reload the current page, if it has the given title */
    maybeReloadFunc: function(title) {
        return function() {
            if (Page.title == title) {
                window.location.href    = Wiki.readURL(title);
            }
        }
    },
};
TemplateTalk.cfg = {
    // TODO: hardcoded wiktionary_en
    officialTalks:  [ "Welcome", "test", "test2" ],
    personalTalks:  [], // below User:Name/
};

//======================================================================
//## app/feature/UserPage.js 

/** cares for pages in the user namespace */
UserPage = {
    /** create bank of readLinks to private pages */
    bankGoto: function() {
        function addLink(name) {
            var link    = Links.readLink(name, Wiki.userNS + ":" + Wiki.user + "/" + name);
            out.push(link);
        }
        var out     = [];
        var names   = UserPage.cfg.pages;
        if (names == null
        || names.length == 0)   return null;
        for (var i=0; i<names.length; i++) {
            addLink(names[i]);
        }
        return out;
    },
};
UserPage.cfg = {
    pages: null,    // [ "tmp", ... ]
};

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

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

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

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

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

    /** add a bookmark for an arbitrary page */
    arbitrary: function(remark, lemma) {
        var text    = "*\[\[:" + lemma + "\]\]";
        if (remark) text    += " " + remark;
        text        += "\n";
        this.prepend(text);
    },

    /** add a bookmark on a user's bookmark page */
    current: function(remark) {
        var text    = Markup.star;
        var lemma   = Page.title;
        if (Page.whichSpecial()) {
            // HACK: ensure the title is smushed
            var temp    = copyOf(Page.params);
            Wiki.smush(temp, Wiki.specialPageInfo(temp.title));
            lemma   = temp.title;
            
            if (temp._smushed) {
                // TODO: should add smushable values, not only really smushed values
                lemma   += "/" + temp._smushed.value;
            }
            var params  = {
                title:  lemma,
            };
            for (var key in temp) {
                if (key == "title")             continue;
                if (key == "_smushed")          continue;
                if (temp._smushed
                && key == temp._smushed.key)    continue;
                params[key] = temp[key];
            }
            
            // check whether any unsmushed parameters are left
            var leftUnsmushed   = false;
            for (var key in params) {
                if (key == "title") continue;
                leftUnsmushed   = true;
                break;
            }
            
            if (leftUnsmushed) {
                text    += Markup.web_ +Wiki.encodeURL(params) + Markup._web_ + lemma + Markup._web;
            }
            else {
                text    +=  Markup.link_ + ":" + lemma + Markup._link;
            }
        }
        else {
            var mode    = "perma";
            var perma   = Page.perma;
            if (!perma) {
                var params  = Page.params;
                var oldid   = params["oldid"];
                if (oldid) {
                    var diff    = params["diff"];
                    if (diff) {
                        mode    = "diff";
                        if (diff == "prev"
                        ||  diff == "next"
                        ||  diff == "next"
                        ||  diff == "cur")  mode    = diff;
                        else
                        if (diff == "cur"
                        ||  diff == "0")    mode    = "cur";
                        perma   = Wiki.encodeURL({
                            title:  lemma,
                            oldid:  oldid,
                            diff:   diff,
                        });
                    }
                    else {
                        mode    = "old";
                        perma   = Wiki.encodeURL({
                            title:  lemma,
                            oldid:  oldid,
                        });
                    }
                }
            }
            text += Markup.link_ + ":" + lemma + Markup._link;
            if (perma)  text    += " <small>[" + perma + " " + mode + "]</small>";
        }
        if (remark) text    += " " + remark;
        text        += Markup.lf;
        this.prepend(text);
    },

    /** add text to the bookmarks page */
    prepend: function(text) {
        var feedback    = new FeedbackArea();
        Editor.prependText(feedback, this.pageTitle(), text, "", null, true);
    },

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

//======================================================================
//## app/feature/EditWarning.js 

/** displays a stop hand behind the edit link on other people's user page */
EditWarning = {
    init: function() {
        var name    = Page.title.scan(Wiki.userNS + ":");
        if (!name)                      return;
        if (name.indexOf("/") != -1)    return;
        if (name == Wiki.user)          return;
        
        var ed  = $('ca-edit');
        if (!ed)    return;
        var a   = DOM.fetch(ed, "a", null, 0);
        if (!a)     return;
        a.style.background = "left url(http://upload.wikimedia.org/wikipedia/commons/thumb/f/ff/Stop_hand.png/32px-Stop_hand.png);";
    },
};

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

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

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

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

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

        var select  = document.createElement("select");
        select.id   = "langSelect";
        select.options[0]   = new Option(Lang.msg.select, "");

        var list    = pLang.getElementsByTagName("a");
        for (var i=0; i<list.length; i++) {
            var a       = list[i];
            var label   = a.textContent
                            .replace(/\s*\/.*/, "");
            select.options[i+1] = new Option(label, a.href);
        }

        select.onchange = function() {
            var selected    = this.options[this.selectedIndex].value;
            if (selected == "") return;
            window.location.href    = selected;
        }

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

    select: "auswählen",
};

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

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

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

        SideBar.labelItems(Cactions.msg.labels);

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

        // bugfix: diskussion pages lick to action=edit without a local description page
        if (Wiki.site != "http://commons.wikimedia.org") {
            var tab = $('ca-nstab-image');
            if (tab) {
                var a   = tab.firstChild;
                a.href  = a.href.replace(/&action=edit$/, "");
            }
        }
        
    },

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

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

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

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

    init: function() {
        var tools1  = [];
        if (Page.editable) {
        }
        if (tools1.length == 0) tools1  = null;

        var tools2  = null;
        if (Page.editable) {        
            tools2  = TemplatePage.bankAllPage(Page.title);
        }

        SideBar.createPortlet(this.id, Tools.msg.title, [
            tools1,
            tools2,
            UserBookmarks.bankView(),
            UserPage.bankGoto(),
        ]);
    },
};
Tools.msg = {
    title:  "Tools",
};

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

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

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

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

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

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

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

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

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

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

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

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

        var ipOwner = IP.isV4(Page.owner);

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

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

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

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

/** onload hook */
function initialize() {
    // user configuration
    if (typeof configure == "function") configure();

    // init features
    Wiki.init();
    Page.init();
    FastWatch.init();
    ActionHistory.init();
    ActionDiff.init();
    Special.init();
    EditWarning.init();

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

    // display portlets created before
    SideBar.showPortlets();
    
    // insert sitename header
    SideBar.insertSiteName();
}

// loads when the DOM is complete, but in contrast to the onload event, 
// this happens before any images have been loaded. this lessens GUI flicker.
document.addEventListener("DOMContentLoaded", initialize, false);

/* </nowiki></pre> */
 
 function configure() {
    Search.cfg.goNotSearch = true;
}
  
 document.write('<link rel="stylesheet" type="text/css" href="'
            + 'http://de.wikipedia.org/w/index.php?title=Benutzer:D/monobook/user.css'
            + '&action=raw&ctype=text/css&dontcountme=s"></link>');