Zum Inhalt springen

Benutzer:Schnark/js/syntaxhighlight.js

aus Wikipedia, der freien Enzyklopädie
Dies ist eine alte Version dieser Seite, zuletzt bearbeitet am 5. Juli 2014 um 10:36 Uhr durch Schnark (Diskussion | Beiträge) (strict, onevar, Links korrekt hervorheben). 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, 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'),
	'^ ': function () {
		if (!this.isOpen('template')) {
			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);
		}
	},
	'\\{\\{\\{': makeParserFunctionOpen('parameter'),
	'\\{\\{': makeParserFunctionOpen('template'),
	'\\{\\|': makeParserFunctionOpen('table'),
	'\\}': function (x, text, before) {
		var closeTable = (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--; i >= 0) {
			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,
	"'''''": function () { //FIXME both '''''bi''b''' and '''''bi'''i'' are ok
		var b = this.isOpen('bold'), i = this.isOpen('italic');
		if (b && i) {
			this.write("'''''");
			this.close('bold');
			this.close('italic');
		} else if (!b && !i) {
			this.open('bold');
			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("'''");
		}
	},
	"'''": function () {
		if (this.isOpen('bold')) {
			this.write("'''");
			this.close('bold');
		} else {
			this.open('bold');
			this.write("'''");
		}
	},
	"''": function () {
		if (this.isOpen('italic')) {
			this.write("''");
			this.close('italic');
		} else {
			this.open('italic');
			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(['addEventListener', 'appendChild', 'charAt', 'charCodeAt', 'clearInterval', 'clearTimeout', 'console', 'createElement',
		'decodeURIComponent', 'decodeURI', 'document', 'encodeURIComponent', 'encodeURI', 'getElementById', 'getElementsByTagName',
		'indexOf', 'insertBefore', 'join', 'length', 'parentNode', 'removeEventListener', 'setInterval', 'setTimeout', 'splice',
		'substr', 'substring', 'test', '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')
)
};
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 BasicHighlighter (syntax, colors, box, prefix) {
	this.parser = new Parser(syntax);
	this.colors = 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 Highlighter (syntax, colors, textarea, paren) {
	var _this = this;
	this.textarea = textarea;
	this.initBoxes();
	this.basicHighlighter = new BasicHighlighter(syntax, colors, this.highlightbox, textarea.id + '-');
	this.onoff = $.noop;
	this.reportTime = $.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);
	},

	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];
	},

	initBoxes: function () {
		var scrolltop, focus, style, commonStyle, bugfixStyle = {}, profile = $.client.profile();
		this.container = document.createElement('div');
		this.oldStyle = getStyles(this.textarea, ['backgroundColor', /*'borderTopWidth', 'borderTopStyle', 'borderTopColor',
			'borderRightWidth', 'borderRightStyle', 'borderRightColor', 'borderBottomWidth', 'borderBottomStyle',
			'borderBottomColor', 'borderLeftWidth', 'borderLeftStyle', 'borderLeftColor',*/ 'marginTop', 'marginRight',
			'marginBottom', 'marginLeft', 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft', 'overflowX', 'overflowY',
			'position', 'resize', 'left', 'top', 'width']);

		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 = {
			//border: '1px inset gray',
			margin: 0,
			overflowX: 'auto',
			overflowY: 'scroll',
			padding: 0,
			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 https://bugzilla.mozilla.org/show_bug.cgi?id=157846, see omni.ja!/chrome/toolkit/res/forms.css: textarea > .anonymous-div
		if (profile.layout === 'gecko' && profile.layoutVersion <= 20140101) { //FIXME since which version is this fixed (is it really fixed?)
			if (profile.platform === 'win') {
				bugfixStyle.padding = '0px 1px';
			} else {
				bugfixStyle.padding = '0px 2px';
			}
		}

		style = getStyles(this.textarea, ['direction', 'fontFamily', 'fontSize', 'lineHeight', 'height', 'wordWrap',
			'verticalAlign', 'unicodeBidi', 'MozAppearance', 'WebkitAppearance']);
		setStyles(this.highlightbox, $.extend({
			backgroundColor: this.oldStyle.backgroundColor,
			color: 'transparent'
		}, style, commonStyle, bugfixStyle));

		setStyles(this.container, {
			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();
		}
	},
	destroy: function () {
		this.disable();
		this.basicHighlighter.destroy();
		this.container.parentNode.insertBefore(this.textarea, this.container);
		this.container.parentNode.removeChild(this.container);
		setStyles(this.textarea, this.oldStyle);
	},

	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);
	},

	getPos: $.noop,

	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();
			}
		});
		/*$checkbox.on('dblclick', function () {
			highlighter.destroy();
		});*/
		$('.editCheckboxes').append('\n').append($checkbox).append('\n').append($label);
	}
	if (mw.config.get('wgUserName') === 'Schnark') {
		highlighter.reportTime = function (ms) {
			if (ms) {
				window.console.log(ms + ' ms (#' + id + ')');
			}
		};
	}
}

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'
	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',
	template: 'yellow',
	value: 'gray',
	wikilink: '#55f'
};

function initHighlighter(ext, label, additional) {
	var word, id, colors, syntax;
	id = mw.config.get('wgCanonicalSpecialPageName') === 'Upload' ? 'wpUploadDescription' : 'wpTextbox1';
	colors = getColors(ext);
	syntax = getSyntax(ext);

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

	makeHighlighter(syntax, colors, id, id === 'wpTextbox1' ? (label || 'enable syntax highlighter') : false);
	if (id === 'wpTextbox1') {
		makeHighlighter(syntax, colors, 'wpTextbox2'); //for edit conflicts
	}
}

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 === '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 === 'lua') {
		return luaSyntax;
	}
}

function killCodeEditor () {
	mw.config.set('wgCodeEditorCurrentLanguage', false);
	$(function () {
		try { //FIXME WTF?
			var context = $('#wpTextbox1').data('wikiEditorContext');
			$('#wikiEditor-ui-toolbar .group-format [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 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.val(text); //$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});
				}
			}
		});
	});
}

function init () {
	if (!$.client.test(map) && !mw.util.getParamValue('ignoreBlacklist')) {
		return;
	}
	var modelToExt = {
		javascript: 'js',
		css: 'css',
		Scribunto: 'lua'
	}, ext, label, deps = [];
	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;}');
	}

	switch (mw.config.get('wgUserLanguage')) {
	case 'de':
	case 'de-at':
	case 'de-ch':
	case 'de-formal':
		label = 'Syntaxhervorhebung aktivieren';
	}

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

	if (ext) { //FIXME
		killCodeEditor();
	}
	if (ext !== '') {
		mw.loader.using('jquery.textSelection', allowTabs);
	}

	mw.loader.using(deps, function () {
		$(function () {
			window.setTimeout(function () { //make sure we initialize *after* WikiEditor
				initHighlighter(ext, label, mw.user.options.get('schnark-syntaxhighlight-additional', false));
			}, 0);
		});
	});
}

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

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

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