„Benutzer:D/monobook.js“ – Versionsunterschied

Inhalt gelöscht Inhalt hinzugefügt
Keine Bearbeitungszusammenfassung
kaputt
Markierung: Geleert
Zeile 1:Zeile 1:
/* [[Benutzer:D/monobook]] ::: achtung entwicklerversion, unsupported und gefährlich ::: [[Benutzer:D/monobook.js]] */

/* <pre><nowiki> */

//======================================================================
//## util/Object.js

var jsutil = jsutil || {};

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

/** Object helper functions */
jsutil.Object = {
/** return a type-indicating string */
type: function(obj) {
return obj === null ? "null" :
obj == null ? "undefined" :
Object.prototype.toString.call(obj).match(/(\w+)\]/)[1];
},
/** returns all keys as an Array */
keys: function(obj) {
var out = [];
for (var key in obj)
if (obj.hasOwnProperty(key))
out.push(key);
return out;
},
/** returns the value behind every key */
values: function(obj) {
var out = [];
for (var key in obj)
if (obj.hasOwnProperty(key))
out.push(obj[key]);
return out;
},
/** copies an Object's properties into an new Object */
clone: function(obj) {
var out = {};
for (var key in obj)
if (obj.hasOwnProperty(key))
out[key] = obj[key];
return out;
},
/** returns an object's slots as an Array of Pairs */
toPairs: function(obj) {
var out = [];
for (var key in obj)
if (obj.hasOwnProperty(key))
out.push([key, obj[key]]);
return out;
},
/** creates an Object from an Array of key/value pairs, the last Pair for a key wins */
fromPairs: function(pairs) {
var out = {};
for (var i=0; i<pairs.length; i++) {
var pair = pairs[i];
out[pair[0]] = pair[1];
}
return out;
}//,
};

//======================================================================
//## util/Function.js

var jsutil = jsutil || {};

/** can be used to copy a function's arguments into a real Array */
jsutil.Function = {
identity: function(x) { return x; },
constant: function(c) { return function(v) { return c; }; }
};

//======================================================================
//## util/Number.js

var jsutil = jsutil || {};

/** can be used to copy a function's arguments into a real Array */
jsutil.Number = {
range: function(from, to) {
var out = [];
for (var i=from; i<to; i++) out.push(i);
return out;
}
};

//======================================================================
//## util/Text.js

var jsutil = jsutil || {};

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

//======================================================================
//## util/XML.js

var jsutil = jsutil || {};

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

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

// @depends jsutil/Object

var jsutil = jsutil || {};

/**
* 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
*/
jsutil.Loc = function(urlStr) {
var m = this.parser.exec(urlStr);
if (!m) throw new Error("cannot parse URL: " + urlStr);
this.local = !m[1];
this.protocol = m[2] ? m[2] : null; // http:
this.host = m[3] ? m[3] : null; // de.wikipedia.org
this.port = m[4] ? parseInt(m[4].substring(1)) : null; // 80
this.pathname = m[5] ? m[5] : ""; // /wiki/Test
this.hash = m[6] ? m[6] : ""; // #Industry
this.search = m[7] ? m[7] : ""; // ?action=edit
};
jsutil.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 before the pathPart */
hostPart: function() {
if (this.local) return "";
return this.protocol + "//" + this.host
+ (this.port ? ":" + this.port : "");
},

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

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

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

var jsutil = jsutil || {};

