Zum Inhalt springen

Benutzer:Schnark/js/syntaxhighlight.js

aus Wikipedia, der freien Enzyklopädie
Dies ist eine alte Version dieser Seite, zuletzt bearbeitet am 20. Dezember 2014 um 11:05 Uhr durch Schnark (Diskussion | Beiträge) (mehr führende Leerzeichen erlauben). Sie kann sich erheblich von der aktuellen Version unterscheiden.

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
  • Edge: Strg+F5 drücken oder Strg drücken und gleichzeitig Aktualisieren anklicken
//based on [[mw:User:Remember the dot/Syntax highlighter.js]] with a completely different parser
//Dokumentation unter [[Benutzer:Schnark/js/syntaxhighlight]]
//<nowiki>
/*global mediaWiki*/

(function($, mw, libs) {
"use strict";

var	map, protocols,
	wikiSyntax, cssSyntax, jsSyntax, jsonSyntax, luaSyntax,
	defaultWikitextColors, defaultCssColors, defaultJsColors, strongColors;

map = {
	opera: [['>=', 15]],
	msie: false
};

function clone (array) {
	return [Array.prototype.slice.call(array[0]), Array.prototype.slice.call(array[1])];
}

function Parser (syntax) {
	var re, res = [];

	this.syntax = {};
	this.syntax.noparse = syntax.noparse || {};
	this.syntax.fn = [];

	for (re in syntax.parse) {
		res.push('(' + re + ')');
		this.syntax.fn.push(syntax.parse[re]);
	}
	this.syntax.re = new RegExp(res.join('|'), 'g');

	this.syntax.eol = syntax.eol || function (o) {
		return o;
	};

	this.syntax.parens = syntax.parens || {
		open: '([{',
		close: ')]}'
	};
	this.syntax.parens.all = this.syntax.parens.open + this.syntax.parens.close;

	this.cache = {};
}

Parser.prototype = {
	open: function (type) {
		this.openTags.push(type);
	},
	close: function (type) {
		if (this.openTags.length === 0) {
			return;
		}
		if (this.openTags[this.openTags.length - 1] === type) {
			this.openTags.pop();
			return;
		}
		var i = this.openTags.lastIndexOf(type);
		if (i > -1) {
			this.openTags.length = i;
		}
	},
	isOpen: function (type) {
		if (this.openTags.length === 0) {
			return false;
		}
		if (this.openTags[this.openTags.length - 1] === type) {
			return true;
		}
		return this.openTags.lastIndexOf(type) > -1;
	},
	current: function () {
		return this.openTags[this.openTags.length - 1] || '';
	},

	noparse: function () {
		return this.syntax.noparse[this.current()] || false;
	},
	updateNoparse: function (type, re) {
		this.syntax.noparse[type] = re;
	},
	exec: function (re) {
		re.lastIndex = this.pos;
		return re.exec(this.text);
	},
	getText: function (l) {
		return this.text.substring(this.pos, this.pos + l);
	},

	write: function (text) {
		if (text === '') {
			return;
		}
		this.pos += text.length;
		this.output.push([text, this.current()]);
	},

	parse: function (text) {
		if (text === this.oldText) {
			return this.oldParse;
		}
		this.oldText = text;
		this.oldParse = [];
		var i, open = [], ret, par = text.split('\n');
		for (i = 0; i < par.length; i++) {
			ret = this.parseParagraph(par[i], open);
			this.oldParse = this.oldParse.concat(ret[0]);
			open = this.syntax.eol(ret[1]);
		}
		return this.oldParse;
	},
	parseParagraph: function (par, open) {
		if (par === '') {
			return [
				[['\n', open[open.length - 1] || '']],
				open
			];
		}
		var key = open.join('|');
		if (!this.cache[key]) {
			this.cache[key] = {};
		}
		if (!this.cache[key][par + '\n']) {
			this.cache[key][par + '\n'] = this.reallyParseParagraph(par, open);
		}
		return clone(this.cache[key][par + '\n']);
	},
	reallyParseParagraph: function (par, open) {
		var noparse, result, i, word;
		this.output = [];
		this.openTags = open;
		this.text = par;
		this.pos = 0;

		while (this.pos < this.text.length) {
			noparse = this.noparse();
			if (noparse) {
				result = this.exec(noparse);
				if (result) {
					this.write(this.getText(result.index + result[0].length - this.pos));
					this.close(this.current());
				} else {
					this.write(this.text.substring(this.pos));
				}
			} else {
				result = this.exec(this.syntax.re);
				if (result) {
					this.write(this.getText(result.index - this.pos));
					for (i = 0; i < this.syntax.fn.length; i++) {
						if (result[i + 1]) {
							word = result[i + 1];
							this.syntax.fn[i].call(this, word, this.text.substring(this.pos + word.length), this.text.substring(0, this.pos));
							break;
						}
					}
				} else {
					this.write(this.text.substring(this.pos));
				}
			}
		}

		this.write('\n');
		return clone([this.output, this.openTags]);
	},


	findMatchingParen: function (text, pos) {
		var me = text.charAt(pos - 1), other, myPos = pos - 1, dir, depth = 1;
		if (me === '' || this.syntax.parens.all.indexOf(me) === -1) {
			me = text.charAt(pos);
			myPos = pos;
		}
		if (me === '' || this.syntax.parens.all.indexOf(me) === -1) {
			return false;
		}
		dir = this.syntax.parens.open.indexOf(me) === -1 ? -1 : 1;
		other = this.syntax.parens[dir === 1 ? 'close' : 'open'].charAt(this.syntax.parens[dir === 1 ? 'open' : 'close'].indexOf(me));
		pos = myPos;
		do {
			pos += dir;
			if (text.charAt(pos) === me) {
				depth++;
			} else if (text.charAt(pos) === other) {
				depth--;
				if (depth === 0) {
					return [Math.min(pos, myPos), Math.max(pos, myPos)];
				}
			}
		} while (text.charAt(pos) !== '');
		return false;
	},
	parseWithParen: function (text, pos) {
		var output = this.parse(text), parens = this.findMatchingParen(text, pos), newOutput = [], i, j = 0, oldLen = 0, newLen, t;
		if (!parens) {
			return output;
		}
		parens.push(Infinity);
		for (i = 0; i < output.length; i++) {
			t = output[i][0];
			newLen = oldLen + t.length;
			while (parens[j] < newLen) {
				if (parens[j] > oldLen) {
					newOutput.push([t.substring(0, parens[j] - oldLen), output[i][1]]);
					t = t.substring(parens[j] - oldLen);
				}
				newOutput.push([t.charAt(0), 'matching-paren']);
				t = t.substring(1);
				oldLen = parens[j] + 1;
				j++;
			}
			if (t) {
				newOutput.push([t, output[i][1]]);
			}
			oldLen = newLen;
		}
		return newOutput;
	}
};

function makeParserFunctionOpen (type) {
	return function (text) {
		this.open(type);
		this.write(text);
	};
}
function makeParserFunctionType (type) {
	return function (text) {
		this.open(type);
		this.write(text);
		this.close(type);
	};
}
function makeParserFunctionClose (type) {
	return function (text) {
		this.write(text);
		this.close(type);
	};
}

protocols = new RegExp('^(?:' + mw.config.get('wgUrlProtocols') + ')');
function parsePlainLink (proto, text) {
	/*jshint validthis: true*///Parser-Funktion mit explizitem Kontext
	var link = /[^ <>|\[\]]*/.exec(text)[0], punc = ',;.:!?';
	if (link.indexOf('(') === -1) {
		punc += ')';
	}
	while (link && punc.indexOf(link.charAt(link.length - 1)) !== -1) {
		link = link.substring(0, link.length - 1);
	}
	this.open('externalLink');
	this.write(proto + link);
	this.close('externalLink');
}
function keywords (words, type) {
	var i, syntax = {};
	for (i = 0; i < words.length; i++) {
		syntax['\\b' + words[i] + '\\b'] = makeParserFunctionType(type || 'keyword');
	}
	return syntax;
}

wikiSyntax = {
noparse: {
	'comment': /-->/g,
	'<hiero>': /<\/hiero>/gi,
	'<math>': /<\/math>/gi,
	'<nowiki>': /<\/nowiki>/gi,
	'<pre>': /<\/pre>/gi,
	'<score>': /<\/score>/gi,
	'<source>': /<\/source>/gi,
	'<syntaxhighlight>': /<\/syntaxhighlight>/gi,
	'<templatedata>': /<\/templatedata>/gi,
	'<timeline>': /<\/timeline>/gi
},
parse: {
	'^=': makeParserFunctionOpen('heading'),
	'^ *:*\\{\\|': makeParserFunctionOpen('table'),
	'^ *\\|-+|^ *\\|\\+|^ *[|!]|\\|\\||!!': function (s, next) {
		if (this.current() === 'table' && !(s === '|' && next.charAt(0) === '}')) {
			this.open('table-syntax');
			this.write(s);
			this.close('table-syntax');
		} else {
			this.write(s);
		}
	},
	'^ ': function () {
		if (!this.isOpen('template') && !this.isOpen('table')) {
			this.open('pre');
		}
		this.write(' ');
	},
	'^[*#:;]+': makeParserFunctionType('listAndIndent'),
	'^-{4,}': makeParserFunctionType('hr'),
	'\\[\\[': makeParserFunctionOpen('wikilink'),
	'\\[': function (x, text) {
		if (protocols.test(text)) {
			this.close('externalLink');
			this.open('externalLink');
		}
		this.write('[');
	},
	'\\]': function (x, text) {
		if (this.isOpen('externalLink')) {
			this.write(']');
			this.close('externalLink');
		} else if (text.charAt(0) === ']') {
			this.write(']]');
			this.close('wikilink');
		} else {
			this.write(']');
		}
	},
	'<!--': makeParserFunctionOpen('comment'),
	'</?[a-zA-Z]+[^>]*>': function (tag) {
		var	tagParts = (/<(\/?)([a-z]+)([^>]*)>/i).exec(tag),
			tagname = tagParts[2].toLowerCase(),
			selfclosing = tagParts[3] && tagParts[3].charAt(tagParts[3].length - 1) === '/',
			singleTag = ['br', 'hr', 'wbr', 'nowiki', 'ref', 'references', 'section'],
			selfclosingTag = ['br', 'hr', 'wbr'];
		if (tagParts[1]) {
			this.write(tag);
			this.close('<' + tagname + '>');
		} else if ((selfclosing && $.inArray(tagname, singleTag) > -1) || $.inArray(tagname, selfclosingTag) > -1) {
			this.open('<' + tagname + '>');
			this.write(tag);
			this.close('<' + tagname + '>');
		} else {
			this.open('<' + tagname + '>');
			this.write(tag);
		}
	},
	'\\{\\{+': function (braces, text) {
		//Multiple braces are either parameters, or templates, or just braces. It is impossible to tell, unless you go ahead and check in which order they are closed. That's something we just wo'n't do, so this code might fail, but it should work for all common patterns.

		//if the following text starts with # or !}}, the last pair of braces is start of a "template" (well, of a parser function)
		var isTemplate = (text.charAt(0) === '#' || (text.charAt(0) === '!' && text.indexOf('!}}') === 0)), count = braces.length - (isTemplate ? 2 : 0), i;
		if (count === 1) {
			this.write('{'); //{{{!}} is { + {{!}}
		} else {
			switch (count % 3) {
			case 1: //especially {{{{ gets a template with a name specified by "template" (parser function in most cases), count is at least 4
				this.open('template');
				this.write('{{');
				count -= 2;
			/*falls through*/
			case 2: //especially {{{{{ gets a template with a name specified by parameter
				this.open('template');
				this.write('{{');
				count -= 2;
			/*falls through*/
			case 0: //especially {{{{{{ gets a parameter with a name specified by parameter
				for (i = 0; i < count / 3; i++) {
					this.open('parameter');
					this.write('{{{');
				}
			}
		}
		if (isTemplate) {
			this.open('template');
			this.write('{{');
		}
	},
	'\\}': function (x, text, before) {
		var closeTable = (/^ *\|$/.test(before)), count = 1, i;
		if (text.charAt(0) === '}') {
			count = 2;
			if (text.charAt(1) === '}') {
				count = 3;
			}
		}
		if (count === 1) {
			this.write('}');
			if (closeTable) {
				this.close('table');
			}
			return;
		}
		if (count === 3 && this.current() !== 'template' && this.isOpen('parameter')) {
			this.write('}}}');
			this.close('parameter');
			return;
		}
		if (!closeTable || !this.isOpen('table')) {
			this.write('}}');
			this.close('template');
			return;
		}
		if (!this.isOpen('template')) {
			this.write('}');
			this.close('table');
			return;
		}
		//both an open table and a template, and a string "|}}"
		for (i = this.openTags.length - 1; i >= 0; i--) {
			if (this.openTags[i] === 'table') {
				this.write('}');
				this.close('table');
				return;
			} else if (this.openTags[i] === 'template') {
				this.write('}}');
				this.close('template');
				return;
			}
		}
	},
	'~{3,5}': makeParserFunctionType('signature'),
	'https?://': parsePlainLink, //TODO? Should we use mw.config.get('wgUrlProtocols') instead?
	'\'\'+': function (apos, next) { //FIXME this is a bit too simple
		var b = this.isOpen('bold'), i = this.isOpen('italic');
		if (apos.length === 4) {
			this.write('\'');
			apos = '\'\'\'';
		} else if (apos.length > 5) {
			this.write(apos.substr(5));
			apos = '\'\'\'\'\'';
		} else if (apos.length === 3 && !b && next.indexOf('\'\'\'') === -1 && (i || next.indexOf('\'\'') !== -1)) { //just a guess
			this.write('\'');
			apos = '\'\'';
		}
		if (apos.length === 2) {
			if (i) {
				this.write('\'\'');
				this.close('italic');
			} else {
				this.open('italic');
				this.write('\'\'');
			}
		} else if (apos.length === 3) {
			if (b) {
				this.write('\'\'\'');
				this.close('bold');
			} else {
				this.open('bold');
				this.write('\'\'\'');
			}
		} else { //if (apos.length === 5)
			if (b && i) { //both bold and italic, figure out which we have to close first
				for (i = this.openTags.length - 1; i >= 0; i--) {
					if (this.openTags[i] === 'bold') {
						this.write('\'\'\'');
						this.close('bold');
						this.write('\'\'');
						this.close('italic');
						return;
					} else if (this.openTags[i] === 'italic') {
						this.write('\'\'');
						this.close('italic');
						this.write('\'\'\'');
						this.close('bold');
						return;
					}
				}
			} else if (!b && !i) { //figure out which we should open first
				if (next.indexOf('\'\'') === next.indexOf('\'\'\'')) {
					this.open('italic');
					this.write('\'\'');
					this.open('bold');
					this.write('\'\'\'');
				} else {
					this.open('bold');
					this.write('\'\'\'');
					this.open('italic');
					this.write('\'\'');
				}
			} else if (b /* && !i */) {
				this.write('\'\'\'');
				this.close('bold');
				this.open('italic');
				this.write('\'\'');
			} else /* if (!b && i) */ {
				this.write('\'\'');
				this.close('italic');
				this.open('bold');
				this.write('\'\'\'');
			}
		}
	},
	'&(?:#[xX]?\\d+|\\w+);': makeParserFunctionType('entity'),
	'__[A-Z_]+__': makeParserFunctionType('magic'),
	'\u2013': makeParserFunctionType('char-endash'),
	'\u2014': makeParserFunctionType('char-emdash'),
	'\u2212': makeParserFunctionType('char-minus')
},
eol: function (open) {
	var i;
	for (i = 0; i < open.length; i++) {
		if ($.inArray(open[i], ['externalLink', 'bold', 'italic', 'heading', 'pre']) !== -1) {
			open.length = i;
			break;
		}
	}
	return open;
}
};
cssSyntax = {
noparse: {
	'comment': /\*\//g
},
parse: {
	'/\\*': makeParserFunctionOpen('comment'),
	'!important': function () {
		if (!this.isOpen('decleration')) {
			this.write('!important');
			return;
		}
		this.open('important');
		this.write('!important');
		this.close('important');
	},
	'#[^ ,.*#:>{\\[~]*': function (id) {
		if (this.isOpen('decleration')) {
			this.write('#');
			return;
		}
		this.open('id');
		this.write(id);
		this.close('id');
	},
	'\\.[^ ,.*#:>{\\[~]*': function (cls) {
		if (this.isOpen('decleration')) {
			this.write('.');
			return;
		}
		this.open('class');
		this.write(cls);
		this.close('class');
	},
	':[^ ,.*#:>{\\[~]*': function (pseudo, text) {
		var i = 0;
		if (this.isOpen('decleration')) {
			if (pseudo === ':') {
				while (text.charAt(i++) === ' ') {
					pseudo += ' ';
				}
			} else {
				pseudo = ':';
			}
			this.write(pseudo);
			this.open('value');
			return;
		}
		this.open('pseudo');
		this.write(pseudo);
		this.close('pseudo');
	},
	'@\\S*': function (at) {
		if (this.isOpen('decleration')) {
			this.write('@');
			return;
		}
		this.open('at');
		this.write(at);
		this.close('at');
	},
	'\\[': function () {
		if (this.isOpen('decleration')) {
			this.write('[');
			return;
		}
		this.open('attr');
		this.write('[');
	},
	'\\]': function () {
		this.write(']');
		this.close('attr');
	},
	'\\{': function () {
		this.open('decleration');
		this.write('{');
	},
	'\\}': function () {
		this.close('value');
		this.write('}');
		this.close('decleration');
	},
	';': function () {
		this.close('value');
		this.write(';');
	},
	'url\\([^)]*\\)?': makeParserFunctionType('string'),
	'"(?:[^\\\\"]+|\\\\.)*"?': makeParserFunctionType('string'), // /"(?:[^\\"]+|\\.)*"?/
	'\'(?:[^\\\\\']+|\\\\.)*\'?': makeParserFunctionType('string') // /'(?:[^\\']+|\\.)*'?/
}
};
jsSyntax = {
noparse: {
	'comment': /\*\//g
},
parse: $.extend({
	'"(?:[^\\\\"]+|\\\\.)*"?': makeParserFunctionType('string'),
	'\'(?:[^\\\\\']+|\\\\.)*\'?': makeParserFunctionType('string'),
	'//': function (c, text) {
		this.open('comment');
		this.write(c + text);
		this.close('comment');
	},
	'/\\*': makeParserFunctionOpen('comment'),
	'/(?:[^\\\\/]+|\\\\.)*/?': function (re, after, before) {
		if (/[)\]\w]\s*$/.test(before)) { //probably not a regexp
			this.write('/');
		} else {
			this.open('regexp');
			this.write(re);
			this.close('regexp');
		}
	},
	'\\[': makeParserFunctionOpen('array'),
	'\\]': makeParserFunctionClose('array')
},
	//reserved words
	keywords(['break', 'case', 'catch', 'continue', 'debugger', 'default', 'delete', /*'do',*/ 'else', 'finally', 'for', 'function',
		'if', /*'in',*/ 'instanceof', 'new', 'return', 'switch', 'this', 'throw', 'try', 'typeof', 'var', 'void', 'while'], 'reserved'),
	//literals, globals
	keywords(['Array', 'Boolean', 'Date', 'Error', 'eval', 'false', 'Function', 'Infinity', 'isFinite', 'isNaN', 'JSON', 'Math',
		'NaN', 'null', 'Number', 'Object', 'parseInt', 'parseFloat', 'RegExp', 'String', 'true', 'undefined'], 'global'),
	//common functions etc. (incomplete)
	keywords(['abs', 'addEventListener', 'appendChild', 'apply', 'call', 'ceil', 'charAt', 'charCodeAt', 'clearInterval',
		'clearTimeout', 'concat', 'console', 'createElement', 'decodeURIComponent', 'decodeURI', 'document',
		'encodeURIComponent', 'encodeURI', 'exec', 'floor', 'fromCharCode', 'getElementById', 'getElementsByTagName',
		'indexOf', 'insertBefore', 'join', 'lastIndexOf', 'length', 'match', 'parentNode', 'pop', 'push', 'random',
		'removeEventListener', 'replace', 'reverse', 'round', 'search', 'setInterval', 'setTimeout', 'shift', 'slice',
		'sort', 'splice', 'split', 'substr', 'substring', 'test', 'toLowerCase', 'toString', 'toUpperCase', 'unshift',
		'valueOf', 'window'], 'common'),
	//reserved for future / deprecated
	keywords(['class', 'const', 'enum', 'export', 'extends', 'implements', 'import', 'interface', 'let', 'package', 'private',
		'protected', 'public', 'static', 'super', 'with', 'yield'], 'future'),
	//short words
	keywords(['do', 'in'], 'reserved')
)
};
jsonSyntax = {
parse: {
	'"(?:[^\\\\"]+|\\\\.)*"?': makeParserFunctionType('string'),
	'\\btrue\\b': makeParserFunctionType('global'),
	'\\bfalse\\b': makeParserFunctionType('global'),
	'\\bnull\\b': makeParserFunctionType('global')
}
};
luaSyntax = {
parse: $.extend({
	'--\\[=*\\[': function (c) {
		this.open('comment');
		this.write(c);
		this.updateNoparse('comment', new RegExp(c.replace(/--/, '').replace(/\[/g, '\\]'), 'g'));
	},
	'--': function (c, text) {
		this.open('comment');
		this.write(c + text);
		this.close('comment');
	},
	'"(?:[^\\\\"]+|\\\\.)*"?': makeParserFunctionType('string'),
	'\'(?:[^\\\\\']+|\\\\.)*\'?': makeParserFunctionType('string'),
	'\\[=*\\[': function (s) {
			this.open('string');
			this.write(s);
			this.updateNoparse('string', new RegExp(s.replace(/\[/g, '\\]'), 'g'));
	},
	'\\[': makeParserFunctionOpen('array'), //index
	'\\]': makeParserFunctionClose('array'),
	'\\{': makeParserFunctionOpen('array'), //table
	'\\}': makeParserFunctionClose('array')
},
	keywords(['and', 'break', 'do', 'elseif', 'else', 'end', 'false', 'for', 'function', 'if', 'in', 'local', 'nil', 'not',
		'or', 'repeat', 'return', 'then', 'true', 'until', 'while'], 'reserved'),
	keywords(['ipairs', 'next', 'pairs', 'select', 'tonumber', 'tostring', 'type', 'unpack', '_VERSION', 'coroutine',
		'module', 'require', 'string', 'table', 'math'], 'common')
)
};

function DebugParser () {
}

DebugParser.prototype = {
	parse: function (text) {
		return [[text, '']];
	},
	parseWithParen: function (text) {
		return [[text, '']];
	}
};

function BasicHighlighter (syntax, colors, box, prefix) {
	this.parser = syntax ? new Parser(syntax) : new DebugParser();
	this.colors = syntax ? colors : {};
	this.box = box;
	this.prefix = prefix;
	this.stylesheet = document.getElementsByTagName('head')[0].appendChild(document.createElement('style'));
	this.spanCount = 0;
}

BasicHighlighter.prototype = {
	enable: function () {
		this.stylesheet.disabled = false;
	},
	disable: function () {
		this.stylesheet.disabled = true;
	},
	destroy: function () {
		this.stylesheet.parentNode.removeChild(this.stylesheet);
	},

	makeSpans: function (n) {
		for (; this.spanCount < n; this.spanCount++) {
			this.box.appendChild(document.createElement('span')).id = this.prefix + this.spanCount;
		}
	},

	getColor: function (type) {
		if (type in this.colors) {
			return this.colors[type];
		}
		if (type.charAt(0) === '<') {
			return this.colors.tag;
		}
	},

	getCSS: function (syntax) {
		var lastColor = false, css = [], spans = -1, color, text, i;
		for (i = 0; i < syntax.length; i++) {
			color = this.getColor(syntax[i][1]);
			text = syntax[i][0].replace(/(\\|")/g, '\\$1').replace(/\n/g, '\\A ');
			if (color !== lastColor) {
				if (lastColor !== false) {
					css.push('"}');
				}
				spans++;
				lastColor = color;
				if (color) {
					color = 'background-color:' + color + ';';
				} else {
					color = '';
				}
				css.push('#' + this.prefix + Math.floor(spans / 2) + (spans % 2 === 0 ? ':before' : ':after') + '{' + color + 'content:"');
			}
			css.push(text);
		}
		css.push('"}');
		this.makeSpans(Math.floor(spans / 2) + 1);
		return css.join('');
	},

	highlight: function (text, pos) {
		var css;
		if (pos === undefined) {
			if (text === this.lastText) {
				return;
			}
			this.lastText = text;
			css = this.getCSS(this.parser.parse(text));
		} else {
			this.lastText = false;
			css = this.getCSS(this.parser.parseWithParen(text, pos));
		}
		if (css === this.lastCSS) {
			return;
		}
		this.lastCSS = css;
		this.stylesheet.textContent = css;
	}
};

function getStyles (el, styles) {
	var computedStyle = window.getComputedStyle(el, null), ret = {}, i;
	for (i = 0; i < styles.length; i++) {
		ret[styles[i]] = computedStyle[styles[i]];
	}
	return ret;
}
function setStyles (el, styles) {
	var s;
	for (s in styles) {
		el.style[s] = styles[s];
	}
}
function addPx (length, d) {
	var l = Number(length.replace(/px$/, '')) + d;
	if (isNaN(l)) {
		return length;
	}
	return String(l) + 'px';
}

function Highlighter (syntax, colors, textarea, paren) {
	var _this = this;
	if (!syntax) {
		this.debug = true;
	}
	this.textarea = textarea;
	this.initBoxes();
	this.basicHighlighter = new BasicHighlighter(syntax, colors, this.highlightbox, textarea.id + '-');
	this.onoff = $.noop;
	this.reportTime = $.noop;
	this.getPos = $.noop;
	this.paren = paren;
	if (paren) {
		this.$textarea = $(this.textarea);
		mw.loader.using('jquery.textSelection', function () {
			_this.getPos = function () {
				return _this.$textarea.textSelection('getCaretPosition');
			};
		});
	}
	this.enable();
}

Highlighter.prototype = {
	isEnabled: function () {
		return this.enabled;
	},
	enable: function () {
		if (this.isEnabled()) {
			return;
		}
		this.bindHandlers();
		this.basicHighlighter.enable();
		this.enabled = true;
		this.highlight();
		this.syncScroll();
		this.onoff(true);
	},
	disable: function () {
		if (!this.isEnabled()) {
			return;
		}
		this.unbindHandlers();
		this.basicHighlighter.disable();
		this.enabled = false;
		this.onoff(false);
	},
	destroy: function () {
		var scrolltop, focus;
		focus = (this.textarea === this.textarea.ownerDocument.activeElement);
		scrolltop = this.textarea.scrollTop;
		this.disable();
		this.basicHighlighter.destroy();
		this.container.parentNode.insertBefore(this.textarea, this.container);
		this.container.parentNode.removeChild(this.container);
		setStyles(this.textarea, this.oldStyle);
		this.textarea.scrollTop = scrolltop;
		if (focus) {
			this.textarea.focus();
		}
	},

	initBoxes: function () {
		var scrolltop, focus, style, commonStyle, bugfixStyle = {}, profile = $.client.profile();
		this.container = document.createElement('div');
		this.oldStyle = getStyles(this.textarea, ['backgroundColor', 'display', 'height', 'left',
			'marginTop', 'marginRight', 'marginBottom', 'marginLeft', 'overflowX', 'overflowY', 'position',
			'resize', 'top', 'whiteSpace', 'width', 'MozBoxSizing', 'WebkitBoxSizing', 'boxSizing']);

		focus = (this.textarea === this.textarea.ownerDocument.activeElement);
		scrolltop = this.textarea.scrollTop;
		this.highlightbox = document.createElement('div');

		//make sure "highlightbox" is behind the transparent "textarea" and looks *exactly* the same
		commonStyle = {
			display: 'block',
			height: '100%',
			margin: '0px',
			overflowX: 'auto',
			overflowY: 'scroll',
			resize: 'none',
			whiteSpace: 'pre-wrap',
			width: '100%',
			MozBoxSizing: 'border-box',
			WebkitBoxSizing: 'border-box',
			boxSizing: 'border-box'
		};
		setStyles(this.textarea, $.extend({
			backgroundColor: 'transparent',
			position: 'absolute',
			left: 0,
			top: 0
		}, commonStyle));

		//workaround for subpixel text positioning in Google Chrome/Opera, cf. https://code.google.com/p/chromium/issues/detail?id=395425
		setStyles(this.textarea, {
			fontSize: getStyles(this.textarea, ['fontSize']).fontSize
		});

		style = getStyles(this.textarea, ['MozAppearance', 'WebkitAppearance', 'borderTopWidth', 'borderRightWidth',
			'borderBottomWidth', 'borderLeftWidth', 'direction', 'fontFamily', 'fontSize', 'fontStyle',
			'fontVariant', 'fontWeight', 'letterSpacing', 'lineHeight', 'paddingTop', 'paddingRight',
			'paddingBottom', 'paddingLeft', 'MozTabSize', 'tabSize', 'textAlign', 'textIndent', 'textTransform',
			'unicodeBidi', 'verticalAlign', 'wordSpacing', 'wordWrap']);

		//workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=157846
		//see style for textarea > .anonymous-div
		//in jar:file://*/omni.ja!/chrome/toolkit/res/forms.css resp. https://hg.mozilla.org/mozilla-central/file/*/layout/style/forms.css
		if (profile.layout === 'gecko' && profile.versionNumber < 29) {
			bugfixStyle.paddingLeft = addPx(style.paddingLeft, 1);
			bugfixStyle.paddingRight = addPx(style.paddingRight, 1);
		}

		setStyles(this.highlightbox, $.extend({
			backgroundColor: this.oldStyle.backgroundColor,
			borderColor: 'transparent',
			borderStyle: 'solid',
			color: (this.debug ? 'black' : 'transparent')
		}, style, commonStyle, bugfixStyle));

		this.highlightbox.lang = this.textarea.lang;

		if (this.debug) {
			this.oldStyle.color = getStyles(this.textarea, ['color']).color;
			setStyles(this.textarea, {
				color: this.oldStyle.backgroundColor
			});
			setStyles(this.highlightbox, getStyles(this.textarea, ['text-rendering']));
		}

		setStyles(this.container, {
			height: this.oldStyle.height,
			marginTop: this.oldStyle.marginTop,
			marginRight: this.oldStyle.marginRight,
			marginBottom: this.oldStyle.marginBottom,
			marginLeft: this.oldStyle.marginLeft,
			position: 'relative'
		});
		this.textarea.parentNode.insertBefore(this.container, this.textarea);
		this.container.appendChild(this.highlightbox);
		this.container.appendChild(this.textarea);
		this.textarea.scrollTop = scrolltop;
		if (focus) {
			this.textarea.focus();
		}
	},

	proxy: function (f) {
		var _this = this;
		if (!this.proxyCache) {
			this.proxyCache = {};
		}
		if (!(f in this.proxyCache)) {
			this.proxyCache[f] = function () {
				_this[f].apply(_this);
			};
		}
		return this.proxyCache[f];
	},

	bindHandlers: function () {
		this.textarea.addEventListener('input', this.proxy('onInput'), false);
		if (this.paren) {
			this.textarea.addEventListener('keyup', this.proxy('onInput'), false);
		}
		this.textarea.addEventListener('scroll', this.proxy('syncScroll'), false);
		this.intervalID1 = window.setInterval(this.proxy('highlight'), 500);
		this.intervalID2 = window.setInterval(this.proxy('syncScroll'), 500);
	},
	unbindHandlers: function () {
		this.textarea.removeEventListener('input', this.proxy('onInput'), false);
		if (this.paren) {
			this.textarea.removeEventListener('keyup', this.proxy('onInput'), false);
		}
		this.textarea.removeEventListener('scroll', this.proxy('syncScroll'), false);
		window.clearInterval(this.intervalID1);
		window.clearInterval(this.intervalID2);
	},

	syncScroll: function () {
		if (this.highlightbox.scrollLeft !== this.textarea.scrollLeft) {
			this.highlightbox.scrollLeft = this.textarea.scrollLeft;
		}
		if (this.highlightbox.scrollTop !== this.textarea.scrollTop) {
			this.highlightbox.scrollTop = this.textarea.scrollTop;
		}
	},
	onInput: function () {
		window.setTimeout(this.proxy('highlight'), 1);
	},
	highlight: function () {
		var time = $.now();
		this.basicHighlighter.highlight(this.textarea.value, this.getPos());
		this.reportTime($.now() - time);
	}
};

function makeHighlighter (syntax, colors, id, label) {
	var textarea = document.getElementById(id), highlighter, $label, $checkbox;
	if (!textarea) {
		return;
	}
	highlighter = new Highlighter(syntax, colors, textarea, true);

	if (label) {
		$label = $('<label>', {
			'for': 'syntaxhighlight'
		}).text(label);
		$checkbox = $('<input id="syntaxhighlight" type="checkbox" checked="checked" />');
		highlighter.onoff = function (on) {
			$checkbox.prop('checked', on);
		};
		$checkbox.change(function () {
			if ($checkbox.prop('checked')) {
				highlighter.enable();
			} else {
				highlighter.disable();
			}
		});
		$('.editCheckboxes').append('\n').append($checkbox).append('\n').append($label);
	}
	if (mw.util.getParamValue('logTime')) {
		highlighter.reportTime = function (ms) {
			if (ms) {
				window.console.log(ms + ' ms (#' + id + ')');
			}
		};
	}
	return highlighter;
}

defaultWikitextColors = {
	bold: '#E5E5E5', //'gray'
	'char-emdash': '#FFE6FF', //'pink'
	'char-endash': '#E5E5E5', //'gray'
	'char-minus': '#FFFFCC', //'yellow'
	comment: '#E6FFE6', //'green'
	entity: '#E6FFE6', //'green'
	externalLink: '#E6FFFF', //'cyan'
	italic: '#E5E5E5', //'gray'
	heading: '#E5E5E5', //'gray'
	hr: '#E5E5E5', //'gray'
	listAndIndent: '#E6FFE6', //'green'
	magic: '#E5E5E5', //'gray'
	'matching-paren': '#FFCCCC',
	parameter: '#FFCC66', //'orange'
	pre: '#E5E5E5', //'gray'
	signature: '#FFCC66', //'orange'
	tag: '#FFE6FF', //'pink'
	table: '#FFFFCC', //'yellow'
	'table-syntax': '#FFCC66', //'orange'
	template: '#FFFFCC', //'yellow'
	wikilink: '#E6E6FF' //'blue'
};
defaultCssColors = {
	at: '#FFE6FF',
	attr: '#FFE6FF',
	'class': '#FFE6FF',
	comment: '#E6FFE6',
	decleration: '#FFFFCC',
	id: '#E6E6FF',
	important: '#E6E6FF',
	'matching-paren': '#FFCCCC',
	pseudo: '#FFE6FF',
	string: '#FFCC66',
	value: '#E5E5E5'
};
defaultJsColors = {
	array: '#FFFFCC',
	comment: '#E6FFE6',
	common: '#E6FFFF',
	future: '#FFE6FF',
	global: '#E6FFE6',
	'matching-paren': '#FFCCCC',
	regexp: '#FFFFCC',
	reserved: '#E6E6FF',
	string: '#FFCC66'
};
strongColors = { //I use a misconfigured screen, I cannot see the above colors on it at all
	at: 'pink',
	array: 'yellow',
	attr: 'pink',
	bold: 'gray',
	'char-emdash': 'pink',
	'char-endash': 'gray',
	'char-minus': 'yellow',
	'class': 'pink',
	comment: 'green',
	common: 'cyan',
	decleration: 'yellow',
	entity: 'green',
	externalLink: 'cyan',
	future: 'pink',
	global: 'green',
	id: '#55f',
	important: '#55f',
	italic: 'gray',
	heading: 'gray',
	hr: 'gray',
	listAndIndent: 'green',
	magic: 'gray',
	'matching-paren': 'red',
	parameter: 'orange',
	pre: 'gray',
	pseudo: 'pink',
	regexp: 'yellow',
	reserved: '#55f',
	signature: 'orange',
	string: 'orange',
	tag: 'pink',
	table: 'yellow',
	'table-syntax': 'orange',
	template: 'yellow',
	value: 'gray',
	wikilink: '#55f'
};

function initHighlighter(ext, additional) {
	var word, id, debug, colors, syntax, highlighter;
	id = mw.config.get('wgCanonicalSpecialPageName') === 'Upload' ? 'wpUploadDescription' : 'wpTextbox1';
	debug = !!mw.util.getParamValue('debugSyntaxhighlight');
	if (!debug) {
		colors = getColors(ext);
		syntax = getSyntax(ext);
	}

	if (!debug && additional) {
		for (word in additional) {
			colors['additional-' + word] = additional[word];
			additional[word] = makeParserFunctionType('additional-' + word);
		}
		syntax = $.extend({}, additional, syntax);
	}

	highlighter = makeHighlighter(syntax, colors, id, id === 'wpTextbox1' ? mw.msg('schnark-syntaxhighlight-enable') : false);
	if (!debug && id === 'wpTextbox1') {
		makeHighlighter(syntax, colors, 'wpTextbox2'); //for edit conflicts
	}

	return function () {
		highlighter.destroy();
		$('#syntaxhighlight, label[for="syntaxhighlight"]').remove();
	};
}

function getColors (ext) {
	if (mw.config.get('wgServer') === 'http://localhost') {
		return strongColors;
	}
	if (ext === '') {
		//TODO use window.syntaxHighlighterConfig to configure colors, remove Color there
		return defaultWikitextColors;
	}
	if (ext === 'css') {
		return defaultCssColors;
	}
	if (ext === 'js' || ext === 'json' || ext === 'lua') { //TODO separate these
		return defaultJsColors;
	}
}

function getSyntax (ext) {
	if (ext === '') {
		return wikiSyntax;
	}
	if (ext === 'css') {
		return cssSyntax;
	}
	if (ext === 'js') {
		return jsSyntax;
	}
	if (ext === 'json') {
		return jsonSyntax;
	}
	if (ext === 'lua') {
		return luaSyntax;
	}
}

function allowTabs () {
	$(function () {
		var $textarea = $('#wpTextbox1'), scrolltop;
		$textarea.keypress(function (e) {
			if (e.keyCode === 9 && !(e.ctrlKey || e.altKey)) {
				e.preventDefault();
				var	text = $textarea.textSelection('getSelection'),
					sel = $textarea.textSelection('getCaretPosition', {startAndEnd: true}),
					lines = text.split('\n'), i, len = 0;
				if (text === '') {
					if (e.shiftKey) {
						text = $textarea.textSelection('getContents');
						if (text.charAt(sel[0] - 1) !== '\t') {
							return;
						}
						text = text.substring(0, sel[0] - 1) + text.substring(sel[0]);
						scrolltop = $textarea[0].scrollTop;
						$textarea.textSelection('setContents', text);
						$textarea.textSelection('setSelection', {start: sel[0] - 1, end: sel[0] - 1});
						$textarea[0].scrollTop = scrolltop;
					} else {
						$textarea.textSelection('encapsulateSelection', {pre: '\t'});
					}
					return;
				}
				for (i = 0; i < lines.length; i++) {
					if (e.shiftKey) {
						if (lines[i].charAt(0) === '\t') {
							lines[i] = lines[i].substr(1);
							len--;
						}
					} else {
						if (lines[i] !== '') {
							lines[i] = '\t' + lines[i];
							len++;
						}
					}
				}
				if (len !== 0) {
					$textarea.textSelection('encapsulateSelection', {peri: lines.join('\n'), replace: true});
					$textarea.textSelection('setSelection', {start: sel[0], end: sel[1] + len});
				}
			}
		});
	});
}

//only call this when CodeEditor is disabled!
function createHighlighter (deps, ext) {
	mw.loader.using(deps, function () {
		$(function () {
			window.setTimeout(function () { //make sure we initialize *after* WikiEditor
				removeHighlighter = initHighlighter(ext, mw.user.options.get('schnark-syntaxhighlight-additional', false));
			}, 0);
		});
	});
}
//call this before you enable CodeEditor!
function removeHighlighter () {
}

function killCodeEditor () {
	mw.config.set('wgCodeEditorCurrentLanguage', false);
	$(function () {
		try { //FIXME WTF?
			var context = $('#wpTextbox1').data('wikiEditorContext');
			$('img.tool[rel="codeEditor"]').remove();
			context.fn.disableCodeEditor();
			if (mw.user.options.get('usecodeeditor') !== '0') {
				context.fn.setCodeEditorPreference(false);
			}
			/* var $textarea = $('#wpTextbox1');
			$textarea.wikiEditor('disableCodeEditor');
			$textarea.wikiEditor('removeFromToolbar', {section: 'main', group: 'format', tool: 'codeEditor'}); */
		} catch (e) {
		}
	});
}

function handleCodeEditor (deps, ext, ceEnabled) {
	var $button = $('img.tool[rel=codeEditor]'), $clone = $button.clone(true).removeClass('tool'); //FIXME very hackish
	$button.off('click').click(function () {
		if (ceEnabled) {
			$clone.click();
			ceEnabled = false;
			createHighlighter(deps, ext);
		} else {
			removeHighlighter();
			$clone.click();
			ceEnabled = true;
		}
	});
}

function initL10N (l10n) {
	var i, chain = mw.language.getFallbackLanguageChain();
	for (i = chain.length - 1; i >= 0; i--) {
		if (chain[i] in l10n) {
			mw.messages.set(l10n[chain[i]]);
		}
	}
}

function init () {
	initL10N({
		en: {
			'schnark-syntaxhighlight-enable': 'enable syntax highlighter'
		},
		de: {
			'schnark-syntaxhighlight-enable': 'Syntaxhervorhebung aktivieren'
		}
	});

	if (!$.client.test(map) && !mw.util.getParamValue('ignoreBlacklist')) {
		return;
	}

	var modelToExt = {
		javascript: 'js',
		css: 'css',
		Scribunto: 'lua',
		wikitext: ''
	}, ext, deps = [], hasCodeEditor = 0; //0 - no, 1 - yes, disabled, 2 - yes, enabled
	if (mw.config.get('wgCanonicalSpecialPageName') !== 'Upload' && mw.user.options.get('usebetatoolbar')) {
		deps.push('ext.wikiEditor.toolbar');
		mw.util.addCSS('.tool-select .options {z-index: 5;}');
	}

	ext = mw.config.get('wgPageContentModel') in modelToExt ? modelToExt[mw.config.get('wgPageContentModel')] : 'json';
	if (ext in mw.user.options.get('schnark-syntaxhighlight-exclude', {})) {
		return;
	}

	if (ext) {
		mw.loader.using('jquery.textSelection', allowTabs);
	}

	if (ext && mw.user.options.get('usebetatoolbar')) {
		if (mw.user.options.exists('usecodeeditor')) {
			if (mw.user.options.get('usecodeeditor') !== '0') {
				hasCodeEditor = 2;
			} else {
				hasCodeEditor = 1;
			}
		} else {
			hasCodeEditor = mw.loader.getState('ext.codeEditor') ? 2 : 0;
		}
	}
	if (hasCodeEditor && mw.user.options.get('userjs-schnark-syntaxhighlight-no-code-editor')) {
		killCodeEditor();
		hasCodeEditor = 0;
	}

	if (hasCodeEditor) {
		mw.loader.using('ext.codeEditor', function () {
			$(function () {
				handleCodeEditor(deps, ext, hasCodeEditor === 2);
			});
		});
	}
	if (hasCodeEditor !== 2) {
		createHighlighter(deps, ext);
	}
}

if ($.inArray(mw.config.get('wgAction'), ['edit', 'submit']) !== -1 || mw.config.get('wgCanonicalSpecialPageName') === 'Upload') {
	mw.loader.using(['jquery.client', 'mediawiki.util', 'mediawiki.language', 'user.options'], init);
}

if (libs.qunit) {
	libs.qunit.Parser = Parser;
	libs.qunit.wikiSyntax = wikiSyntax;
	libs.qunit.jsSyntax = jsSyntax;
	//libs.qunit.jsonSyntax = jsonSyntax;
	libs.qunit.cssSyntax = cssSyntax;
	libs.qunit.luaSyntax = luaSyntax;
}

})(jQuery, mediaWiki, mediaWiki.libs);