Benutzer:Schnark/js/search++.js

aus Wikipedia, der freien Enzyklopädie
Zur Navigation springen Zur Suche springen

Hinweis: Leere nach dem Veröffentlichen den Browser-Cache, um die Änderungen sehen zu können.

  • Firefox/Safari: Umschalttaste drücken und gleichzeitig Aktualisieren anklicken oder entweder Strg+F5 oder Strg+R (⌘+R auf dem Mac) drücken
  • Google Chrome: Umschalttaste+Strg+R (⌘+Umschalttaste+R auf dem Mac) drücken
  • Internet Explorer/Edge: Strg+F5 drücken oder Strg drücken und gleichzeitig Aktualisieren anklicken
  • Opera: Strg+F5
//Dokumentation unter [[Benutzer:Schnark/js/search++]] <nowiki>
/*global mediaWiki*/
(function ($, mw, libs) {
"use strict";
var l10n = {
//jscs:disable maximumLineLength
	en: {
		'pagetitle': '$1 - {{SITENAME}}',

		'search++': 'Search++',
		'search++-error': 'Internal error',
		'search++-no-propertyGetter': 'Internal error: Unknown property "$1"',
		'search++-no-searchEngines': 'Unknown function "$1"',
		'search++-no-search-get': 'The function "$1" can be used only to limit a search.',
		'search++-no-search-map': 'The function "$1" can\'t be used to map a result.',
		'search++-no-search-sort': 'The function "$1" can\'t be used to sort a result.',
		'search++-no-operator': 'Unknown operator "$1"',
		'search++-parser-error': 'Syntax error in query',
		'search++-search-go': 'Search',
		'search++-help-start': 'The syntax for queries is <code>function(arg1, arg2) OP function(arg) OP ... OP function()</code>. The arguments can be strings enclosed in double quotes, regular expression enclosed in slashes or whole numbers. The query will be executed from left to right. Parentheses are not possible directly (only by using the function <code>list</code>).',
		'search++-help-operator': 'The possible operators are <code>AND</code> (both criterions must be met), <code>OR</code> (one criterion must be met), <code>NOT</code> (the first, but not the second criterion must be met), <code>MAP</code> (the current result is mapped to a different set), and <code>SORT</code> (the current result is sorted).',
		'search++-help-functions': 'The possible functions are:',
		'search++-help-function': '<code>$1</code>: $2',

		'search++-help-function-list': 'Find all pages by the query.',
		'search++-help-function-search': 'Normal fulltext search for "search", optionally restricted to the specified namespaces (numerical, pipe-separated).',
		'search++-help-function-source': 'Find all pages with a source code containing the specified text.',
		'search++-help-function-sourceRE': 'Find all pages with a source code matched by the regular expression.',
		'search++-help-function-cattree': 'Find all pages in the category tree unto the specified depth.',
		'search++-help-function-prefix': 'Find all pages starting with the specified prefix.',
		'search++-help-function-title': 'Find all pages with a title matched by the regular expression. Can be used to sort by title.',
		'search++-help-function-iw': 'Find all pages with an interwiki link to the specified language, or with any language link. Can be used as map to translate the result. Please note that you cannot further process these translated titles.',
		'search++-help-function-links': 'Find all pages linking to the specified page.',
		'search++-help-function-transcludes': 'Find all pages transcluding the specified page.',
		'search++-help-function-file': 'Find all pages using the specified file.',
		'search++-help-function-url': 'Find all pages linking to the specified external page (without protocoll, can contain wildcards).',
		'search++-help-function-linkedOn': 'Find all pages linked on the specified page.',
		'search++-help-function-api': 'Find all pages by the following API query: action=query&list=list&query&format=json.',
		'search++-help-function-redir': 'Find all redirects. Can be used as map to resolve redirects.',
		'search++-help-function-minSize': 'Find all pages with the specified minimal size. Can be used to sort from smallest size to largest.',
		'search++-help-function-maxSize': 'Find all pages with the specified maximal size. Can be used to sort from largest size to smallest.'
	},
	de: {
		'pagetitle': '$1 – {{SITENAME}}',
		'search++': 'Suche++',
		'search++-error': 'Interner Fehler',
		'search++-no-propertyGetter': 'Interner Fehler: Unbekannte Eigenschaft "$1"',
		'search++-no-searchEngines': 'Unbekannte Funktion "$1"',
		'search++-no-search-get': 'Die Funktion "$1" kann nur zum Einschränken von Suchen verwendet werden.',
		'search++-no-search-map': 'Die Funktion "$1" kann nicht als Abbildung verwendet werden.',
		'search++-no-search-sort': 'Die Funktion "$1" kann nicht zum Sortieren verwendet werden.',
		'search++-no-operator': 'Unbekannter Operator "$1"',
		'search++-parser-error': 'Syntaxfehler in der Suchanfrage',
		'search++-search-go': 'Suchen',
		'search++-help-start': 'Die Syntax der Suchanfragen lautet <code>funktion(arg1, arg2) OP funktion(arg) OP ... OP funktion()</code>. Die Argumente können dabei in doppelte Anführungszeichen eingeschlossene Strings, in Schrägstriche eingeschlossene reguläre Ausdrücke oder ganze Zahlen sein. Der Ausdruck wird von links nach rechts ausgewertet, Klammern sind nicht direkt möglich (nur über die Funktion <code>list</code>).',
		'search++-help-operator': 'Die möglichen Operatoren sind <code>AND</code> (beide Kriterien müssen zutreffen), <code>OR</code> (ein Kriterium muss zutreffen), <code>NOT</code> (das erste, aber nicht das zweite Kriterium muss zutreffen), <code>MAP</code> (die aktuelle Liste wird mit einer Funktion auf eine neue Liste abgebildet) und <code>SORT</code> (die aktuelle Liste wird sortiert).',
		'search++-help-functions': 'Die möglichen Funktionen sind:',
		'search++-help-function-list': 'Liefert alle Seiten, auf die Suchanfrage zutrifft.',
		'search++-help-function-search': 'Gewöhnliche Volltextsuche nach "search", optional eingeschränkt auf die angegebenen Namensräume (numerisch, Pipe-separiert).',
		'search++-help-function-source': 'Ermittelt alle Seiten, deren Quelltext einen bestimmten Ausdruck enthält.',
		'search++-help-function-sourceRE': 'Ermittelt alle Seiten, deren Quelltext auf einen bestimmten regulären Ausdruck passen.',
		'search++-help-function-cattree': 'Ermittelt alle Seiten im Kategoriebaum einer bestimmten Tiefe.',
		'search++-help-function-prefix': 'Ermittelt alle Seiten, die mit einem bestimmten Präfix beginnen.',
		'search++-help-function-title': 'Ermittelt alle Seiten, deren Titel auf einen regulären Ausdruck passt. Kann zum Sortieren nach dem Titel verwendet werden.',
		'search++-help-function-iw': 'Ermittelt alle Seiten, die einen Interwikilink zu einer bestimmten oder überhaupt einer Sprache haben. Kann als Abbildung verwendet werden um das Ergebnis zu übersetzen. Diese übersetzten Titel können nicht mehr weiterverarbeitet werden.',
		'search++-help-function-links': 'Ermittelt alle Seiten, die auf eine bestimmte Seite verlinken.',
		'search++-help-function-transcludes': 'Ermittelt alle Seiten, die eine bestimmte Seite einbinden.',
		'search++-help-function-file': 'Ermittelt alle Seiten, die eine bestimmte Datei nutzen.',
		'search++-help-function-url': 'Ermittelt alle Seiten, die eine bestimmte externe Seite (ohne Protokoll, evt. mit Wildcard) verlinken.',
		'search++-help-function-linkedOn': 'Ermittelt alle Seiten, die auf einer bestimmten Seite verlinkt sind.',
		'search++-help-function-api': 'Ermittelt alle Seiten durch eine spezielle API-Abfrage: action=query&list=list&query&format=json.',
		'search++-help-function-redir': 'Ermittelt alle Weiterleitungen. Kann als Abbildung verwendet werden um Weiterleitungen aufzulösen.',
		'search++-help-function-minSize': 'Ermittelt alle Seiten mit einer bestimmten Mindestgröße. Kann zum Sortieren nach der Seitengröße (aufsteigend) verwendet werden.',
		'search++-help-function-maxSize': 'Ermittelt alle Seiten mit einer bestimmten Höchstgröße. Kann zum Sortieren nach der Seitengröße (absteigend) verwendet werden.'
	}
//jscs:enable maximumLineLength
},

//Backend ohne Funktionen für Eigenschaften und Suche
cachedProperties = {}, cachedApiQueries = {}, propertyGetter = {}, searchEngines = {},

hasOwn = Object.prototype.hasOwnProperty;

function addPropertyGetter (name, f) {
	propertyGetter[name] = f;
}
function addSearchEngine (name, o) {
	searchEngines[name] = o;
}

function setOr (l1, l2) {
	var i, l = [];
	for (i = 0; i < l1.length; i++) {
		l.push(l1[i]);
	}
	for (i = 0; i < l2.length; i++) {
		if (l.indexOf(l2[i]) === -1) {
			l.push(l2[i]);
		}
	}
	return l;
}
function setAnd (l1, l2) {
	var i, l = [];
	for (i = 0; i < l1.length; i++) {
		if (l2.indexOf(l1[i]) !== -1) {
			l.push(l1[i]);
		}
	}
	return l;
}
function setNot (l1, l2) {
	var i, l = [];
	for (i = 0; i < l1.length; i++) {
		if (l2.indexOf(l1[i]) === -1) {
			l.push(l1[i]);
		}
	}
	return l;
}

function doMulti (list, step, end) {
	var d = $.Deferred(), i = -1;
	function next () {
		i++;
		if (i === list.length) {
			d.resolve(end());
		} else {
			step(list[i]).then(next, d.reject);
		}
	}
	next();
	return d.promise();
}
function doWhile (test, step, end) {
	var d = $.Deferred();
	function next () {
		if (!test()) {
			d.resolve(end());
		} else {
			step().then(next, d.reject);
		}
	}
	next();
	return d.promise();
}

function getOneProperty (titles, p) {
	if (!hasOwn.call(cachedProperties, p)) {
		cachedProperties[p] = {};
	}
	var d = $.Deferred(), g, i, list = [];
	for (i = 0; i < titles.length; i++) {
		if (!hasOwn.call(cachedProperties[p], titles[i])) {
			list.push(titles[i]);
		}
	}
	if (list.length === 0) {
		d.resolve();
	} else {
		g = propertyGetter[p];
		if (!g) {
			d.reject(mw.msg('search++-no-propertyGetter', p));
		} else {
			g(list).then(function (data) {
				$.extend(cachedProperties[p], data);
				d.resolve();
			}, d.reject);
		}
	}
	return d.promise();
}
function getProperties (titles, props) {
	return doMulti(props, function (p) {
		return getOneProperty(titles, p);
	}, function () {
		var data = {}, i, j;
		for (i = 0; i < titles.length; i++) {
			data[titles[i]] = {};
			for (j = 0; j < props.length; j++) {
				data[titles[i]][props[j]] = cachedProperties[props[j]][titles[i]];
			}
		}
		return data;
	});
}

function queryGet (search, args) {
	var d = $.Deferred(), s = searchEngines[search];
	if (!s) {
		d.reject(mw.msg('search++-no-searchEngines', search));
	} else if (!s.get) {
		d.reject(mw.msg('search++-no-search-get', search));
	} else {
		s.get.apply(null, args).then(d.resolve, d.reject);
	}
	return d.promise();
}
function queryTest (search, args, list, neg) {
	var d = $.Deferred(), s = searchEngines[search];
	if (!s) {
		d.reject(mw.msg('search++-no-searchEngines', search));
	} else if (!s.test) {
		queryGet(search, args).then(function (list2) {
			if (neg) {
				d.resolve(setNot(list, list2));
			} else {
				d.resolve(setAnd(list, list2));
			}
		}, d.reject);
	} else {
		args.unshift('', '');
		getProperties(list, s.data || []).then(function (data) {
			var i, newList = [], r;
			for (i = 0; i < list.length; i++) {
				args[0] = list[i];
				args[1] = data[list[i]];
				r = s.test.apply(null, args);
				if (neg) {
					r = !r;
				}
				if (r) {
					newList.push(list[i]);
				}
			}
			d.resolve(newList);
		}, d.reject);
	}
	return d.promise();
}
function queryMap (search, args, list) {
	var d = $.Deferred(), s = searchEngines[search];
	if (!s) {
		d.reject(mw.msg('search++-no-searchEngines', search));
	} else if (!s.map) {
		d.reject(mw.msg('search++-no-search-map', search));
	} else {
		args.unshift('', '');
		getProperties(list, s.data || []).then(function (data) {
			var i, newList = [], r;
			for (i = 0; i < list.length; i++) {
				args[0] = list[i];
				args[1] = data[list[i]];
				r = s.map.apply(null, args);
				if (Array.isArray(r)) {
					newList = setOr(newList, r);
				} else if (r) {
					newList = setOr(newList, [r]);
				}
			}
			d.resolve(newList);
		}, d.reject);
	}
	return d.promise();
}
function querySort (search, args, list) {
	var d = $.Deferred(), s = searchEngines[search];
	if (!s) {
		d.reject(mw.msg('search++-no-searchEngines', search));
	} else if (!s.sort) {
		d.reject(mw.msg('search++-no-search-sort', search));
	} else {
		args.unshift('', '');
		getProperties(list, s.data || []).then(function (data) {
			var i, keys = {};
			for (i = 0; i < list.length; i++) {
				args[0] = list[i];
				args[1] = data[list[i]];
				keys[list[i]] = s.sort.apply(null, args);
			}
			d.resolve(list.sort(function (a, b) {
				return keys[a] > keys[b] ? 1 : (keys[a] < keys[b] ? -1 : 0);
			}));
		}, d.reject);
	}
	return d.promise();
}
function doQuery (query) {
	var curList = [];
	return doMulti(query, function (q) {
		var d = $.Deferred();
		switch (q[0]) {
		case 'GET': case 'OR':
			queryGet(q[1], q[2]).then(function (list) {
				curList = setOr(curList, list);
				d.resolve();
			}, d.reject);
			break;
		case 'AND': case 'NOT':
			queryTest(q[1], q[2], curList, q[0] === 'NOT').then(function (list) {
				curList = list;
				d.resolve();
			}, d.reject);
			break;
		case 'MAP':
			queryMap(q[1], q[2], curList).then(function (list) {
				curList = list;
				d.resolve();
			}, d.reject);
			break;
		case 'SORT':
			querySort(q[1], q[2], curList).then(function (list) {
				curList = list;
				d.resolve();
			}, d.reject);
			break;
		default:
			d.reject(mw.msg('search++-no-operator', q[0]));
		}
		return d.promise();
	}, function () {
		return curList;
	});
}

function parseArg (a) {
	var i;
	switch (a.charAt(0)) {
	case '"': return JSON.parse(a);
	case '/':
		i = a.lastIndexOf('/');
		return new RegExp(a.slice(1, i), a.slice(i + 1));
	default: return Number(a);
	}
}
function makeManyRE (re) {
	//(?:(foo))* liefert nur das letzte foo, wir wollen aber alle
	//hässliche Lösung, aber besser als denken zu müssen
	var n = 10, i, ret = '';
	for (i = 0; i < n; i++) {
		ret += '(?:' + re + ')?';
	}
	return ret;
}
function parseFunction (f) {
	var reString = '"(?:\\\\.|[^"])*"',
		reRegexp = '/(?:\\\\.|[^/])*/[a-z]*',
		reNumber = '\\d+',
		reArgument = '(' + reString + '|' + reRegexp + '|' + reNumber + ')',
		reArglist = reArgument + makeManyRE('\\s*,\\s*' + reArgument),
		reFunction = '^([a-zA-Z]+)\\s*\\(\\s*(?:' + reArglist + ')?\\s*\\)$',
		re = new RegExp(reFunction),
		result = re.exec(f), i = 2, fa = [];
	fa[0] = result[1];
	fa[1] = [];
	while (result[i]) {
		fa[1][i - 2] = parseArg(result[i]);
		i++;
	}
	return fa;
}
function parseQuery (q) {
	var reString = '"(?:\\\\.|[^"])*"',
		reRegexp = '/(?:\\\\.|[^/])*/[a-z]*',
		reNumber = '\\d+',
		reArgument = '(?:' + reString + '|' + reRegexp + '|' + reNumber + ')',
		reArglist = reArgument + '(?:\\s*,\\s*' + reArgument + ')*',
		reFunction = '[a-zA-Z]+\\s*\\(\\s*(?:' + reArglist + ')?\\s*\\)',
		reKey = 'AND|OR|NOT|MAP|SORT',
		reQuery = '^\\s*(' + reFunction + ')' + makeManyRE('\\s+(' + reKey + ')\\s+(' + reFunction + ')') + '\\s*$',
		re = new RegExp(reQuery),
		result = re.exec(q),
		query = [], i = 2, fa;
	if (!result) {
		return false;
	}
	fa = parseFunction(result[1]);
	query[0] = ['GET', fa[0], fa[1]];
	while (result[i]) {
		fa = parseFunction(result[i + 1]);
		query[i / 2] = [result[i], fa[0], fa[1]];
		i += 2;
	}
	return query;
}

function queryByString (q) {
	q = parseQuery(q);
	if (!q) {
		return $.Deferred().reject(mw.msg('search++-parser-error')).promise();
	}
	return doQuery(q);
}

//API
function ajaxGet (data) {
	data = $.param(data);
	if (!cachedApiQueries[data]) {
		cachedApiQueries[data] = $.getJSON(mw.util.wikiScript('api'), data);
	}
	return cachedApiQueries[data];
}
function queryMwApiListWithCont (name, param, cont) {
	var d = $.Deferred();
	param.action = 'query';
	param.list = name;
	param.format = 'json';
	param.formatversion = 2;
	if (cont) {
		$.extend(param, cont);
	} else {
		param['continue'] = '';
	}
	ajaxGet(param).then(function (json) {
		if (!json || !json.query || !json.query[name]) {
			d.reject(mw.msg('search++-error'));
		} else {
			d.resolve(json.query[name].map(function (o) {
				return o.title;
			}), json['continue']);
		}
	}, function () {
		d.reject(mw.msg('search++-error'));
	});
	return d.promise();
}
function queryMwApiList (name, param, count) {
	var list = [], cont, done = false, i = 0;
	return doWhile(function () {
		return !done && i++ < (count || 100);
	}, function () {
		var d = $.Deferred();
		queryMwApiListWithCont(name, param, cont).then(function (l, c) {
			list = setOr(list, l);
			if (!c) {
				done = true;
			} else {
				cont = c;
			}
			d.resolve();
		}, d.reject);
		return d.promise();
	}, function () {
		return list;
	});
}
function queryMwApiPropWithCont (name, param, list, extract, cont) {
	var d = $.Deferred();
	param.action = 'query';
	param.prop = name;
	param.titles = list.join('|');
	param.format = 'json';
	param.formatversion = 2;
	if (cont) {
		$.extend(param, cont);
	} else {
		param['continue'] = '';
	}
	ajaxGet(param).then(function (json) {
		var i, page, result = {}, res;
		if (!json || !json.query || !json.query.pages) {
			d.reject(mw.msg('search++-error'));
		} else {
			for (i = 0; i < json.query.pages.length; i++) {
				page = json.query.pages[i];
				res = extract(page, json.query);
				result[page.title] = res; //extract kann page.title aktualisieren (für Weiterleitungen)
			}
			d.resolve(result, json['continue']);
		}
	}, function () {
		d.reject(mw.msg('search++-error'));
	});
	return d.promise();
}
function queryMwApiPropSome (name, param, list, extract, count) {
	var data = {}, cont, done = false, i = 0;
	return doWhile(function () {
		return !done && i++ < (count || 100);
	}, function () {
		var d = $.Deferred();
		queryMwApiPropWithCont(name, param, list, extract, cont).then(function (r, c) {
			var k;
			for (k in r) {
				if (hasOwn.call(r, k)) {
					if (hasOwn.call(data, k)) {
						if (Array.isArray(data[k])) {
							data[k] = setOr(data[k], r[k]);
						} else {
							$.extend(data[k], r[k]);
						}
					} else {
						data[k] = r[k];
					}
				}
			}
			if (!c) {
				done = true;
			} else {
				cont = c;
			}
			d.resolve();
		}, d.reject);
		return d.promise();
	}, function () {
		return data;
	});
}
function queryMwApiProp (name, param, list, extract, count1, count2) {
	var result = {};
	return doWhile(function () {
		return list.length > 0;
	}, function () {
		var d = $.Deferred(), titles = list.slice(0, count1 || 50);
		queryMwApiPropSome(name, param, titles, extract, count2).then(function (data) {
			var title;
			titles = [];
			for (title in data) {
				if (hasOwn.call(data, title)) {
					titles.push(title);
					result[title] = data[title];
				}
			}
			list = setNot(list, titles);
			d.resolve();
		}, d.reject);
		return d.promise();
	}, function () {
		return result;
	});
}

//Funktionen für Eigenschaften
addPropertyGetter('text', function (titles) {
	return queryMwApiProp('revisions',
		{rvprop: 'content', rvslots: 'main'},
		titles,
		function (data) {
			return data.revisions[0].slots.main.content;
		});
});
addPropertyGetter('subcats', function (cats) {
	var data = {};
	return doMulti(cats, function (cat) {
		var d = $.Deferred();
		queryMwApiList('categorymembers', {
			cmtype: 'subcat',
			cmtitle: cat,
			cmlimit: 'max'
		}).then(function (list) {
			data[cat] = list;
			d.resolve();
		}, d.reject);
		return d.promise();
	}, function () {
		return data;
	});
});
addPropertyGetter('catmembers', function (cats) {
	var data = {};
	return doMulti(cats, function (cat) {
		var d = $.Deferred();
		queryMwApiList('categorymembers', {
			cmtype: 'page',
			cmtitle: cat,
			cmlimit: 'max'
		}).then(function (list) {
			data[cat] = list;
			d.resolve();
		}, d.reject);
		return d.promise();
	}, function () {
		return data;
	});
});
addPropertyGetter('iw', function (titles) {
	return queryMwApiProp('langlinks', {lllimit: 'max'}, titles, function (data) {
		return [(data.langlinks || []).map(function (ll) {
			return ll.lang;
		}), (data.langlinks || []).map(function (ll) {
			return ll.title;
		})];
	});
});
addPropertyGetter('size', function (titles) {
	return queryMwApiProp('info', {}, titles, function (data) {
		return Number(data.length);
	});
});
addPropertyGetter('redir', function (titles) {
	return queryMwApiProp('', {redirects: true}, titles, function (data, query) {
		var i, redirs = query.redirects || [];
		for (i = 0; i < redirs.length; i++) {
			if (data.title === redirs[i].to) {
				data.title = redirs[i].from;
				return redirs[i].to;
			}
		}
		return false;
	});
});

//Funktionen für Suchen
addSearchEngine('list', {
	get: queryByString,
	param: 'query',
	helpMsg: 'search++-help-function-list'
});
addSearchEngine('search', {
	get: function (s, ns) {
		var param = {
			srsearch: s,
			srlimit: 50
		};
		if (ns) {
			param.srnamespace = ns;
		}
		return queryMwApiList('search', param, 1);
	},
	param: 'search [, namespaces]',
	helpMsg: 'search++-help-function-search'
});
addSearchEngine('source', {
	data: ['text'],
	test: function (title, data, search) {
		return data.text.indexOf(search) > -1;
	},
	param: 'search',
	helpMsg: 'search++-help-function-source'
});
addSearchEngine('sourceRE', {
	data: ['text'],
	test: function (title, data, search) {
		return search.test(data.text);
	},
	param: 'search',
	helpMsg: 'search++-help-function-sourceRE'
});
addSearchEngine('cattree', {
	get: function (cat, depth) {
		var cats = [cat], d = $.Deferred();
		if (depth === undefined) {
			depth = 3;
		}
		doMulti(new Array(depth), function () {
			var d = $.Deferred();
			getProperties(cats, ['subcats']).then(function (data) {
				var x;
				for (x in data) {
					if (hasOwn.call(data, x)) {
						cats = setOr(cats, data[x].subcats);
					}
				}
				d.resolve();
			}, d.reject);
			return d.promise();
		}, function () {}).then(function () {
			getProperties(cats, ['catmembers']).then(function (data) {
				var list = [], x;
				for (x in data) {
					if (hasOwn.call(data, x)) {
						list = setOr(list, data[x].catmembers);
					}
				}
				d.resolve(list);
			}, d.reject);
		}, d.reject);
		return d.promise();
	},
	param: 'cat [, depth]',
	helpMsg: 'search++-help-function-cattree'
});
addSearchEngine('prefix', {
	get: function (prefix) {
		return queryMwApiList('allpages', {apprefix: prefix, aplimit: 'max'});
	},
	test: function (title, data, prefix) {
		return title.indexOf(prefix) === 0;
	},
	param: 'prefix',
	helpMsg: 'search++-help-function-prefix'
});
addSearchEngine('title', {
	test: function (title, data, re) {
		return re.test(title);
	},
	sort: function (title) {
		return title;
	},
	param: 'regexp',
	helpMsg: 'search++-help-function-title'
});
addSearchEngine('iw', {
	data: ['iw'],
	test: function (title, data, lang) {
		if (lang) {
			return data.iw[0].indexOf(lang) > -1;
		}
		return !!data.iw[0].length;
	},
	map: function (title, data, lang) {
		var i, ret = [];
		if (lang) {
			i = data.iw[0].indexOf(lang);
			if (i > -1) {
				return ':' + lang + ':' + data.iw[1][i];
			}
		} else {
			for (i = 0; i < data.iw[0].length; i++) {
				ret.push(':' + data.iw[0][i] + ':' + data.iw[1][i]);
			}
			return ret.length ? ret : false;
		}
	},
	param: '[lang]',
	helpMsg: 'search++-help-function-iw'
});
addSearchEngine('links', {
	get: function (title) {
		return queryMwApiList('backlinks', {bltitle: title, bllimit: 'max', blnamespace: 0});
	},
	param: 'title',
	helpMsg: 'search++-help-function-links'
});
addSearchEngine('transcludes', {
	get: function (title) {
		return queryMwApiList('embeddedin', {eititle: title, einamespace: 0, eilimit: 'max'});
	},
	param: 'title',
	helpMsg: 'search++-help-function-transcludes'
});
addSearchEngine('file', {
	get: function (title) {
		return queryMwApiList('imageusage', {iutitle: title, iulimit: 'max', iunamespace: 0});
	},
	param: 'title',
	helpMsg: 'search++-help-function-file'
});
addSearchEngine('url', {
	get: function (url) {
		return queryMwApiList('exturlusage', {euquery: url, eulimit: 'max'});
	},
	param: 'url',
	helpMsg: 'search++-help-function-url'
});
addSearchEngine('linkedOn', {
	get: function (title) {
		var d = $.Deferred();
		queryMwApiProp('links',
			{plnamespace: 0, pllimit: 'max'},
			[title],
			function (data) {
				return (data.links || []).map(function (l) {
					return l.title;
				});
			}).then(function (data) {
				d.resolve(data[title]);
			}, d.reject);
		return d.promise();
	},
	param: 'title',
	helpMsg: 'search++-help-function-linkedOn'
});
addSearchEngine('api', {
	get: function (list, query) {
		var i, j, q = query.split('&');
		query = {};
		for (i = 0; i < q.length; i++) {
			j = q[i].indexOf('=');
			query[q[i].slice(0, j)] = q[i].slice(j + 1);
		}
		return queryMwApiList(list, query);
	},
	param: 'list, query',
	helpMsg: 'search++-help-function-api'
});
addSearchEngine('redir', {
	data: ['redir'],
	test: function (title, data) {
		return !!data.redir;
	},
	map: function (title, data) {
		return data.redir || title;
	},
	helpMsg: 'search++-help-function-redir'
});
addSearchEngine('minSize', {
	data: ['size'],
	test: function (title, data, size) {
		return data.size >= size;
	},
	sort: function (title, data) {
		return data.size;
	},
	param: 'size',
	helpMsg: 'search++-help-function-minSize'
});
addSearchEngine('maxSize', {
	data: ['size'],
	test: function (title, data, size) {
		return data.size <= size;
	},
	sort: function (title, data) {
		return -data.size;
	},
	param: 'size',
	helpMsg: 'search++-help-function-maxSize'
});

//Frontend
function initL10N (l10n, keep) {
	var i, chain = mw.language.getFallbackLanguageChain();
	keep = $.grep(mw.messages.get(keep), function (val) {
		return val !== null;
	});
	for (i = chain.length - 1; i >= 0; i--) {
		if (chain[i] in l10n) {
			mw.messages.set(l10n[chain[i]]);
		}
	}
	mw.messages.set(keep);
}

function translate (o, key) {
	if (o[key + 'Msg'] && mw.messages.exists(o[key + 'Msg'])) {
		return mw.msg(o[key + 'Msg']);
	}
	return o[key] || '';
}

function getHelp () {
	var html = '', list;
	list = $.map(searchEngines, function (val, key) {
		return mw.msg('search++-help-function', key + '(' + translate(val, 'param') + ')', translate(val, 'help'));
	});
	list = mw.html.element('ul', {}, new mw.html.Raw(list.map(function (e) {
		return mw.html.element('li', {}, new mw.html.Raw(e));
	}).join('')));
	html += mw.html.element('p', {}, new mw.html.Raw(mw.msg('search++-help-start')));
	html += mw.html.element('p', {}, new mw.html.Raw(mw.msg('search++-help-operator')));
	html += mw.html.element('p', {}, new mw.html.Raw(mw.msg('search++-help-functions') + list));
	return html;
}
function formatError (error) {
	return mw.html.element('div', {'class': 'error'}, error);
}
function formatList (list) {
	return mw.html.element('ol', {}, new mw.html.Raw(
		list.map(function (title) {
			return mw.html.element('li', {},
				new mw.html.Raw(mw.html.element('a', {href: mw.util.getUrl(title)}, title))
			);
		}).join('')
	));
}
function queryByInterface ($from, $to) {
	function show (html) {
		$.removeSpinner('search-extended');
		$to.html(html);
		mw.hook('wikipage.content').fire($to);
	}
	$to.empty().injectSpinner('search-extended');
	queryByString($from.val()).then(function (list) {
		show(formatList(list));
	}, function (error) {
		show(formatError(error));
	});
}
function showInterface ($content) {
	var html = '';
	html += mw.html.element('div', {id: 'search-extended-resultbox'}, '');
	html += mw.html.element('textarea', {id: 'search-extended-inputbox', rows: 5, cols: 80}, '');
	html += mw.html.element('input',
		{id: 'search-extended-searchbutton', type: 'button', value: mw.msg('search++-search-go')});
	html += mw.html.element('div', {}, new mw.html.Raw(getHelp()));
	$content.html(html);
	$('#search-extended-searchbutton').on('click', function () {
		queryByInterface($('#search-extended-inputbox'), $('#search-extended-resultbox'));
	});
}

//Init
function setup () {
	if (mw.config.get('wgNamespaceNumber') !== -1 || mw.config.get('wgTitle') !== 'Search++') {
		return;
	}
	mw.loader.using(['mediawiki.util', 'mediawiki.language', 'mediawiki.jqueryMsg', 'jquery.spinner']).then(function () {
		document.title = mw.msg('pagetitle', mw.msg('search++'));
		$(function () {
			$('#firstHeading').text(mw.msg('search++'));
			showInterface($('#mw-content-text'));
		});
	});
}

mw.loader.using('mediawiki.language').then(function () {
	initL10N(l10n, ['pagetitle']);
});

libs['search++'] = {
	addPropertyGetter: addPropertyGetter,
	addSearchEngine: addSearchEngine,
	getHelp: getHelp,
	doQuery: doQuery,
	queryByString: queryByString
};

setup();

})(jQuery, mediaWiki, mediaWiki.libs);
//</nowiki>