/** DOM helper functions */
jsutil.DOM = {
//------------------------------------------------------------------------------
//## events

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

//------------------------------------------------------------------------------
//## find
/** checks if obj is a DOM node */
isNode: function(obj) {
return !!(obj && obj.nodeType);
},
/** find an element in document by its id */
get: function(id) {
return document.getElementById(id);
},

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

/** whether an ancestor contains an element */
contains: function(ancestor, element) {
for (;;) {
if (element === ancestor) return true;
if (element === null) return false;
element = element.parentNode;
}
},
//------------------------------------------------------------------------------
//## single insertions
// BETTER implement something like an insertion point?
/** insert a node as the first child of the parent */
insertBegin: function(parent, node) {
parent.insertBefore(node, parent.firstChild);
},
/** insert a node as the last child of the parent */
insertEnd: function(parent, node) {
parent.appendChild(node);
},
/** insert a node before target */
insertBefore: function(target, node) {
target.parentNode.insertBefore(node, target);
},
/** insert a node after target */
insertAfter: function(target, node) {
target.parentNode.insertBefore(node, target.nextSibling);
},
//------------------------------------------------------------------------------
//## multi insertions
/** insert many nodes at the front of the children of the parent */
insertBeginMany: function(parent, nodes) {
var reference = parent.firstChild;
for (var i=0; i<nodes.length; i++) {
parent.insertBefore(nodes[i], reference);
}
},
/** insert many nodes at the end of the children of the parent */
insertEndMany: function(parent, nodes) {
for (var i=0; i<nodes.length; i++) {
parent.appendChild(nodes[i]);
}
},
/** insert many nodes before the target */
insertBeforeMany: function(target, nodes) {
var parent = target.parentNode;
for (var i=0; i<nodes.length; i++) {
parent.insertBefore(nodes[i], target);
}
},
/** insert many nodes after the target */
insertAfterMany: function(target, nodes) {
var parent = target.parentNode;
var reference = target.nextSibling;
for (var i=0; i<nodes.length; i++) {
parent.insertBefore(nodes[i], reference);
}
},
//------------------------------------------------------------------------------
//## insertion helpers
/** turn String into text node, pass through everything else */
textAsNode: function(it) {
return it.constructor === String
? document.createTextNode(it)
: it;
},
/** textAsNode lifted to an Array */
textAsNodeMany: function(its) {
var out = [];
for (var i=0; i<its.length; i++) {
out.push(this.textAsNode(its[i]));
}
return out;
},
//------------------------------------------------------------------------------
//## remove

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

/** removes all children of a node */
removeChildren: function(node) {
//while (node.lastChild) node.removeChild(node.lastChild);
node.innerHTML = "";
},
//------------------------------------------------------------------------------
//## replace
/** replace a target with a replacement node */
replaceNode: function(target, replacement) {
target.parentNode.replaceChild(replacement, target);
},
/** replace a target with many replacement nodes */
replaceNodeMany: function(target, replacements) {
var parent = target.parentNode;
for (var i=0; i<replacements.length; i++) {
parent.insertBefore(replacements[i], target);
}
parent.removeChild(target);
},
/** replace all children of a target node */
replaceChildren: function(target, replacements) {
this.removeChildren(target);
this.insertEndMany(target, replacements);
},

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

// BETTER use StringSet here
/** returns an Array of the classes of an element */
getClasses: function(element) {
var raw = (element.className || "").trim();
return raw ? raw.split(/\s+/) : [];
},
/** sets all classes of an element from an Array of names */
setClasses: function(element, classNames) {
element.className = classNames.join(" ");
},
/** pass the class names array to a function modifying it */
modifyClasses: function(element, func) {
this.setClasses(element, func(this.getClasses(element)));
},

/** returns whether an element has a class */
hasClass: function(element, className) {
return this.getClasses(element).contains(className);
},

/** adds a class to an element */
addClass: function(element, className) {
var set = this.getClasses(element);
var ok = !set.contains(className);
if (ok) {
set.push(className);
this.setClasses(element, set);
}
return ok;
},

/** removes a class to an element */
removeClass: function(element, className) {
var set = this.getClasses(element);
var ok = set.contains(className);
if (ok) {
set.remove(className);
this.setClasses(element, set);
}
return ok;
},

/** replaces a class in an element with another */
replaceClass: function(element, oldClassName, newClassName) {
var set = this.getClasses(element);
if (set.contains(oldClassName)) {
set.remove(oldClassName);
if (!set.contains(newClassName)) {
set.push(newClassName);
}
}
this.setClasses(element, set);
},
/** sets or unsets a class on an element */
updateClass: function(element, className, active) {
if (active) this.addClass(element, className);
else this.removeClass(element, className);
},
/** switches between two different classes */
switchClass: function(element, condition, trueClassName, falseClassName) {
if (condition) this.replaceClass(element, falseClassName, trueClassName);
else this.replaceClass(element, trueClassName, falseClassName);
},

//------------------------------------------------------------------------------
//## position
/** analog to element.scrollTop, but from the bottom */
getScrollBottom: function(element) {
return element.scrollHeight - element.offsetHeight - element.scrollTop;
},

/** analog to element.scrollTop, but from the bottom */
setScrollBottom: function(element, scrollBottom) {
element.scrollTop = element.scrollHeight - element.offsetHeight - scrollBottom;
},

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

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

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

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

var jsutil = jsutil || {};

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

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

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

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

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

var jsutil = jsutil || {};

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

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

var jsutil = jsutil || {};

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

//======================================================================
//## util/ext/function.js

/**
* call this thunk after some millis.
* optionally call the given continuation with the result afterwards.
* returns an object with an cancel method to prevent this thunk from being called.
* the cancel method returns whether cancellation was successful
*/
Function.prototype.callAfter = function(millis, continuation) {
var self = this;
var running = false;
function execute() {
running = true;
var out = self.call();
if (continuation) continuation(out);
}
var timer = window.setTimeout(execute, millis);
function cancel() {
window.clearTimeout(timer);
return !running;
}
return {
cancel: cancel
};
};

//======================================================================
//## util/ext/array.js

//------------------------------------------------------------------------------
//## mutating

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

//------------------------------------------------------------------------------
//## not mutating

/** returns a new Array without the given element */
Array.prototype.cloneRemove = function(element) {
var index = this.indexOf(element);
if (index === -1) return this;
var out = [].concat(this);
out.splice(index, 1);
return out;
};

/** filter with an inverted predicate */
Array.prototype.filterNot = function(predicate, thisVal) {
var len = this.length;
var out = new Array();
for (var i=0; i<len; i++) {
var it = this[i];
if (!predicate.call(thisVal, it, i, this)) {
out.push(it);
}
}
return out;
};

/** zip with another array */
Array.prototype.zip = function(that) {
var thisLen = this.length;
var thatLen = that.length;
var out = new Array();
for (var i=0; i<thisLen && i<thatLen; i++) {
out.push([ this[i], that[i] ]);
}
return out;
};

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

/** two partitions in a 2-element Array, first the partition where the predicate returned true */
Array.prototype.partition = function(predicate) {
var yes = [];
var no = [];
for (var i=0; i<this.length; i++) {
var item = this[i];
(predicate(item) ? yes : no).push(item);
}
return [ yes, no ];
};

/** flatten an Array of Arrays into a simple Array */
Array.prototype.flatten = function() {
var out = [];
for (var i=0; i<this.length; i++) {
out = out.concat(this[i]);
}
return out;
};

/** map every element to an Array and concat the resulting Arrays */
Array.prototype.flatMap = function(func, thisVal) {
var out = [];
for (var i=0; i<this.length; i++) {
out = out.concat(func.call(thisVal, this[i], i, this));
}
return out;
};

/** returns a copy of this Array */
Array.prototype.clone = function() {
return [].concat(this);
};

/** returns a reverse copy of this Array */
Array.prototype.cloneReverse = function() {
var out = [].concat(this);
out.reverse();
return out;
};

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

/** optionally insert an element between every two elements and boundaries */
Array.prototype.inject = function(func, thisVal) {
var out = [];
for (var i=0; i<=this.length; i++) {
var a = i > 0 ? this[i-1] : null;
var b = i < this.length ? this[i] : null;
var tmp = func.call(thisVal, a, b);
if (tmp !== null) out.push(tmp);
if (i < this.length) out.push(this[i]);
}
return out;
};
/** use a function to extract keys and build an Object */
Array.prototype.mapBy = function(keyFunc) {
var out = {};
for (var i=0; i<this.length; i++) {
var item = this[i];
out[keyFunc(item)] = item;
}
return out;
};

//======================================================================
//## util/ext/string.js

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

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

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

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

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

//======================================================================
//## util/fill/nonstandard.js

/** remove whitespace from the left */
if (!String.prototype.trimLeft)
String.prototype.trimLeft = function() {
return this.replace(/^\s+/gm, "");
};
/** remove whitespace from the right */
if (!String.prototype.trimRight)
String.prototype.trimRight = function() {
return this.replace(/\s+$/gm, "");
};

//======================================================================
//## util/fill/es6.js

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

/** copies an object's properties into another object */
if (!Object.assign)
Object.assign = function(target, source) {
for (var key in source) {
if (source.hasOwnProperty(key)) {
target[key] = source[key];
}
}
};

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

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

if (!Array.prototype.find)
Array.prototype.find = function(predicate, thisVal) {
var len = this.length;
for (var i=0; i<len; i++) {
var it = this[i];
if (predicate.call(thisVal, it, i, this)) return it;
}
return undefined;
};

if (!Array.prototype.findIndex)
Array.prototype.findIndex = function(predicate, thisVal) {
var len = this.length;
for (var i=0; i<len; i++) {
var it = this[i];
if (predicate.call(thisVal, it, i, this)) return i;
}
return undefined;
};
//------------------------------------------------------------------------------
//## String

/** whether this string starts with another */
if (!String.prototype.startsWith)
String.prototype.startsWith = function(s, position) {
var pos = position || 0;
return this.indexOf(s, pos) === pos;
};

/** whether this string ends with another */
if (!String.prototype.endsWith)
String.prototype.endsWith = function(s, position) {
var pos = (position || this.length) - s.length;
var idx = this.lastIndexOf(s);
return idx !== -1 && idx === pos;
};
/** whether this string contains another */
if (!String.prototype.contains)
String.prototype.contains = function(s, start) {
return this.indexOf(s, start) !== -1;
};

/** repeat this string count times */
if (!String.prototype.repeat)
String.prototype.repeat = function(count) {
if (count < 0) throw new Error("count must be greater or equal than zero");
else if (count === 0) return "";
else {
var out = "";
for (var i=0; i<count; i++) {
out += this;
}
return out;
}
};

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

/** encoding and decoding of MediaWiki URLs */
var Wiki = {
//## config
/** the current wiki site without any path */
site: null, // "http://de.wikipedia.org",
/** the protocol of the current site */
protocol: null, // "http://"
/** the current wiki domain */
domain: null, // "de.wikipedia.org",

/** language of the site */
language: null, // "de"
/** name of the logged in user */
user: null,

/** private: path to read pages */
readPath: null, // "/wiki/",

/** private: path for page actions */
actionPath: null, // "/w/index.php",
//## info
/** whether user has news */
haveNews: function() {
// li#pt-mytalk a.mw-echo-unread-notifications.mw-echo-notifications-badge
return jsutil.DOM.fetch("pt-mytalk", null, "mw-echo-alert", 0) !== null;
},
/**
* the bodyContent-equivalent for all skins.
* parent is optional, for null the document is used
*/
bodyContent: function(parent) {
parent = parent || document;
return parent.getElementById('bodyContent')
|| parent.getElementById('mw_contentholder')
|| parent.getElementById('article');
},
//## URLs
/** compute an URL in the read form without a title parameter. the args object is optional */
readURL: function(title, args) {
args = jsutil.Object.clone(args);
args.title = title;
return this.encodeURL(args, true);
},

/** encode parameters into an URL */
encodeURL: function(args, shorten) {
args = jsutil.Object.clone(args);
args.title = this.normalizeTitle(args.title);

// HACK: Special:Randompage _requires_ smushing!
var specialInfo = Special.pageInfo(args.title);
if (specialInfo) {
if (shorten
|| specialInfo.canonicalName == "Randompage" && specialInfo.smushValue)
this.smush(args, specialInfo);
else
this.desmush(args, specialInfo);
}

// create start path
var path;
if (shorten) {
// "+" means "+" here!
path = this.readPath
+ this.fixPlus(this.fixTitle(encodeURIComponent(args.title)));
// not needed any more
delete args.title;
}
else {
path = this.actionPath;
}
path += "?";

// normalize title-type parameters
var normalizeParams = ["title"];
if (specialInfo) {
normalizeParams = normalizeParams.concat(specialInfo.titleParams);
}
for (var key in args) {
var value = args[key];
if (value === null) continue;

var code = encodeURIComponent(value.toString());
if (normalizeParams.contains(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.
*/
decodeURL: function(url, titleFallback) {
var args = {};
var loc = new jsutil.Loc(url);
// TODO check protocol, host and port

// readPath has the title directly attached, "+" means "+" here!
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("=");
if (parts.length !== 2) continue;
var key = decodeURIComponent(parts[0]);
var code = parts[1].replace(/\+/g, "%20");
args[key] = decodeURIComponent(code)
}
}

if (!args.title) args.title = titleFallback;
if (!args.title) throw "decode: missing page title in: " + loc;
args.title = this.normalizeTitle(args.title);
// normalize title-type parameters and desmush
var specialInfo = Special.pageInfo(args.title);
if (specialInfo) {
var normalizeParams = specialInfo.titleParams;
for (var i=0; i<normalizeParams.length; i++) {
var key = normalizeParams[i];
var value = args[key];
if (value) args[key] = this.normalizeTitle(value);
}
this.desmush(args, specialInfo);
}
return args;
},
//## hacks
/** returns a smushed copy of a params object */
smushedParams: function(args) {
var out = jsutil.Object.clone(args);
var specialInfo = Special.pageInfo(this.normalizeTitle(args.title));
this.smush(out, specialInfo);
return out;
},
/** returns the owner for a Page */
owner: function(args) {
var self = this;
function names(title) {
if (!title) return null;
var tmp = Namespace.scan(2, title) || Namespace.scan(3, title);
if (!tmp) return null;
return tmp.replace(/\/.*/, "");
}
function special() {
var specialInfo = Special.pageInfo(self.normalizeTitle(args.title));
if (!specialInfo) return null;
var desmushed = jsutil.Object.clone(args);
self.desmush(desmushed, specialInfo);
if (specialInfo.canonicalName === "EmailUser") return desmushed.target;
if (specialInfo.canonicalName === "Contributions") return desmushed.target;
if (specialInfo.canonicalName === "DeletedContributions") return desmushed.target;
if (specialInfo.canonicalName === "BlockIP") return desmushed.ip;
if (specialInfo.canonicalName === "WhatLinksHere") return names(desmushed.target);
if (specialInfo.canonicalName === "RecentChangesLinked") return names(desmushed.target);
if (specialInfo.canonicalName === "Undelete") return names(desmushed.target);
if (specialInfo.canonicalName === "Log") return names(desmushed.page);
// TODO Userrights MovePage CheckUser UserLogin Preferences Export
return null;
}
return names(args.title) || special();
},
//------------------------------------------------------------------------------
//## private

/** replaces Special:Page?key=value with Special:Page/value */
smush: function(args, specialInfo) {
var param = specialInfo.smushParam;
if (param && args.hasOwnProperty(param)) {
args.title += "/" + args[param];
delete args[param];
}
},

/** replaces Special:Page/value with Special:Page?key=value */
desmush: function(args, specialInfo) {
var param = specialInfo.smushParam;
var value = specialInfo.smushValue;
if (param && value) {
args.title = Titles.specialPage(specialInfo.localName);
args[param] = value;
}
},

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

/** some characters are encoded differently in titles */
fixTitle: jsutil.Text.recoder([
[ "%%", "%%" ],
[ "%3a", ":" ],
[ "%3A", ":" ],
[ "%2f", "/" ],
[ "%2F", "/" ],
[ "%20", "_" ],
[ "%5f", "_" ],
[ "%5F", "_" ]//,
]),
/** in read urls the title uses literal "+" */
fixPlus: jsutil.Text.recoder([
[ "%%", "%%" ],
[ "%2b", "+" ],
[ "%2B", "+" ]//,
]),

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

/** to be called onload */
init: function() {
// for some reason the http: prefix is missing on commons
var hackServer = wgServer.replace(/^\/\//, location.protocol + "//");
this.site = hackServer;
var m = /([^\/]+\/+)(.*)/.exec(hackServer);
this.protocol = m[1];
this.domain = m[2];
this.language = wgContentLanguage;
this.user = wgUserName;
this.readPath = wgArticlePath.replace(/\$1$/, "");
this.actionPath = wgScriptPath + "/index.php";
Special.init(this.language);
}//,
};

//======================================================================
//## lib/core/Titles.js

// NOTE: named Titles instead of Title to avoid a nameclash with lupin's popups
var Titles = {
/** generates a page title. nsIndex and subPage are optional */
page: function(nsIndex, page, subPage) {
var title = page;
if (nsIndex !== null) title = Namespace.add(nsIndex, title);
if (subPage) title = title + "/" + subPage;
return title;
},
/** create a localized page title from the canonical special page name and an optional parameter */
specialPage: function(name, param) {
var specialNames = Special.pageNames(name);
if (!specialNames) throw "cannot localize specialpage: " + name;
return this.page(-1, specialNames.localName, param);
},
/** generates a user page title. subPage is optional */
userPage: function(user, subPage) {
return this.page(2, user, subPage);
},
/** generates a user talk page title. subPage is optional */
userTalkPage: function(user, subPage) {
return this.page(3, user, subPage);
}//,
};

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

/** represents the current Page */
var Page = {
/** 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: null,
/** the existing user this page belongs to, or null */
ownerExists: null,
//------------------------------------------------------------------------------
//## public
/** returns the canonical name of the current Specialpage or null */
whichSpecial: function() {
return this.specialInfo && this.specialInfo.canonicalName;
},
/** other languages of this page */
languages: function() {
var languages = [];
var pLang = jsutil.DOM.get('p-lang');
if (!pLang) return languages;
var lis = pLang.getElementsByTagName("li");
for (var i=0; i<lis.length; i++) {
var li = lis[i];
var a = li.firstChild;
if (!a) continue;
languages.push({
code: li.className.replace(/^interwiki-/, ""),
name: a.textContent,
href: a.href//,
});
}
return languages;
},
//------------------------------------------------------------------------------
//## private

/** SpecialInfo object */
specialInfo: null,
/** to be called onload */
init: function() {
this.params = Wiki.decodeURL(window.location.href, wgPageName);

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

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

this.deletable = jsutil.DOM.get('ca-delete') !== null;
this.editable = jsutil.DOM.get('ca-edit') !== null;
// #t-permalink
var tPermalink = jsutil.DOM.fetch('t-permalink', "a", null, 0);
if (tPermalink !== null) this.perma = tPermalink.href;
// title is already normalized
this.specialInfo = Special.pageInfo(this.params.title);

// standard way
this.owner = Wiki.owner(this.params);
// try to find a really existing owner
var self = this;
this.ownerExists = (function() {
var tBlockip = jsutil.DOM.fetch('t-blockip', "a", null, 0);
if (tBlockip) return Wiki.decodeURL(tBlockip.href).ip;
var tContributions = jsutil.DOM.fetch('t-contributions', "a", null, 0);
if (tContributions) return Wiki.decodeURL(tContributions.href).target;
if (!self.specialInfo) return null;
if (self.specialInfo.canonicalName !== "Contributions"
&& self.specialInfo.canonicalName !== "DeletedContributions") return null;
// block and deleted exist for admins only
// nonexisting: _ talk logs deleted
// ip: _ talk block blocklog logs deleted
// user: user talk block blocklog logs deleted
// if a blocklog-link exists, the user exists, too.
var as = jsutil.DOM.fetch('contentSub', "a");
for (var i=0; i<as.length; i++) {
var logA = as[i];
var logArgs = Wiki.decodeURL(logA.href);
var logInfo = Special.pageInfo(logArgs.title);
if (!logInfo) continue;
if (logInfo.canonicalName === "Log"
&& logArgs.type === "block") return self.params.target;
}
return null;
})();
}//,
};

//======================================================================
//## lib/core/Namespace.js

/** namespaces of the current Wiki */
var Namespace = {
// TODO check canonical
// TODO allow spaces around the colon (?)
/** the local name of a namespace */
name: function(nsIndex) {
if (!wgFormattedNamespaces.hasOwnProperty(nsIndex)) throw "unknown Namespace: " + nsIndex;
return wgFormattedNamespaces[nsIndex];
},
/** put a page into a namespace */
add: function(nsIndex, page) {
return this.name(nsIndex) + ":" + page;
},
/** calculate page title without the namespace or return null */
scan: function(nsIndex, title) {
return title.scanNoCase(this.name(nsIndex) + ":");
},
/** returns an object with internal nsIndex and page */
split: function(title) {
var split = title.split(/:/);
if (split.length > 1) {
var prefix = split[0].toLowerCase();
if (wgNamespaceIds.hasOwnProperty(prefix)) {
return {
nsIndex: wgNamespaceIds[prefix],
page: title.replace(/.*?:/, "")//,
};
}
}
return {
nsIndex: 0,
page: title//,
};
}//,
};

//======================================================================
//## lib/core/Special.js

/** special page specialities */
var Special = {
/**
* returns an information object for titles like Special:Name or Special:Name/param
* or null for unknown specialPages or non-specialPages.
*
* localName the localized name
* canonicalName the canonical name
* smushParam which parameter may be sushed to this specialPages's title
* param the value of the smushed parameter, or null if none
* titleParams an Array of parameter names for page titles
*/
pageInfo: function(normalizedTitle) {
var title = Namespace.scan(-1, normalizedTitle);
if (title === null) return null;
// split after the first slash
var m = /(.*?)\/(.*)/.exec(title);
var name;
var param;
var localName;
if (m) {
name = m[1];
param = m[2];
}
else {
name = title;
param = null;
}
// NOTE: name may be canonical or local
var names = this.pageNames(name);
if (names == null) throw "could not localize special page: " + name + " for title: " + normalizedTitle;
var localName = names.localName;
var canonicalName = names.canonicalName;
var titleParams = this.titleParams[canonicalName] || [];
var smushParam = this.smush[canonicalName];
return {
localName: localName,
canonicalName: canonicalName,
smushParam: smushParam,
smushValue: param,
titleParams: titleParams
};
},
//------------------------------------------------------------------------------
//## private
/** maps lowercased canonical special page names to localized name */
localPages: null,
/** maps lowercased localized special page names to canonical name */
canonicalPages: null,
init: function(language) {
var pages = this.pages[language];
if (!pages) throw "unconfigured pages language: " + language;
// init forward and inverse name maps
this.localPages = {};
this.canonicalPages = {};
for (key in pages) {
var value = pages[key];
this.localPages[key.toLowerCase()] = value;
this.canonicalPages[value.toLowerCase()] = key;
}
},
/** returns an object containing localName and canonicalName or null for unknown pages */
pageNames: function(name) {
var canonicalName = this.canonicalPages[name.toLowerCase()];
var localName = this.localPages[name.toLowerCase()];
if (!canonicalName && !localName) return null;
if (!canonicalName) canonicalName = this.canonicalPages[localName.toLowerCase()];
if (!localName) localName = this.localPages[canonicalName.toLowerCase()];
return {
localName: localName,
canonicalName: canonicalName//,
};
},
/** maps language to wgCanonicalSpecialPageName to wgTitle */
pages: {
de: {
// TODO "Benutzerkonto anlegen" redirects to "Anmelden/signup"
"CreateAccount": "Benutzerkonto anlegen",
"DisambiguationPages": "Begriffsklärungsseiten",
"Nearby": "In der Nähe",
"PagesWithProp": "Seiten mit Eigenschaften",
"DisambiguationPageLinks": "Begriffsklärungslinks",
"Notifications": "Benachrichtigungen",
"OAuthListConsumers": "Verbraucher auflisten",
"OAuthManageMyGrants": "Meine Berechtigungen verwalten",
"Thanks": "Danke",
"ValidationStatistics": "Sichtungsstatistik",
"ArticleFeedbackv5": "Artikelrückmeldungen v5",
"QualityOversight": "QualityOversight",
"PendingChanges": "PendingChanges",
"SimpleSurvey": "SimpleSurvey",
"ArticleFeedback": "ArticleFeedback",
"FeedbackDashboard": "FeedbackDashboard",
"ApiSandbox": "API-Spielwiese",
"PasswordReset": "Passwort neu vergeben",
"Hieroglyphs": "Hieroglyphen",
"Interwiki": "Interwikitabelle",
"ZeroRatedMobileAccess": "ZeroRatedMobileAccess",
"ChangeEmail": "E-Mail-Adresse ändern",
"Unblock": "Freigeben",
// "UploadWizard": "UploadWizard", // does not exist on de.wikipedia
"Protectedtitles": "Geschützte Titel",
"CentralAuth": "Verwaltung Benutzerkonten-Zusammenführung",
"WikiSets": "Wikigruppen",
"ComparePages": "Seiten vergleichen",
"ConfiguredPages": "ConfiguredPages",
"Revisiondelete": "Versionslöschung",
"ProblemChanges": "Seiten mit problematischen Versionen",
"PrefStats": "PrefStats",
"Mypage": "Meine Benutzerseite",
"Nuke": "Massenlöschung",
"Activeusers": "Aktive Benutzer",
"PrefSwitch": "UsabilityInitiativePrefSwitch",
// "UsabilityInitiativeOptIn": "UsabilityInitiativeOptIn",
"GlobalGroupPermissions": "Globale Gruppenrechte",
"Wantedfiles": "Gewünschte Dateien",
"Wantedtemplates": "Gewünschte Vorlagen",
"Resetpass": "Passwort ändern",
"Tags": "Markierungen",
"AbuseLog": "Missbrauchsfilter-Logbuch",
"Book": "Buch",
"UnstablePages": "Unstabile Seiten",
"AbuseFilter": "Missbrauchsfilter",
"ValidationStatistics": "Markierungsstatistik",
"GlobalBlockStatus": "Ausnahme von globaler Sperre",
"AutoLogin": "AutoLogin",
"Captcha": "Captcha",
"MergeHistory": "Versionsgeschichten vereinen",
"GlobalBlockList": "Liste globaler Sperren",
"Userrights": "Benutzerrechte",
"ReviewedPages": "Gesichtete Seiten",
"StablePages": "Konfigurierte Seiten",
"QualityOversight": "Markierungsübersicht",
"OldReviewedPages": "Seiten mit ungesichteten Versionen",
"RevisionReview": "RevisionReview",
"UnreviewedPages": "Ungesichtete Seiten",
"DepreciationOversight": "Zurückgezogene Markierungen",
"Listgrouprights": "Gruppenrechte",
"Mostlinkedtemplates": "Meistbenutzte Vorlagen",
"Uncategorizedtemplates": "Nicht kategorisierte Vorlagen",
"Gadgets": "Helferlein",
"GlobalUsers": "Globale Benutzerliste",
"ParserDiffTest": "Parser-Differenz-Test", //### the link title on Special:SpecialPages is ParserDiffTest on wp:de
"MergeAccount": "Benutzerkonten zusammenführen",
"FileDuplicateSearch": "Dateiduplikatsuche",
"DeletedContributions": "Gelöschte Beiträge",
"Contributions": "Beiträge",
"SpecialPages": "Spezialseiten",
"Emailuser": "E-Mail senden",
"Whatlinkshere": "Linkliste",
"MovePage": "Verschieben",
"CheckUser": "CheckUser",
"Recentchangeslinked": "Änderungen an verlinkten Seiten",
"Protectedpages": "Geschützte Seiten",
"Fewestrevisions": "Wenigstbearbeitete Seiten",
"Withoutinterwiki": "Fehlende Interwikis",

"Allpages": "Alle Seiten",
"Userlogin": "Anmelden",
"CrossNamespaceLinks": "Seiten mit Links in andere Namensräume",
"Mostrevisions": "Meistbearbeitete Seiten",
"Disambiguations": "Begriffsklärungsverweise",
"Listusers": "Benutzer",
"Wantedcategories": "Gewünschte Kategorien",
"Watchlist": "Beobachtungsliste",
"Listfiles": "Dateien",
"Filepath": "Dateipfad",
"DoubleRedirects": "Doppelte Weiterleitungen",
"Preferences": "Einstellungen",
"Wantedpages": "Gewünschte Seiten",
"Upload": "Hochladen",
"Mostlinked": "Meistverlinkte Seiten",
"Booksources": "ISBN-Suche",
"BrokenRedirects": "Defekte Weiterleitungen",
"Categories": "Kategorien",
"CategoryTree": "Kategorienbaum",
"Shortpages": "Kürzeste Seiten",
"LongPages": "Längste Seiten",
"Recentchanges": "Letzte Änderungen",
"Ipblocklist": "Liste der Sperren",
"SiteMatrix": "Liste der Wikimedia-Wikis",
"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": "Vorlagen expandieren",
"Boardvote": "Boardvote",
"LinkSearch": "Weblinksuche",
"Listredirects": "Weiterleitungen",
"Cite": "Zitierhilfe",
"RandomRedirect": "Zufällige Weiterleitung",
"Random": "Zufällige Seite",
"Undelete": "Wiederherstellen",
"BlockIP": "Sperren",
"UnwatchedPages": "Ignorierte Seiten",
"Import": "Importieren"//,
},
// en is (mostly) canonical, so this is almost an identity mapping
en: {
// TODO "???" redirects to "Userlogin/signup"
"CreateAccount": "CreateAccount",
"DisambiguationPages": "DisambiguationPages",
"Nearby": "Nearby",
"PagesWithProp": "PagesWithProp",
"DisambiguationPageLinks": "DisambiguationPageLinks",
"Notifications": "Notifications",
"OAuthListConsumers": "OAuthListConsumers",
"OAuthManageMyGrants": "OAuthManageMyGrants",
"Thanks": "Thanks",
"ValidationStatistics": "ValidationStatistics",
"ArticleFeedbackv5": "ArticleFeedbackv5",
"QualityOversight": "AdvancedReviewLog",
"PendingChanges": "PendingChanges",
"SimpleSurvey": "SimpleSurvey",
"ArticleFeedback": "ArticleFeedback",
"FeedbackDashboard": "FeedbackDashboard",
"ApiSandbox": "ApiSandbox",
"PasswordReset": "PasswordReset",
"Hieroglyphs": "Hieroglyphs",
"Interwiki": "Interwiki",
"ZeroRatedMobileAccess": "ZeroRatedMobileAccess",
"ChangeEmail": "ChangeEmail",
"Unblock": "Unblock",
"UploadWizard": "UploadWizard",
"CentralAuth": "CentralAuth",
"WikiSets": "WikiSets",
"ComparePages": "ComparePages",
"ConfiguredPages": "ConfiguredPages",
"Revisiondelete": "Revisiondelete",
"ProblemChanges": "ProblemChanges",
"PrefStats": "PrefStats",
"Mypage": "Mypage",
"Nuke": "Nuke",
"Activeusers": "Activeusers",
"PrefSwitch": "UsabilityInitiativePrefSwitch",
"UsabilityInitiativeOptIn": "UsabilityInitiativeOptIn",
"GlobalGroupPermissions": "GlobalGroupPermissions",
"Wantedfiles": "WantedFiles",
"Wantedtemplates": "Wantedtemplates",
"Resetpass": "ChangePassword",
"Tags": "Tags",
"AbuseLog": "AbuseLog",
"Book": "Book",
"UnstablePages": "UnstablePages",
"AbuseFilter": "AbuseFilter",
"ValidationStatistics": "ValidationStatistics",
"GlobalBlockStatus": "GlobalBlockStatus",
"AutoLogin": "AutoLogin",
"Captcha": "Captcha",
"MergeHistory": "Versionsgeschichten vereinen",
"GlobalBlockList": "GlobalBlockList",
"Userrights": "Userrights",
"ReviewedPages": "ReviewedPages",
"StablePages": "StablePages",
"QualityOversight": "QualityOversight",
"OldReviewedPages": "OldReviewedPages",
"RevisionReview": "RevisionReview",
"UnreviewedPages": "UnreviewedPages",
"DepreciationOversight": "DepreciationOversight",
"Protectedtitles": "ProtectedTitles", // attention, not equal
"Listgrouprights": "ListGroupRights", // attention, not equal
"Mostlinkedtemplates": "MostLinkedTemplates", // attention, not equal
"Uncategorizedtemplates": "UncategorizedTemplates", // attention, not equal
"Gadgets": "Gadgets",
"GlobalUsers": "GlobalUsers",
"ParserDiffTest": "ParserDiffTest",
"MergeAccount": "MergeAccount",
"FileDuplicateSearch": "FileDuplicateSearch",
"DeletedContributions": "DeletedContributions",
"Contributions": "Contributions",
"SpecialPages": "SpecialPages",
"Emailuser": "EmailUser",
"Whatlinkshere": "WhatLinksHere",
"MovePage": "MovePage",
"CheckUser": "CheckUser",
"Recentchangeslinked": "RecentChangesLinked",
"Protectedpages": "ProtectedPages",
"Fewestrevisions": "FewestRevisions",
"Withoutinterwiki": "WithoutInterwiki",
"Allpages": "AllPages",
"Userlogin": "UserLogin",
"CrossNamespaceLinks": "CrossNamespaceLinks",
"Mostrevisions": "MostRevisions",
"Disambiguations": "Disambiguations",
"Listusers": "ListUsers",
"Wantedcategories": "WantedCategories",
"Watchlist": "Watchlist",
"Listfiles": "ListFiles", // attention, not equal
"Filepath": "FilePath",
"DoubleRedirects": "DoubleRedirects",
"Preferences": "Preferences",
"Wantedpages": "WantedPages",
"Upload": "Upload",
"Mostlinked": "MostLinkedPages",
"Booksources": "BookSources",
"BrokenRedirects": "BrokenRedirects",
"Categories": "Categories",
"CategoryTree": "CategoryTree",
"Shortpages": "ShortPages",
"LongPages": "LongPages",
"Recentchanges": "RecentChanges",
"Ipblocklist": "BlockList",
"SiteMatrix": "SiteMatrix",
"Log": "Log",
"Mostcategories": "MostCategories",
"Mostimages": "MostLinkedFiles",
"Mostlinkedcategories": "MostLinkedCategories",
"Newpages": "NewPages",
"Newimages": "NewFiles", // attention, not equal
"Unusedtemplates": "UnusedTemplates",
"Uncategorizedpages": "UncategorizedPages",
"Uncategorizedimages": "UncategorizedFiles",
"Uncategorizedcategories": "UncategorizedCategories",
"Prefixindex": "PrefixIndex",
"Deadendpages": "DeadendPages",
"Ancientpages": "AncientPages",
"Export": "Export",
"Allmessages": "AllMessages",
"Statistics": "Statistics",
"Search": "Search",
"MIMEsearch": "MIMESearch",
"Version": "Version",
"Unusedimages": "UnusedFiles",
"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 parameters of special pages point to pages, in this case space and underscore mean the same */
titleParams: {
"EmailUser": [ "target" ],
"Contributions": [ "target" ],
"DeletedContributions": [ "target" ],
"Whatlinkshere": [ "target" ],
"Recentchangeslinked": [ "target" ],
"Undelete": [ "target" ],
"Allpages": [ "from" ],
"Prefixindex": [ "from" ],
"BlockIP": [ "ip" ],
"Log": [ "page" ],
"Filepath": [ "file" ],
"Randompage": [ "namespace" ]//,
},
/** some special pages can smush one parameter to the page title */
smush: {
"EmailUser": "target",
"Contributions": "target",
"DeletedContributions": "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",
"Watchlist": "action",
// Contributions
// a smushed /newbies does not mean a user named "newbies"!
// Randompage
// the namespace parameter is fake, it exists only in smushed form
}//,
};

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

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

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

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

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

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

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

/** watch or unwatch a page. the doneFunc is optional */
watchedPage: function(feedback, title, watch, doneFunc) {
feedback = feedback || this.NoFeedback;
var action = watch ? "watch" : "unwatch";
feedback.job(action + " page: " + title);
var args = {
title: title,
action: action//,
};
function change(form, doc) {
return true;
}
this.action(feedback, args, 0, change, 200, doneFunc);
},

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

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

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

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

/**
* block a user.
* createAccount defaults to true
* hardBlock, autoBlock, disableEmail and disableUTEdit default to false
* hardBlock makes sense for ip users, autoBlock for logged in users
* expiry may be "indefinite",
* or a number followed by a space and
* "years", "months", "days", "hours" or "minutes"
*/
blockUser: function(feedback, user, expiry, reason, autoBlock, hardBlock, createAccount, disableEmail, disableUTEdit, watchUT, allowChange, doneFunc) {
feedback = feedback || this.NoFeedback;
feedback.job("block user: " + user + " for: " + expiry);
var args = {
title: this.specialTitle("BlockIP"),
ip: user // url-encoded, optional
};
function change(form, doc) {
var hasError = form.getElementsByClassName("error").length !== 0;
if (!allowChange && hasError) return false;
form.elements["wpTarget"].value = user;
form.elements["wpExpiry-other"].value = expiry;
form.elements["wpReason-other"].value = reason;
form.elements["wpCreateAccount"].checked = createAccount;
form.elements["wpDisableEmail"].checked = disableEmail;
form.elements["wpDisableUTEdit"].checked = disableUTEdit;
form.elements["wpAutoBlock"].checked = autoBlock;
form.elements["wpHardBlock"].checked = hardBlock;
form.elements["wpWatch"].checked = watchUT;
// TODO wpConfirm to true for self-blocking
return true;
}
this.action(feedback, args, 0, change, 200, doneFunc);
},

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

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

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

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

/** WikiText generator */
var Markup = {
//------------------------------------------------------------------------------
//## complex
/** make a link to a user page */
userLink: function(user, label) {
return this.link(Titles.userPage(user), label);
},
/** make a link to a user's talkpage */
talkLink: function(user, label) {
return this.link(Titles.userTalkPage(user), label);
},
/** make a link to a user's contributions */
contribsLink: function(user, label) {
return this.link(Titles.specialPage("Contributions", user), label);
},
/** make a link to a lemma preventing inclusion */
referenceLink: function(title) {
var split = Namespace.split(title);
if (split.nsIndex === 6
|| split.nsIndex === 10
|| split.nsIndex === 14) {
return this.link(":" + title, title);
}
else {
return this.link(title);
}
},
/** make a redirect link */
redirect: function(title) {
return "#redirect " + this.link(title);
},
//------------------------------------------------------------------------------
//## enclosing
// enclosing
template: function() { return this.TEMPLATE_ + Array.from(arguments).join(this._TEMPLATE_) + this._TEMPLATE; },
link: function() { return this.LINK_ + Array.from(arguments).join(this._LINK_) + this._LINK; },
web: function() { return this.WEB_ + Array.from(arguments).join(this._WEB_) + this._WEB; },
h2: function(text) { return this.H2_ + text + this._H2; },
//------------------------------------------------------------------------------
//## composite
SIGAPP: " -- ~\~\~\~\n",
DASH: "--", // "—" em dash U+2014 &#8212;
//------------------------------------------------------------------------------
//## tokens
// 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 = {
/** the title with spaces instead of underscores */
normalizedTitle: function() {
return title.replace(/_/g, " ");
},
/** the label if set, the normalized title if not */
displayedLabel: function() {
return label ? label : this.normalizedTitle();
},
/** omits the label if it equals the title */
toPrettyString: function() {
var title = this.normalizedTitle();
var label = this.displayedLabel();
return title !== label
? Markup.link(title, label)
: Markup.link(title);
},
/** simple variant, omits the label if it's null */
toString: function() {
return this.label !== null
? Markup.link(this.title, this.label)
: Markup.link(this.title);
}//,
};

/** mangle all WikiLinks in a WikiText */
WikiLink.changeAll = function(wikiText, changeFunc) {
var re = /\[\[[ \t]*([^\]|]+?)[ \t]*(\|[ \t]*([^\]]*?)[ \t]*)?\]\]/g;
return wikiText.replace(re, function($0, $1, $2, $3, pos, string) {
var title = $1;
var second = $2;
var label = $3;
if (!second) label = null;
var orig = new WikiLink(title, label);
var changed = changeFunc(orig);
return changed !== null ? changed.toString() : $0;
});
}

/** returns an Array of all WikiLinks contained in a WikiText */
WikiLink.parseAll = function(wikiText) {
var out = [];
this.changeAll(wikiText, function(wikiLink) {
out.push(wikiLink);
return null;
});
return out;
}

//======================================================================
//## lib/core/Config.js

/** helper for wiki-local settings */
var Config = {
/** configure a configuration for the current wiki domain or language */
patch: function(cfg) {
var language = cfg[Wiki.language];
var domain = cfg[Wiki.domain];
if (language) Object.assign(cfg, language);
if (domain) Object.assign(cfg, domain);
}//,
};

//======================================================================
//## lib/core/IP.js

// @see mw.util.isIPv4Address
// @see mw.util.isIPv6Address
var IP = {
/** matches IPv4-like strings */
v4RE: /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/,

/** whether the string denotes an IPv4-address */
isV4: function(s) {
var m = this.v4RE.exec(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/Afterwards.js

/** creates functions to be used after user actions */
var Afterwards = {
/** returns a function reloading the page if changed and current page are the same */
maybeReloadFunc: function(title) {
return this.maybeReload.bind(this, title);
},
/** returns a function loading the history of a page if changed and current page are the same */
maybeHistoryFunc: function(title) {
return this.maybeHistory.bind(this, title);
},
//------------------------------------------------------------------------------
/** reload a page if it is the current page */
maybeReload: function(title) {
if (title !== Page.title) return;
window.location.href = Wiki.readURL(title);
},
/** show a pages history if it is the current page */
maybeHistory: function(title) {
if (title !== Page.title) return;
window.location.href = Wiki.encodeURL({
title: title,
action: "history"//,
});
}//,
};

//======================================================================
//## 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) {
jsutil.DOM.addClass(knob, "switch-knob");
jsutil.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) jsutil.DOM.replaceClass(knob, "switch-off", "switch-on");
else jsutil.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);
jsutil.DOM.moveElement(this.canvas, pos);
},
/** limits canvas position to the window */
limit: function(pos) {
var min = jsutil.DOM.minPos();
var max = jsutil.DOM.maxPos();
var size = jsutil.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 jsutil.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 below 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;
jsutil.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 (jsutil.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;
jsutil.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() {
jsutil.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 = jsutil.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 */
var Links = {
/**
* create an action link which
* - onclick queries a text and
* - oncontextmenu opens a popup with default texts
* and calls the single-argument selectFunc with it.
*
* promptLabel may be null to indicate no prompt is
* wanted, the selecteFunc is called with null in this case.
*
* groups is an Array of Arrays of preset reason Strings.
* a separator is placed between rows. null is allowed to
* disable the popup.
*/
promptPopupLink: function(label, promptLabel, groups, selectFunc) {
var link = promptLabel !== null
? this.promptLink(label, promptLabel, selectFunc)
: this.functionLink(label, selectFunc.bind(this, null));
if (groups) this.popupOnLink(link, groups, selectFunc);
return link;
},
/** add a popup calling a function to an existing link */
popupOnLink: function(link, groups, popupFunc) {
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(link, popup);
},

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

/** create an action link calling a function on click */
functionLink: function(label, clickFunc) {
var a = document.createElement("a");
a.className = "link-function";
a.onclick = clickFunc;
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 = jsutil.DOM.get('progress-global');
if (mainDiv === null) {
mainDiv = document.createElement("div");
mainDiv.id = 'progress-global';
mainDiv.className = "progress-global";
jsutil.DOM.insertBefore(jsutil.DOM.get('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();
jsutil.DOM.removeChildren(this.headerDiv);
jsutil.DOM.insertEnd(this.headerDiv, document.createTextNode(content));
},

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

/** destructor, called by fade */
destroy: function() {
jsutil.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 the Feedback interface defined in Actions.js
* change an ActionLink's CSS-class link-running
*/
function FeedbackLink(link) {
this.link = link;
}
FeedbackLink.prototype = {
job: function(s) { },
work: function(s) { jsutil.DOM.addClass(this.link, "link-running"); },
success: function(s) { jsutil.DOM.removeClass(this.link, "link-running"); },
failure: function(s) { jsutil.DOM.replaceClass(this.link, "link-running", "link-failed" ); }
};

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

/**
* implements the Feedback interface defined in Actions.js
* delegates to a ProgressArea
*/
function FeedbackArea() {
this.area = new ProgressArea();
}
FeedbackArea.prototype = {
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); }, // #body unfades
};

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

/** encapsulates column-one */
var 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 addSimplePortlet/addComplexPortlet and displayed in showPortlets */
preparedPortlets: [],
/** add a portlet from an element */
addSimplePortlet: function(id, title, content) {
this.preparedPortlets.push(
this.createPortlet(id, title, content));
},

/**
* 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
*/
addComplexPortlet: function(id, title, labels, rows) {
this.addSimplePortlet(id, title,
this.renderPortletContent(rows, labels));
},
/**
* render a complex portlet's content element from an Array of Arrays
* containing elements or ids of elements to steal
*/
renderPortletContent: function(rows, labels) {
// remove nulls, steal nodes
var newRows = [];
for (var i=0; i<rows.length; i++) {
var cells = rows[i];
if (!cells) continue;
var newCells = [];
for (var j=0; j<cells.length; j++) {
var cell = cells[j];
if (!cell) continue;
if (cell.constructor === String) {
var element = jsutil.DOM.get(cell);
if (!element) continue;
var label = labels[cell];
cell = element.firstChild.cloneNode(true);
if (label) {
cell.textContent = label;
}
}
newCells.push(cell);
}
if (newCells.length === 0) continue;
newRows.push(newCells);
}
//if (newRows.length === 0) return null;
// compile into ul/li/node
var ul = document.createElement("ul");
for (var i=0; i<newRows.length; i++) {
var newCells = newRows[i];
var li = document.createElement("li");
for (var j=0; j<newCells.length; j++) {
if (j !== 0) {
li.appendChild(document.createTextNode(" "));
}
var newCell = newCells[j];
li.appendChild(newCell);
}
ul.appendChild(li);
}
return ul;
},
/** create a portlet div */
createPortlet: function(id, title, content) {
var header = document.createElement("h3");
header.textContent = title;
var body = document.createElement("div");
body.className = "pBody";
body.appendChild(content);
var portlet = document.createElement("div");
portlet.id = id;
portlet.className = "portlet";
portlet.appendChild(header);
portlet.appendChild(body);
return portlet;
},

/** display the portlets created before and remove older ones with the same id */
showPortlets: function() {
var columnOne = jsutil.DOM.get('column-one');
for (var i=0; i<this.preparedPortlets.length; i++) {
var portlet = this.preparedPortlets[i];
var replaces = jsutil.DOM.get(portlet.id);
if (replaces) jsutil.DOM.removeNode(replaces);
columnOne.appendChild(portlet);
}
// 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 heading = jsutil.DOM.fetch('p-search', "h3", null, 0);
jsutil.DOM.removeChildren(heading);
var name = this.siteName();
if (name == null) return;
var a = document.createElement("a");
a.id = "siteName";
a.textContent = name;
a.href = Wiki.site;
heading.appendChild(a);
},
/** find the name of the current wiki */
siteName: function() {
var links = document.getElementsByTagName("link");
for (var i=0; i<links.length; i++) {
var link = links[i];
if (link.rel == "search") return link.title;
}
return null;
}//,
};

//======================================================================
//## app/extension/ActionHistory.js

/** helper for action=history */
var 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() {
var lis = jsutil.DOM.fetch('pagehistory', "li");
if (!lis) return;
for (var i=0; i<lis.length; i++) {
var li = lis[i];
this.editAndRestore(li);
this.immediatize(li);
}
},
/** add edit and restore links to an item */
editAndRestore: function(li) {
var diffInput = jsutil.DOM.fetch(li, "input", null, 1);
if (!diffInput) return;

// gather data
var histSpan = jsutil.DOM.fetch(li, "span", "history-user", 0);
var histA = jsutil.DOM.fetch(histSpan, "a", null, 0);
// (Username or IP removed)
if (!histA) return;
var dateA = jsutil.DOM.nextElement(diffInput, "a");
if (!dateA) return;
var oldid = diffInput.value;
var user = histA.textContent;
var date = dateA.textContent;
var msg = ActionHistory.msg;

// add edit and restore version link
function done() { window.location.reload(true); }
var restore = FastRestore.linkRestore(Page.title, oldid, user, date, done);
var edit = Links.pageLink(msg.edit, {
title: Page.title,
oldid: oldid,
action: "edit"//,
});
jsutil.DOM.insertBeforeMany(dateA,
jsutil.DOM.textAsNodeMany([
" [", edit, "] ",
" [", restore, "] "
])
);
},

/** extend undo and rollback links */
immediatize: function(li) {
// TODO unify with ActionDiff
var undoSpan = jsutil.DOM.fetch(li, "span", 'mw-history-undo', 0);
if (undoSpan) {
var undoA = jsutil.DOM.fetch(undoSpan, "a", null, 0);
FastUndo.patchLink(undoA,
Afterwards.maybeHistoryFunc(Page.title));
}
// TODO add a revert-button for users without rollback
var rollbackSpan = jsutil.DOM.fetch(li, "span", 'mw-rollback-link', 0);
if (rollbackSpan) {
var rollbackA = jsutil.DOM.fetch(rollbackSpan, "a", null, 0);
FastRollback.patchLink(rollbackA,
Afterwards.maybeHistoryFunc(Page.title));
}
}
};
ActionHistory.msg = {
edit: "edit"//,
};

//======================================================================
//## app/extension/ActionDiff.js

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

//------------------------------------------------------------------------------
//## private
hasRollback: false,

/** extend undo links */
fastUndo: function() {
var newVersion = jsutil.DOM.get('mw-diff-ntitle1');
if (!newVersion) return;
var as = jsutil.DOM.fetch(newVersion, "a");
if (!as.length) return;
var a = as[as.length-1]; // the last link
FastUndo.patchLink(a,
Afterwards.maybeHistoryFunc(Page.title));
},
/** extend rollback links */
fastRevert: function() {
var newVersion = jsutil.DOM.get('mw-diff-ntitle2');
if (!newVersion) return;
var span = jsutil.DOM.fetch(newVersion, "span", "mw-rollback-link", 0);
if (!span) return;
var a = jsutil.DOM.fetch(span, "a", null, 0);
FastRollback.patchLink(a,
Afterwards.maybeHistoryFunc(Page.title));
this.hasRollback = true;
},

/** extends one of the two sides */
addLinks: function(tdClassName) {
// get container
var td = jsutil.DOM.fetch(document, "td", tdClassName, 0);
if (!td) return;
// get lines
var divs = td.getElementsByTagName("div");
// extract revision info
var revisionA = jsutil.DOM.fetch(divs[0], "a", null, 0);
if (!revisionA) return;
var params = Wiki.decodeURL(revisionA.href);
var oldid = params.oldid;
var dateP = ActionDiff.cfg.versionExtractRE.exec(revisionA.textContent);
var date = dateP ? dateP[1] : null;
// extract user info
var userA = jsutil.DOM.fetch(divs[1], "a", null, 0);
// (Username or IP removed)
if (!userA) return;
var user = userA.textContent;
var done = Afterwards.maybeHistoryFunc(Page.title);
// if an oldid exists: add a restore revision link
var restorable = params.oldid;
if (restorable) {
var restore = FastRestore.linkRestore(Page.title, oldid, user, date, done);
var targetA = jsutil.DOM.fetch(divs[0], "a", null, 1);
if (targetA) {
jsutil.DOM.insertBeforeMany(targetA,
jsutil.DOM.textAsNodeMany([ restore, ") ("])
);
}
}
// note: some users have rollback, others don't
if (!this.hasRollback && divs.length > 3) {
// latest revision: add a revert link
var div3 = divs[3];
var latest = div3.id === "mw-diff-ntitle4" && (
div3.innerHTML === "&nbsp;" || // firefox
div3.innerHTML === "\u00a0" ); // safari
if (latest) {
var revert = UserRevert.linkRevert(Page.title, user, done);
jsutil.DOM.insertEndMany(divs[1],
jsutil.DOM.textAsNodeMany([ " [", revert, "]" ])
);
}
}
}//,
};
ActionDiff.cfg = {
// TODO: hardcoded lang_de OR lang_en
versionExtractRE: /(?:Version vom|Revision as of) (.*)/ //,
};

//======================================================================
//## app/extension/SpecialAny.js

/** dispatcher for SpecialPages */
var SpecialAny = {
/** 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();
}

this.mangleForm(name);
},
mangleForm: function(specialPage) {
// if there is only one form, it's the searchform
if (document.forms.length < 2) return;
var form = document.forms[0];
if (!form) return;
// only pages listed below are mangled
var elementNames = SpecialAny.cfg.autoSubmitElements[specialPage];
if (!elementNames) return;
this.autoSubmit(form, elementNames);
// HACK: in Watchlist the submit-button is necessary
if (specialPage === "Watchlist"
&& Page.params["action"])
return;
// HACK: in the Undelete sometimes the buttons are necessary. or at least a restore-parameter
if (specialPage === "Undelete") {
if (Page.params["timestamp"] || Page.params["action"] === "submit")
return;
form.action += "&restore=yes";
}
this.removeButtons(form);
},

/** adds an onchange handler to elements in a form submitting the form */
autoSubmit: function(form, elementNames) {
function change() { form.submit(); }
var elements = form.elements;
for (var i=0; i<elementNames.length; i++) {
var elementName = elementNames[i];
var element = elements[elementName];
if (!element) continue;
// radioButtons return an array, not a single element,
// but only when accessed by name (!)
if (element.type) {
element.onchange = change;
}
}
},
/** removes submit button(s) */
removeButtons: function(form) {
var elements = form.elements;
for (var i=0; i<elements.length; i++) {
var element = elements[i];
if (element.type !== "submit"
&& element.type !== "reset") continue;
element.style.display = "none";
}
}//,
};
SpecialAny.cfg = {
/**
* maps Specialpage names to the autosubmitting form elements.
* only SpecialPages listed here have their submit-buttons hidden.
*/
autoSubmitElements: {
AbuseFilter: [ // "deletedfilters",
"mw-abusefilter-deletedfilters-show",
"mw-abusefilter-deletedfilters-hide",
"mw-abusefilter-deletedfilters-only",
"hidedisabled", "limit" ],
StablePages: [ "namespace" ],
GlobalUsers: [ "group", "username" ],
Allpages: [ "namespace", "nsfrom" ],
Contributions: [ "namespace", "month" ], // contribs is a bit useless
Ipblocklist: [ "title", // default action
"wpUnblockAddress", "wpUnblockReason" // action=unblock
],
LinkSearch: [ "title" ],
Listusers: [ "group", "username" ],
Log: [ "type", "user", "page",
"year", "month" ],
Newimages: [ "wpIlMatch" ],
Newpages: [ "namespace", "username" ],
Prefixindex: [ "namespace", "nsfrom" ],
Recentchanges: [ "namespace", "invert" ],
Watchlist: [ "namespace" ],
Whatlinkshere: [ "namespace" ],
Booksources: [ "isbn" ],
CategoryTree: [ "mode", "target" ],
Cite: [ "page" ],
Filepath: [ "file" ],
Listfiles: [ "limit" ],
MIMEsearch: [ "mime" ],
Search: [ "lsearchbox" ],
Undelete: [
],
DeletedContributions:
[ "namespace", "target" ],
ReviewedPages: [ "level" ],
Userrights: [ "user" ],
Recentchangeslinked:
[ "recentchangeslinked-target", "showlinkedto" ]//,
}//,
};

//======================================================================
//## app/extension/SpecialBlockIP.js

/** extends Special:BlockIP */
var SpecialBlockIP = {
/** onload initializer */
init: function() {
this.presetBlockIP();
},

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

/** fill in default values into the blockip form */
presetBlockIP: function() {
// if there is only one form, it's the searchform
if (document.forms.length < 2) return;
var form = document.forms[0]; // was blockip
if (!form) return; // action=success

var def = SpecialBlockIP.cfg.defaults;
form.elements["mw-input-wpExpiry"].value = "other";
form.elements["mw-input-wpExpiry-other"].value = def.expiry;
form.elements["mw-input-wpReason-other"].value = def.reason;
form.elements["mw-input-wpReason-other"].select();
form.elements["mw-input-wpReason-other"].focus();
}//,
};
SpecialBlockIP.cfg = {
defaults: {
expiry: "2 hours",
reason: "vandalismus"//,
}//,
};

//======================================================================
//## app/extension/SpecialContributions.js

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

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

/** extend rollback links */
fastRevert: function() {
var warning = jsutil.DOM.fetch('bodyContent', "div", "mw-warning-with-logexcerpt", 0);
var ul = jsutil.DOM.fetch('bodyContent', "ul", null, warning ? 1 : 0);
if (ul === null) return;

function changeLink(link) {
link.textContent = SpecialContributions.msg.reverted;
}
var all = [];
var spans = jsutil.DOM.fetch(ul, "span", "mw-rollback-link");
for (var i=0; i<spans.length; i++) {
var span = spans[i];
var a = jsutil.DOM.fetch(span, "a", null, 0);
var p = FastRollback.patchLink(a, changeLink);
all.push(p);
}
var revert = Links.functionLink(SpecialContributions.msg.revertAll, function() {
for (var i=0; i<all.length; i++) { all[i].onclick(); }
});
jsutil.DOM.addClass(revert, "link-immediate");
var li = document.createElement("li");
li.appendChild(revert);
jsutil.DOM.insertBegin(ul, li);
}//,
};
SpecialContributions.msg = {
reverted: "zurückgesetzt",
revertAll: "alle zurücksetzen",
};

//======================================================================
//## app/extension/SpecialLog.js

/** extends Special:Log */
var SpecialLog = {
/** onload initializer */
init: function() {
if (Page.params.type === "newusers") {
this.extendNewusers();
}
},

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

/** faster */
extendNewusers: function() {
var ul = jsutil.DOM.fetch('mw-log-deleterevision-submit', "ul", null, 0);
if (!ul) return;

function find(as) {
for (var i=0; i<as.length; i++) {
try {
var a = as[i];
var args = Wiki.decodeURL(a.href);
if (args.ip) return args.ip;
}
catch (e) {
// "cannot decode" from wiki.decodeURL when someone
// used an external link in the summary
}
}
return null
}

// TODO use FastBlock
function handler() {
var clicked = this;
var as = jsutil.DOM.fetch(clicked.parseFrom, "a");
var name = find(as);
if (!name) return false;
var feedback = new FeedbackLink(clicked);
var expiry = "indefinite";
var reason = SpecialLog.msg.reason;
var autoBlock = true;
var hardBlock = true;
var createAccount = true;
var disableEmail = true;
var disableUTEdit = true;
var watchUser = false;
var allowChange = false;
Actions.blockUser(feedback, name, expiry, reason, autoBlock, hardBlock, createAccount, disableEmail, disableUTEdit, watchUser, allowChange, function() {
clicked.textContent = SpecialLog.msg.blocked;
});
return false;
}

var lis = jsutil.DOM.fetch(ul, "li");
for (var i=0; i<lis.length; i++) {
var li = lis[i];
var a = Links.functionLink(SpecialLog.msg.block, handler);
jsutil.DOM.addClass(a, "link-immediate");
a.parseFrom = li; // used by the onclick-handler
jsutil.DOM.insertAfterMany(li.firstChild,
jsutil.DOM.textAsNodeMany([ " [", a, "] " ])
);
}
}//,
};
SpecialLog.msg = {
block: "killen",
blocked: "tot",
reason: ""//,
};

//======================================================================
//## app/extension/SpecialNewpages.js

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

//------------------------------------------------------------------------------
//## private
/** extend Special: 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 ? "" : "none";
if (open && foldButton.needsLoad) {
loadContent(li);
foldButton.needsLoad = false;
}
});
foldButton.needsLoad = false;
jsutil.DOM.insertBegin(header, foldButton.button);

// add action links
jsutil.DOM.insertBegin(header, Google.link(title));
jsutil.DOM.insertBegin(header, UserBookmarks.linkMark(title));
var templateTools = TemplatePage.bankAllPage(title);
if (templateTools) jsutil.DOM.insertBeginMany(header, templateTools);
jsutil.DOM.insertBegin(header, FastDelete.linkDeletePopup(title));
// 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;
}
}

/** load the article content and display it inline */
function loadContent(li) {
li.bodyDiv.textContent = SpecialNewpages.msg.loading;
jsutil.Ajax.call({
url: Wiki.readURL(li.pageTitle, { redirect: "no" }),
successFunc: function(source) {
// uses the monobook start content marker
// firefox, safari: [^] works, div is lower case
// opera: [^] does not work, div is upper case
var extractRE = /<!-- start content -->([\s\S]*)<(div|DIV) class="printfooter">/;
var content = extractRE.exec(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 ul = jsutil.DOM.fetch('bodyContent', "ul", null, 0);
if (!ul) return;
ul.className = "SpecialNewpages";

// find article list items
var lis = jsutil.DOM.fetch(ul, "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/extension/SpecialSpecialPages.js

/** extends Special:SpecialPages */
var SpecialSpecialPages = {
/** onload initializer */
init: function() {
this.extendLinks();
},

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

/** make a sorted tables from the links */
extendLinks: function() {
var tables = jsutil.DOM.fetch('bodyContent', "table", "mw-specialpages-table");
for (var i=0; i<tables.length; i++) {
var group = tables[i];
this.extendGroup(group);
}
},
/** make a sorted table from the links of one group */
extendGroup: function(group) {
var as = jsutil.DOM.fetch(group, "a");
for (var i=0; i<as.length; i++) {
var a = as[i];
var name = Namespace.scan(-1, a.title);
var descr = a.textContent;
a.title = descr;
a.textContent = name;
var hint = document.createElement("span");
hint.className = "specialSpecialPagesHint";
jsutil.DOM.insertAfter(a, hint);
var specialNames = Special.pageNames(name);
if (!specialNames) {
hint.textContent = "(missing)";
continue;
}
var canonical = specialNames.canonicalName;
if (canonical === name) {
hint.textContent = "(canonical)";
continue;
}
hint.textContent = canonical;
}
}//,
};

//======================================================================
//## app/extension/SpecialUndelete.js

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

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

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

var button = Links.functionLink(SpecialUndelete.msg.invert, function() {
var els = form.elements;
for (var i=0; i<els.length; i++) {
var el = els[i];
if (el.type !== "checkbox") continue;
el.checked = !el.checked;
}
});

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

//======================================================================
//## app/extension/SpecialRecentchanges.js

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

//======================================================================
//## app/extension/SpecialRecentchangeslinked.js

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

//======================================================================
//## app/extension/SpecialWatchlist.js

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

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

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

// add invert buttons for single namespaces
for (var i=0; i<spaces.length; i++) {
var space = spaces[i];
var button = this.toggleButton(space.ul);
jsutil.DOM.insertAfter(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[form.elements.length-1];
jsutil.DOM.insertBeforeMany(target, [
globalHdr, button,
// HACK to get some space
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 = jsutil.DOM.fetch(document, "form", null, 0);
var uls = jsutil.DOM.fetch(form, "ul");
for (var i=0; i<uls.length; i++) {
var ul = uls[i];
var h2 = jsutil.DOM.previousElement(ul);
var ns = h2 ? h2.textContent : "";
out.push({ ul: ul, h2: h2, ns: ns });
}
return out;
}//,
};
SpecialWatchlist.msg = {
invert: "Invertieren",
article: "Artikel",
global: "Alle"//,
};

//======================================================================
//## app/extension/SpecialPrefixindex.js

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

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

/** sort items into a straight list */
sortItems: function() {
var table = jsutil.DOM.fetch('bodyContent', "table", null, 2);
if (!table) return; // no search results
var tds = jsutil.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 site */
var ForSite = {
init: function() {
Config.patch(ForSite.cfg);
},
/** a link to new pages */
linkNewPages: function() {
return Links.pageLink(ForSite.msg.newpages, {
title: Titles.specialPage("NewPages"),
limit: 20//,
});
},

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

/** a bank of links to interesting pages */
bankProjectPages: function() {
var pages = ForSite.cfg.projectPages;
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(Titles.specialPage("Log", userdata.toLowerCase()));
}
return this.linkAllPopup(
ForSite.msg.logLabel,
Titles.specialPage("Log"),
ForSite.cfg.logs,
selected);
},

/** return a link for fast logfiles access */
linkAllSpecialsPopup: function() {
function selected(userdata) {
window.location.href = Wiki.readURL(Titles.specialPage(userdata));
}
return this.linkAllPopup(
ForSite.msg.specialLabel,
Titles.specialPage("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 = {
/** which logs are displayed in the popup */
logs: [
"Move", "Block", "Protect", "Delete", "Upload"
],

/** which specialpages are displayed in the opoup */
specials:[
"AllMessages", "AllPages", "CategoryTree", "IPBlockList", "Linksearch", "ListUsers", "NewImages", "PrefixIndex",
],
/** useful pages in this wiki */
projectPages: null,
// domain-specific data
"de.wikipedia.org": {
projectPages: [
[ "VM", "Wikipedia:Vandalismusmeldung" ],
[ "EW", "Wikipedia:Entsperrwünsche" ],
[ "SP", "Wikipedia:Sperrprüfung" ],
[ "LP", "Wikipedia:Löschprüfung" ],
[ "LK", "Wikipedia:Löschkandidaten" ],
[ "SL", "Kategorie:Wikipedia:Schnelllöschen" ],
]//,
},
"de.wikiversity.org": {
projectPages: [
[ "Löschen", "Kategorie:Wikiversity:Löschen" ]//,
]//,
},
"de.wikisource.org": {
projectPages: [
[ "LK", "Wikisource:Löschkandidaten" ],
[ "SL", "Kategorie:Wikisource:Schnelllöschen" ]//,
]//,
}//,
};
ForSite.msg = {
logLabel: "Logs",
specialLabel: "Spezial",

newpages: "Frischtext",
newusers: "Noobs",
};

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

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

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

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

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

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

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


/** returns a link to the blockpage for a user */
linkBlock: function(user) {
return Links.readLink(ForUser.msg.block, Titles.specialPage("BlockIP", user));
},

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

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

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

/** returns a link to show subpages of a user */
linkSubpages: function(user) {
return Links.pageLink(ForUser.msg.subpages, {
title: Titles.specialPage("PrefixIndex"),
namespace: 2, // User
from: user + "/"//,
});
},
/** various checks for anonymous users */
bankIP: function(ip) {
return ForUser.cfg.ipChecks.map(function(def) {
return Links.urlLink(def.label, def.template.template({ip:ip}));
});
}//,
};
ForUser.msg = {
home: "Benutzer",
talk: "Diskussion",
email: "Anmailen",
contribs: "Contribs",
block: "Sperren",

news: "☏",
logsAbout: "Log",
logsActor: "Act",
subpages: "Sub",
};
ForUser.cfg = {
ipChecks: [
{ label: "Honey", template: "http://www.projecthoneypot.org/ip_${ip}" },
{ label: "Whois", template: "http://whois.domaintools.com/${ip}" }//,
]//,
};

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

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

// tag list items with a CSS class "is-ip" or "is-named"
var uls = jsutil.DOM.fetch(bodyContent, "ul", "special");
for (var i=0; i<uls.length; i++) {
var ul = uls[i];
var lis = jsutil.DOM.fetch(ul, "li");
for (var j=0; j<lis.length; j++) {
var li = lis[j];
var a = jsutil.DOM.fetch(li, "a", "mw-userlink", 0);
if (!a) continue;
if (IP.isV4(a.textContent)) jsutil.DOM.addClass(li, "is-ip");
else jsutil.DOM.addClass(li, "is-named");
}
}

/** changes the filter state */
function update(link, state) {
board.select(link);
if (state === "named") jsutil.DOM.addClass( bodyContent, "hide-ip");
else jsutil.DOM.removeClass( bodyContent, "hide-ip");
if (state === "ip") jsutil.DOM.addClass( bodyContent, "hide-named");
else jsutil.DOM.removeClass( bodyContent, "hide-named");
jsutil.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 = jsutil.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 = jsutil.DOM.fetch(document, "form", null, 0);
if (!target) return;

// HACK to get some space
var br = document.createElement("br");
br.style.lineHeight = "30%";
var after = document.createElement("div");
after.className = "visualClear";

jsutil.DOM.insertAfterMany(target,
jsutil.DOM.textAsNodeMany([
br,
FilteredEditList.msg.intro,
board.component,
after
])
);
}//,
};
FilteredEditList.msg = {
intro: "Filter: ",
state: {
all: "Alle",
ip: "Ips",
named: "Angemeldete"//,
}//,
};

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

/** page watch and unwatch without reloading the page */
var FastWatch = {
init: function() {
/** initialize link */
function initView() {
var watch = jsutil.DOM.get('ca-watch');
var unwatch = jsutil.DOM.get('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 = jsutil.DOM.get('ca-watch');
var unwatch = jsutil.DOM.get('ca-unwatch');
if ( watched && watch ) exchangeItem(watch, false);
if (!watched && unwatch) exchangeItem(unwatch, true);
}
var feedback = new FeedbackLink(link);
Actions.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);
});
jsutil.DOM.addClass(a, "link-immediate");
li.appendChild(a);
target.parentNode.replaceChild(li, target);
}

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

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

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

/** delete an article with a reason */
fastDelete: function(title, reason) {
var feedback = new FeedbackArea();
Actions.deletePage(feedback, title, reason, Afterwards.maybeReloadFunc(title));
}//,
};
FastDelete.cfg = {
reasons: [
[ "schrott",
"kein artikel",
"aha",
"unfug",
"wirr",
"irrelevant",
"tastaturtest"//,
],
[ "zu mager",
"falsches lemma",
"falsche sprache",
"wörterbucheintrag",
"linkcontainer",
"werbung"//,
],
[ "unnötig",
//"verwaist",
"geleert",
"versehen",
"veraltet",
"erledigt"//,
]//,
]//,
};
FastDelete.msg = {
prompt: "warum löschen?",
label: "Wech",
tooltip: {
deletePage: "seite löschen"//,
}//,
};

//======================================================================
//## app/feature/FastBlock.js

/** fast user block */
var FastBlock = {
/**
* returns a link to kill a user with full templating.
* a simple klickpromps for a reason,
* a context menu provides preset reasons.
*/
linkKillPopup: function(user) {
var self = this;
var msg = FastBlock.msg;
var cfg = FastBlock.cfg;
return Links.promptPopupLink(msg.label, msg.prompt, cfg.reasons,
function(reason) { self.killUser(user, reason); }
);
},

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

/** does everything necessary to block a user indefinitely */
killUser: function(user, reason) {
// TODO split this into two separate actions?
this.blockIndefinite(user, reason);
this.templateBlockedUser(user, reason);
},

/** blocks a user for an indefinite duration */
blockIndefinite: function(user, reason) {
var feedback = new FeedbackArea();
var expiry = "indefinite";
var autoBlock = false;
var hardBlock = false;
var createAccount = true;
var disableEmail = true;
var disableUTEdit = true;
var watchUser = false;
Actions.blockUser(feedback, user, expiry, reason, autoBlock, hardBlock, createAccount, disableEmail, disableUTEdit, watchUser, true);
},

/** inserts blocked templates in a users's home- and talkpage */
templateBlockedUser: function(user, reason) {
this.modifyUserHomePage(user, reason);
this.modifyUserTalkPage(user, reason);
},
/** inserts redirect to the talkpage and protects */
modifyUserHomePage: function(user, reason) {
var self = this;
var title = Titles.userPage(user);
var template = Markup.template(FastBlock.cfg.template);
var feedback = new FeedbackArea();
function phase1() {
var text = "#redirect [[" + Titles.userTalkPage(user) + "]]";
var minorEdit = false;
var allowCreate = true;
Actions.replaceText(feedback, title, jsutil.Function.constant(text), template, minorEdit, allowCreate, phase2);
}
function phase2() {
var level = "sysop";
var cascade = false;
var expiry = "";
var watch = false;
Actions.protectPage(feedback, title, level, expiry, level, expiry, level, expiry, template, cascade, watch);
}
phase1();
},
/** inserts the template in a page and protects */
modifyUserTalkPage: function(user, reason) {
var self = this;
var title = Titles.userTalkPage(user);
var template = Markup.template(FastBlock.cfg.template);
var feedback = new FeedbackArea();
function phase1() {
var text = template + " " + reason + Markup.SIGAPP;
var sepa = Markup.LF + Markup.LINE + Markup.LF;
var allowCreate = true;
Actions.prependText(feedback, title, text, template, sepa, allowCreate, phase2);
}
function phase2() {
var level = "sysop";
var cascade = false;
var expiry = "";
var watch = false;
Actions.protectPage(feedback, title, level, expiry, level, expiry, level, expiry, template, cascade, watch);
}
phase1();
}//,
};
FastBlock.cfg = {
reasons: [
[ "vandalenaccount",
"fakeaccount",
"provokationsaccount",
"ja nee, is klar",
"jaja"//,
]//,
],
template: "gesperrter Benutzer"//,
};
FastBlock.msg = {
label: "Killen",
prompt: "warum killen?"//,
};

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

/** old revision restore */
var FastRestore = {
init: function() {
Config.patch(FastRestore.cfg);
},
/** returns a link restoring a given version */
linkRestore: function(title, oldid, user, date, doneFunc) {
var summary = FastRestore.cfg.summary(title, oldid, user, date);
var restore = Links.functionLink(FastRestore.msg.label, function() {
var feedback = new FeedbackLink(restore);
Actions.restoreVersion(feedback, title, oldid, summary, doneFunc);
});
jsutil.DOM.addClass(restore, "link-immediate");
return restore;
}//,
};
FastRestore.msg = {
label: "restore"//,
};
FastRestore.cfg = {
"de": {
/** compile a summary for a restore-action */
summary: function(title, oldid, user, date) {
return "zurück auf " + Markup.userLink(user, user) + " " + date + " (" + oldid + ")";
}//,
},
"en": {
/** compile a summary for a restore-action */
summary: function(title, oldid, user, date) {
return "back to " + Markup.userLink(user, user) + " " + date + " (" + oldid + ")";
}//,
}//,
};

//======================================================================
//## app/feature/FastUndo.js

/** ajax undo */
var FastUndo = {
/** extend an undo link */
patchLink: function(link, doneFunc) {
var params = Wiki.decodeURL(link.href);
if (!params.undo) return; // TODO necessary?
jsutil.DOM.addClass(link, "link-immediate");
link.onclick = function() {
var feedback = new FeedbackLink(link);
Actions.undoVersion(feedback, params.title, params.undo, params.undoafter, doneFunc);
return false;
};
}//,
};

//======================================================================
//## app/feature/FastRollback.js

/** ajax rollback */
var FastRollback = {
init: function() {
Config.patch(FastRollback.cfg);
},
/** extend a rollback link */
patchLink: function(link, doneFunc) {
var params = Wiki.decodeURL(link.href);
if (!params.token) return null; // TODO necessary?
var msg = FastRollback.msg;
var cfg = FastRollback.cfg;
var promptLabel = cfg.requireReason ? msg.promptLabel : null;
var mainLink = Links.promptPopupLink(msg.label, promptLabel, cfg.reasons, function(reason) {
var feedback = new FeedbackLink(mainLink);
var summary = cfg.commentFunc(params.from, reason);
Actions.rollbackEdit(feedback, params.title, params.from, params.token, summary, doneFunc);
return false;
});
//link.title = msg.tooltip.deletePage;
link.parentNode.replaceChild(mainLink, link);
return mainLink;
},
};
FastRollback.msg = {
label: "rollback",
promptLabel: "Rollback - grund?"//,
};
FastRollback.cfg = {
requireReason: false,
commentFunc: function(victim, reason) { return reason === null ? null : "revert " + Markup.contribsLink(victim, victim) + ": " + reason; },
"de": {
reasons: [
[ "vandalismus", "unfug", "ach", "flame" ],
[ "warum?", "quelle?", "häh?" ],
[ "glaskugelei", "pov" ]
],
},
"en": {
reasons: null
}//,
};

//======================================================================
//## app/feature/UserRevert.js

/** add a revert-button for non-admins */
var UserRevert = {
init: function() {
Config.patch(UserRevert.cfg);
},
/** returns a link revertung edits by a given user */
linkRevert: function(title, badUser, doneFunc) {
var self = this;
var revert = Links.functionLink(UserRevert.msg.revert, function() {
self.revert(feedback, title, badUser, Afterwards.maybeHistoryFunc(title));
});
var feedback = new FeedbackLink(revert);
jsutil.DOM.addClass(revert, "link-immediate");
return revert;
},

//------------------------------------------------------------------------------
//## private
/** reverts new edits of a given user */
revert: function(feedback, title, badUser, doneFunc) {
function phase1() {
Actions.newEdits(feedback, title, badUser, phase2);
}
function phase2(title, user, previousUser, revid, timestamp) {
var summary = UserRevert.cfg.summary(title, user, previousUser, revid, timestamp);
Actions.restoreVersion(feedback, title, revid, summary, doneFunc);
}
phase1();
}//,
};
UserRevert.msg = {
revert: "revert"//,
};
UserRevert.cfg = {
"de": {
summary: function(title, user, previousUser, revid, timestamp) {
// msg:revertpage
// Änderungen von [[{{ns:user}}:$2|$2]] ([[{{ns:special}}:Contributions/$2|Beiträge]]) rückgängig gemacht und letzte Version von $1 wiederhergestellt
return "Änderungen von " +
Markup.userLink(user, user) + " " +
"(" + Markup.contribsLink(user, "Beiträge") + ") " +
"rückgängig gemacht und letzte Version von " +
previousUser + " wiederhergestellt";
}//,
},
"en": {
summary: function(title, user, previousUser, revid, timestamp) {
// msg:revertpage
// Reverted edits by [[Special:Contributions/$2|$2]] ([[User talk:$2|talk]]) to last version by $1
return "Reverted edits by " +
Markup.contribsLink(user, user) + " " +
"(" + Markup.talkLink(user, "talk") + ") " +
"to last version by " + previousUser;
}//,
}//,
};

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

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

var msg = TemplatePage.msg;
var self = this;
return [
Links.promptLink(msg.urv.label, msg.urv.prompt, function(source) { self.urv(title, source); }),
Links.promptLink(msg.qs.label, msg.qs.prompt, function(reason) { self.qs(title, reason); }),
Links.promptLink(msg.la.label, msg.la.prompt, function(reason) { self.la(title, reason); }),
Links.promptLink(msg.sla.label, msg.sla.prompt, function(reason) { self.sla(title, reason); })//,
];
},

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

/** puts an SLA template into an article */
sla: function(title, reason) {
var template = "löschen";
// reason as parameter
var summary = Markup.template(template, reason);
var text = Markup.template(template, reason + Markup.SIGAPP)
var sepa = Markup.LF;
var feedback = new FeedbackArea();
Actions.prependText(feedback, title, text, summary, sepa, false, Afterwards.maybeReloadFunc(title));
},
/** puts an LA template into an article */
la: function(title, reason) {
// TODO should automatically nowiki an SLA
var template = "subst:Löschantrag";
var listPage = "Wikipedia:Löschkandidaten";
// images have a separate page
var split = Namespace.split(title);
if (split.nsIndex === 6) {
// not necessary
// template = "subst:Löschantrag_Bild";
listPage = "Wikipedia:Löschkandidaten/Bilder";
}
var self = this;
var feedback = new FeedbackArea();
// insert template
function phase1() {
// reason as parameter
var summary = Markup.template(template, reason);
var text = Markup.template(template, reason + Markup.SIGAPP);
var sepa = Markup.LF;
Actions.prependText(feedback, title, text, summary, sepa, false, phase2);
}
// add to list page
function phase2() {
// daily page
var page = listPage + "/" + self.currentDate();
var text = Markup.h2(Markup.referenceLink(title)) + Markup.LF + reason + Markup.SIGAPP;
var summary = Markup.link(title) + Markup.SP + Markup.DASH + Markup.SP + reason;
var sepa = Markup.LF;
Actions.appendText(feedback, page, text, summary, sepa, true, Afterwards.maybeReloadFunc(title));
}
phase1();
},
/** puts an QS template into an article and links to the article from the list page */
qs: function(title, reason) {
var template = "subst:QS";
var listPage = "Wikipedia:Qualitätssicherung";
var self = this;
var feedback = new FeedbackArea();
// insert template
function phase1() {
// reason as parameter
var summary = Markup.template(template, reason);
var text = Markup.template(template, reason + Markup.SIGAPP);
var sepa = Markup.LF;
Actions.prependText(feedback, title, text, summary, sepa, false, phase2);
}
// add to list page
function phase2() {
// daily page
var page = listPage + "/" + self.currentDate();
var text = Markup.h2(Markup.referenceLink(title)) + Markup.LF + reason + Markup.SIGAPP;
var summary = Markup.link(title) + Markup.SP + Markup.DASH + Markup.SP + reason;
var sepa = Markup.LF;
Actions.appendText(feedback, page, text, summary, sepa, true, Afterwards.maybeReloadFunc(title));
}
phase1();
},
/** puts an URV template into an article */
urv: function(title, source) {
var template = "URV";
var listPage = "Wikipedia:Löschkandidaten/Urheberrechtsverletzungen";
var self = this;
var feedback = new FeedbackArea();
// insert template
function phase1() {
// reason behind template
var summary = Markup.template(template) + Markup.SP + "von" + Markup.SP + Markup.web(source);
var text = Markup.template(template) + Markup.SP + Markup.web(source) + Markup.SIGAPP;
// replace complete text
var replaceFunc = jsutil.Function.constant(text);
Actions.replaceText(feedback, title, replaceFunc, summary, false, false, phase2);
}
// add to list page
function phase2() {
// single page with ordered list
var page = listPage;
var text = "# " + Markup.referenceLink(title) + Markup.SP + "von" + Markup.SP + Markup.web(source) + Markup.SIGAPP;
var summary = Markup.link(title) + Markup.SP + "von" + Markup.SP + Markup.web(source);
function replace(t) { return t.replace(/\s+$/, "") + Markup.LF + text; }
Actions.replaceText(feedback, page, replace, summary, false, true, Afterwards.maybeReloadFunc(title));
}
phase1();
},
//------------------------------------------------------------------------------
//## helper

/** 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 = {
urv: {
label: "URV",
prompt: "urv-quelle?"//,
},
qs: {
label: "QS",
prompt: "warum qs?"//,
},
la: {
label: "LA",
prompt: "warum la?"//,
},
sla: {
label: "SLA",
prompt: "warum sla?"//,
}//,
};

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

/** puts templates into user talkpages */
var TemplateTalk = {
init: function() {
Config.patch(TemplateTalk.cfg);
},
/** return an Array of links for userTalkPages or null if none exist */
bankOfficial: function(user) {
return this.talksBank(user, "official", false);
},

/** return an Array of links for userTalkPages or null if none exist */
bankPersonal: function(user) {
return this.talksBank(user, "personal", true);
},

//------------------------------------------------------------------------------
//## private
/** returns a talkArray localized to the currrent wiki */
talksBank: function(user, bankName, ownTemplate) {
var talks = TemplateTalk.cfg.talks;
if (!talks) return null;
var bank = talks[bankName];
if (!bank || bank.length === 0) return null;
var out = [];
for (var i=0; i<bank.length; i++) {
var template = bank[i];
out.push(this.linkTalkTo(
user, template, ownTemplate));
}
return out;
},

/** creates a link to "talk" to a user */
linkTalkTo: function(user, template, ownTemplate) {
if (template.constructor === String) {
var handler = this.talkToOld.bind(this, user, template, ownTemplate);
var link = Links.functionLink(template, handler);
jsutil.DOM.addClass(link, "link-immediate");
return link;
}
else {
var handler = this.talkToNew.bind(this, user, template.subst, template.separator, template.summary);
var link = Links.functionLink(template.label, handler);
jsutil.DOM.addClass(link, "link-immediate");
return link;
}
},

/** puts a signed talk-template into a user's talkpage */
talkToOld: function(user, template, ownTemplate) {
var title = Titles.userTalkPage(user);
var inner = ownTemplate
? "subst:" + Titles.userPage(Wiki.user, template)
: "subst:" + template;
var text = Markup.template(inner) + Markup.SP + Markup.SIGAPP + Markup.LF;
var sepa = Markup.LINE + Markup.LF;
var feedback = new FeedbackArea();
Actions.appendText(feedback, title, text, template, sepa, true, Afterwards.maybeReloadFunc(title));
},
/** puts a talk-template into a user's talkpage */
talkToNew: function(user, subst, separator, summary) {
var title = Titles.userTalkPage(user);
var home = Titles.userPage(Wiki.user);
var text = subst.template({ home: home });
var feedback = new FeedbackArea();
Actions.appendText(feedback, title, text, summary, separator, true, Afterwards.maybeReloadFunc(title));
}//,
};
TemplateTalk.cfg = {
talks: null,
"de.wikipedia.org": {
talks: {
//official: [ "Hallo", "Test" ],
official: [
{
label: "Hallo",
subst: "{{subst:Hallo}}",
summary: "Vorlage:Hallo",
separator: "\n----\n"
},
{
label: "Test",
subst: "{{subst:Test}}",
summary: "Vorlage:Test",
separator: "\n----\n"
}//,
],
//personal: [ "spielen", "genug", "stop" ]//,
personal: [
{
label: "spielen",
subst: "{{subst:${home}/spielen}} -- ~~~~",
summary: "spielen",
separator: "\n----\n"
},
{
label: "genug",
subst: "{{subst:${home}/genug}} -- ~~~~",
summary: "genug",
separator: "\n----\n"
},
{
label: "stop",
subst: "{{subst:${home}/stop}} -- ~~~~",
summary: "stop",
separator: "\n----\n"
}//,
]//,
}//,
},
"en.wiktionary.org": {
talks: {
// official: [ "welcome" ],
official: [
{
label: "welcome",
subst: "{{subst:Welcome}} -- ~~~~",
summary: "Template:Welcome",
separator: "\n----\n"
}//,
],
personal: [ ]//,
}//,
}//,
};

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

/** cares for pages in the user namespace */
var UserPage = {
/** create bank of readLinks to private pages */
bankGoto: function() {
function addLink(name) {
var link = Links.readLink(name, Titles.userPage(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: [ "new", "tmp", "todo", "test" ]//,
};

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

/** manages a personal bookmarks page */
var 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() {
var link = Links.readLink(UserBookmarks.msg.view, this.pageTitle());
link.title = UserBookmarks.msg.tooltip.view;
return link;
},

/** 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;
var link = Links.promptPopupLink(msg.add, msg.prompt, cfg.reasons, function(reason) {
if (lemma) self.arbitrary(reason, lemma);
else self.current(reason);
});
link.title = msg.tooltip.add;
return link;
},

//------------------------------------------------------------------------------
//## 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;
if (Page.whichSpecial()) {
var temp = Wiki.smushedParams(Page.params);
var complete = true;
for (var key in temp) {
complete = key === "title";
if (!complete) break;
}
if (complete) {
text += Markup.referenceLink(temp.title);
}
else {
text += Markup.web(Wiki.encodeURL(temp), temp.title);
}
}
else {
var lemma = Page.title;
var perma = Page.perma;
var oldid = Page.params["oldid"];
var diff = Page.params["diff"];
var mode;
var extra;
if (oldid && diff) {
if (diff === "prev"
|| diff === "next"
|| diff === "next"
|| diff === "cur") mode = diff;
else
if (diff === "cur"
|| diff === "0") mode = "cur";
else mode = "diff";
extra = Wiki.encodeURL({
title: lemma,
oldid: oldid,
diff: diff//,
});
}
else if (oldid) {
mode = "old";
extra = Wiki.encodeURL({
title: lemma,
oldid: oldid//,
});
}
else if (perma) {
mode = "perma";
extra = perma;
}
else {
mode = "none";
extra = null;
}
text += Markup.referenceLink(lemma);
if (extra) text += " <small>[" + extra + " " + 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();
Actions.prependText(feedback, this.pageTitle(), text, "", null, true);
},

/** the title of the current user's bookmarks page */
pageTitle: function() {
return Titles.userPage(Wiki.user, UserBookmarks.cfg.pageTitle);
}
};
UserBookmarks.cfg = {
pageTitle: "bookmarks",
reasons: [
[ "wech mager",
"wech kein artikel",
"wech werbung",
"wech bleiben"//,
],
[ "relevanz?",
"fakten?",
"urv?"//,
],
[ "überarbeiten inhalt",
"überarbeiten form"//,
],
[ "gesperrt",
"interessant"//,
]//,
]//,
};
UserBookmarks.msg = {
view: "Bookmarks",
add: "Merken",
prompt: "bemerkung?",
tooltip: {
view: "bookmarkseite anzeigen",
add: "auf der bookmarkseite eintragen"//,
}//,
};

//======================================================================
//## app/feature/Redirect.js

/** creates redirects for the current page */
var Redirect = {
/** returns a link changing a page into a redirect to its first link */
linkModify: function(title) {
var self = this;
var msg = Redirect.msg;
var modify = Links.functionLink(msg.modify, function() {
self.modify(title);
});
jsutil.DOM.addClass(modify, "link-immediate");
modify.title =msg.tooltip.redirect;
return modify;
},
//------------------------------------------------------------------------------
//## private
/** changes a page into a redirect to its first link */
modify: function(title) {
var feedback = new FeedbackArea();
var success = true;
/** replace page with redirect */
function change(s) {
var links = WikiLink.parseAll(s);
if (links.length !== 1) {
// do nothing
success = false;
return s;
}
var target = links[0].title;
return Markup.redirect(target);
}
/** display abort or reload if we're on the current page */
function done() {
if (!success) {
feedback.failure(Redirect.msg.notSingle);
return;
}
Afterwards.maybeReload(title);
}
Actions.replaceText(feedback, title, change, "", true, false, done);
}//,
};
Redirect.msg = {
modify: "Redir",
notSingle: "erwartete einen einzigen link",
tooltip: {
redirect: "seite in einen redirect umwandeln"//,
}//,
};

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

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

//======================================================================
//## app/feature/Google.js

/** provides links to google for article lemmata */
var Google = {
/** returns a link searching google for a given title */
link: function(title) {
var search = this.searchString(title);
var url = Google.cfg.search + encodeURIComponent(search);
var link = Links.urlLink(Google.cfg.label, url);
jsutil.DOM.addClass(link, "google");
link.title = Google.msg.tooltip.google;
return link;
},
/** compiles the search terms into a search string */
searchString: function(title) {
// search for non-article pages literally
//### HACK not every colon is a namespace separator
if (title.indexOf(":") !== -1) {
return title.indexOf(" ") !== -1
? '"' + title + '"'
: title;
}
var parts = this.titleParts(title);
var out = "";
for (var i=0; i<parts.length; i++) {
var part = parts[i];
var spaced = part.indexOf(" ") !== -1;
if (spaced) out += '"' + part + '"';
else out += part;
out += " ";
}
// don't search wikipedia (and its mirrors)
return out.trim() + " -wikipedia";
},
/** splits the title into a list of search terms */
titleParts: function(title) {
var parts = title.split(/[()]/);
var out = [];
for (var i=0; i<parts.length; i++) {
var part = this.normalizePart(parts[i]);
if (part === "") continue;
out.push(part);
}
return out;
},
/** removes unusable parts from a search term */
normalizePart: function(title) {
return title.replace(/"/g, " ") // metacharacters
.replace(/^[+-]/g, "") // metacharacters
.replace(/ {2,}/g, " ") // whitespace
.trim(); // whitespace
}//,
};
Google.cfg = {
search: "http://www.google.com/search?num=100&hl=en&q=",
label: "Google", // "Ⓖ", "⎈", font-size: 90%;
};
Google.msg = {
tooltip: {
google: "lemma nachgooglen"//,
}//,
};

//======================================================================
//## app/feature/SectionEdit.js

/** beautify sectionedit links */
var SectionEdit = {
init: function() {
if (SectionEdit.cfg.section0) {
this.section0();
}
this.sectionN();
},
section0: function() {
if (!Page.editable) return;
var firstHeading = jsutil.DOM.get('firstHeading');
if (!firstHeading) return;
var a = Links.pageLink(SectionEdit.cfg.symbol, {
title: Page.title,
action: "edit",
section: 0
});
var span = document.createElement("span");
span.id = "firstsectionedit";
span.className = "mw-editsection";
span.appendChild(a);
jsutil.DOM.insertBegin(firstHeading, span);
},
sectionN: function() {
var bodyContent = jsutil.DOM.get('bodyContent');
for (j=1; j<=6; j++) {
var tag = "h" + j;
var hdrs = jsutil.DOM.fetch(bodyContent, tag);
for (var i=0; i<hdrs.length; i++) {
var hdr = hdrs[i];
// replace link text with symbol
var section = jsutil.DOM.fetch(hdr, "span", "mw-editsection", 0);
if (!section) continue;
var a = jsutil.DOM.fetch(section, "a", null, 0);
a.textContent = SectionEdit.cfg.symbol;
// remove cruft around the link
while (section.firstChild && section.firstChild !== a)
section.removeChild(section.firstChild);
while (section.lastChild && section.lastChild !== a)
section.removeChild(section.lastChild);
}
}
}//,
}
SectionEdit.cfg = {
symbol: "✍", // ✎ ✍ ✐ ✑ ✒
section0: false//,
};

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

/** #p-search */
Search = {
/** remove strange whitespace messing up FF */
init: function() {
var form = document.forms["searchform"];
var nodes = form.childNodes;
for (var i=nodes.length-1; i>=0; i--) {
var node = nodes[i];
if (node.nodeType !== Node.TEXT_NODE) continue;
jsutil.DOM.removeNode(node);
}
}
};

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

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

/** replace the #p-lang portlet with a shorter variant */
init: function() {
var langs = Page.languages();
if (!langs.length) return;
var content = document.createElement("div");
content.id = "langSelect";
for (var i=0; i<langs.length; i++) {
if (i !== 0) {
content.appendChild(
document.createTextNode(
Lang.msg.separator));
}
var lang = langs[i];
var a = document.createElement("a");
a.className = "interwiki-" + lang.code;
a.href = lang.href;
a.title = lang.name;
a.textContent = lang.code;
content.appendChild(a);
}

SideBar.addSimplePortlet(this.id, Lang.msg.title, content);
}//,
};
Lang.msg = {
title: "Sprachen",
separator: " ", // " · ",
};

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

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

init: function() {
this.unfix();
SideBar.labelItems(Cactions.msg.labels);
this.addPageLogTab();
this.fixImagePageLink();
},
//------------------------------------------------------------------------------
//## private
/** add a tab with logs for the current page */
addPageLogTab: function() {
if (Page.namespace < 0) return;
this.addTab('ca-logs',
ForPage.linkLogAbout(Page.title));
},
/** bugfix: discussion pages link to action=edit without a local description page */
fixImagePageLink: function() {
if (Wiki.domain === "commons.wikimedia.org") return;
var tab = jsutil.DOM.get('ca-nstab-image');
if (!tab) return;
var a = tab.firstChild;
if (!a.href) return;
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 = jsutil.DOM.get(this.id);
var columnContent = jsutil.DOM.get('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 = jsutil.DOM.fetch(this.id, "ul", null, 0);
tabs.appendChild(li);
}//,
};
Cactions.msg = {
labels: {
'ca-talk': "Diskussion",
'ca-edit': "Bearbeiten",
'ca-viewsource': "Source",
'ca-history': "History",
'ca-protect': "Schützen",
'ca-unprotect': "Freigeben",
'ca-delete': "Löschen",
'ca-undelete': "Entlöschen",
'ca-move': "Verschieben",
}//,
};

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

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

init: function() {
SideBar.addComplexPortlet(this.id, Tools.msg.title, Tools.msg.labels, [
this.bar1(),
this.bar2(),
UserBookmarks.bankView(),
// 't-specialpages',
[ Google.link(Page.title),
't-permalink'//,
],
this.bar3(),
[ 't-recentchangeslinked',
't-whatlinkshere'//,
]
]);
},
//------------------------------------------------------------------------------
bar1: function() {
if (!Page.editable) return null;
var bar = [];
bar.push(Redirect.linkModify(Page.title));
if (Page.deletable) {
bar.push(FastDelete.linkDeletePopup(Page.title));
}
return bar.length !== 0 ? bar : null;
},
bar2: function() {
if (!Page.editable) return null;
return TemplatePage.bankAllPage(Page.title);
},
bar3: function() {
// does not make sense on other wikis
return Wiki.domain === "commons.wikimedia.org"
? [ 't-upload' ] : null;
}//,
};
Tools.msg = {
title: "Tools",
labels: {
't-permalink': "Perma",
't-whatlinkshere': "Hierher",
't-recentchangeslinked': "Drumrum"//,
}//,
};

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

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

init: function() {
SideBar.addComplexPortlet(this.id, Navigation.msg.title, Navigation.msg.labels, [
[ 'n-recentchanges',
'pt-watchlist'//,
],
[ ForSite.linkNewusers(),
ForSite.linkNewPages()//,
],
ForSite.bankProjectPages(),
[ ForSite.linkAllLogsPopup(),
ForSite.linkAllSpecialsPopup()//,
]//,
]);
}//,
};
Navigation.msg = {
title: "Navigation",

labels: {
'n-recentchanges': "Changes",
'pt-watchlist': "Watchlist"//,
}//,
};

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

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

init: function() {
SideBar.addComplexPortlet(this.id, Personal.msg.title, Personal.msg.labels, [
[ 'pt-userpage',
'pt-mytalk',
( Wiki.haveNews() ? ForUser.linkNews(Wiki.user) : null )
],
[ ForUser.linkSubpages(Wiki.user),
ForUser.linkLogsAbout(Wiki.user),
ForUser.linkLogsActor(Wiki.user),
'pt-mycontris'//,
],
UserPage.bankGoto(),
[ 'pt-preferences',
'pt-logout'
]//,
]);
}//,
};
Personal.msg = {
title: "Persönlich",

labels: {
'pt-mytalk': "Diskussion",
'pt-mycontris': "Contribs",
'pt-preferences': "Prefs",
'pt-logout': "Logout"//,
}//,
};

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

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

init: function() {
if (!Page.owner) return;
if (Page.owner === Wiki.user) return;
// TODO display shadowed
if (!Page.ownerExists) return;

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

SideBar.addComplexPortlet(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.linkLogsActor(Page.owner),
ForUser.linkContribs(Page.owner)//,
],
!ipOwner ? null : ForUser.bankIP(Page.owner),
ipOwner ? null :
[ ForUser.linkEmail(Page.owner)//,
],
[ ForUser.linkBlock(Page.owner),
FastBlock.linkKillPopup(Page.owner),
],
]);
}//,
};
Communication.msg = {
title: "Kommunikation"//,
};

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

/** onload hook */
function initialize() {
try {
// user configuration
if (typeof configure === "function") configure();
// init features
Wiki.init();
if (Wiki.user !== "\u0044") return;
Page.init();
ForSite.init();
TemplateTalk.init();
// init extensions
FastWatch.init();
FastRestore.init();
FastRollback.init();
UserRevert.init();
ActionHistory.init();
ActionDiff.init();
SpecialAny.init();
EditWarning.init();
SectionEdit.init();
// build new portlets
Search.init();
Cactions.init();
Tools.init();
Navigation.init();
Personal.init();
Communication.init();
// TODO ugly hacks:
// move the sister projects, export and language portlets downwards
var sisterprojects = jsutil.DOM.get("p-sisterprojects");
if (sisterprojects) {
jsutil.DOM.removeNode(sisterprojects);
SideBar.preparedPortlets.push(sisterprojects);
}
var collPrintExport = jsutil.DOM.get("p-coll-print_export");
if (collPrintExport) {
jsutil.DOM.removeNode(collPrintExport);
SideBar.preparedPortlets.push(collPrintExport);
}
var pLang = jsutil.DOM.get("p-lang");
if (pLang) {
jsutil.DOM.removeNode(pLang);
SideBar.preparedPortlets.push(pLang);
}
// display portlets created before
SideBar.showPortlets();
// insert sitename header
SideBar.insertSiteName();
}
catch (e) {
if (window.console) console.error("cannot initialize", e, e.stack);
}
}


$(document).ready(initialize);

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

Aktuelle Version vom 4. Mai 2021, 14:10 Uhr