Benutzer:Schnark/js/syntaxhighlight.js
< Benutzer:Schnark | js
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
//based on [[mw:User:Remember the dot/Syntax highlighter.js]] with a completely different parser
//Dokumentation unter [[Benutzer:Schnark/js/syntaxhighlight]]
//<nowiki>
/*global mediaWiki, OO*/
(function ($, mw, libs) {
"use strict";
var map, protocols,
wikiSyntax, cssSyntax, jsSyntax, jsonSyntax, luaSyntax,
defaultWikitextColors, defaultCssColors, defaultJsColors, strongColors,
hasOwn = Object.prototype.hasOwnProperty,
requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame || function (f) {
f();
};
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) {
if (hasOwn.call(syntax.parse, re)) {
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;
}
},
closeUntil: function (type) {
if (this.openTags.length === 0) {
return;
}
if (this.openTags[this.openTags.length - 1] === type) {
return;
}
var i = this.openTags.lastIndexOf(type);
if (i > -1) {
this.openTags.length = i + 1;
}
},
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.slice(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.slice(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.slice(this.pos + word.length), this.text.slice(0, this.pos)
);
break;
}
}
} else {
this.write(this.text.slice(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.slice(0, parens[j] - oldLen), output[i][1]]);
t = t.slice(parens[j] - oldLen);
}
newOutput.push([t[0], 'matching-paren']);
t = t.slice(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);
};
}
function makeNoparseTags (other, tags) {
var i, tag;
for (i = 0; i < tags.length; i++) {
tag = tags[i];
other['<' + tag + '>'] = new RegExp('</' + tag + '>', 'gi');
}
return other;
}
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 = ',;.:!?', curly = link.indexOf('}');
if (curly > -1 && this.isOpen('template')) {
link = link.slice(0, curly);
}
if (link.indexOf('(') === -1) {
punc += ')';
}
while (link && punc.indexOf(link.charAt(link.length - 1)) !== -1) {
link = link.slice(0, -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: makeNoparseTags(
{'comment': /-->/g},
['categorytree', 'ce', 'charinsert', 'chem', 'graph', 'hiero', 'inputbox', 'mapframe', 'maplink', 'math',
'nowiki', 'pre', 'section', 'score', 'source', 'syntaxhighlight', 'templatedata', 'timeline']
),
parse: {
'^=': makeParserFunctionOpen('heading'),
'^ *:*\\{\\|': makeParserFunctionOpen('table'),
'^ ': function () {
if (!this.isOpen('template') && !this.isOpen('table')) {
this.open('pre');
}
this.write(' ');
},
'^[*#:;]+': makeParserFunctionType('listAndIndent'),
'^-{4,}': makeParserFunctionType('hr'),
'\\[\\[': makeParserFunctionOpen('wikilink'), //TODO [[http://example.org] should be external]
'\\[': function (x, text) {
if (protocols.test(text)) {
this.close('externalLink');
this.open('externalLink');
this.write('[' + text[0]); //write first char to make sure we don't trigger a bare external link
return;
}
this.write('[');
},
'\\]': function (x, text) {
if (this.isOpen('externalLink')) {
this.closeUntil('externalLink');
this.write(']');
this.close('externalLink');
} else if (text[0] === ']') {
this.closeUntil('wikilink');
this.write(']]');
this.close('wikilink');
} else {
this.write(']');
}
},
'<!--': makeParserFunctionOpen('comment'),
'</?[a-zA-Z]+[^>]*>': function (tag) { //TODO <CR> <LF> <quetsch> etc.
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', 'mapframe', 'maplink', 'nowiki', 'ref', 'references', 'section'],
selfclosingTag = ['br', 'hr', 'wbr'];
if (tagParts[1]) {
this.closeUntil('<' + tagname + '>');
this.write(tag);
this.close('<' + tagname + '>');
} else if ((selfclosing && singleTag.indexOf(tagname) > -1) || selfclosingTag.indexOf(tagname) > -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[0] === '#' || (text[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[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.closeUntil('parameter');
this.write('}}}');
this.close('parameter');
return;
}
if (!closeTable || !this.isOpen('table')) {
this.closeUntil('template');
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.closeUntil('template');
this.write('}}');
this.close('template');
return;
}
}
},
'^\\|-+|^\\|\\+|^[|!]|\\|\\||!!': function (s, next) {
if (this.current() === 'table' && !(s === '|' && next[0] === '}')) {
this.open('table-syntax');
this.write(s);
this.close('table-syntax');
} else {
this.write(s);
}
},
'~{3,5}': makeParserFunctionType('signature'),
'https?://': parsePlainLink, //TODO? Should we use mw.config.get('wgUrlProtocols') instead?
'\'\'+': function (apos, next) { //this isn't perfect, but good enough on actual pages
var b = this.isOpen('bold'), i = this.isOpen('italic');
if (apos.length === 4) {
this.write('\'');
apos = '\'\'\'';
} else if (apos.length > 5) {
this.write(apos.slice(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('\'\'\'');
}
}
},
'&(?:#\\d+|#[xX][a-fA-F0-9]+|\\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 (['externalLink', 'bold', 'italic', 'heading', 'pre'].indexOf(open[i]) !== -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'), //RE: /"(?:[^\\"]+|\\.)*"?/
'\'(?:[^\\\\\']+|\\\\.)*\'?': makeParserFunctionType('string') //RE: /'(?:[^\\']+|\\.)*'?/
}
};
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', 'max', 'min', 'parentNode', 'pop', 'push', 'random', 'removeEventListener',
'replace', 'reverse', 'round', 'search', 'setInterval', 'setTimeout', 'shift', 'slice', 'sort',
'splice', 'split', 'sqrt', '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 (hasOwn.call(this.colors, type)) {
return this.colors[type];
}
if (type[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 '); //append space to make sure no longer hex number is created
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('').replace(/\\A "/g, '\\A"'); //in this case the space is wrong
},
hasChanged: function (text, pos) {
if (this.lastText === text && this.lastPos === pos) {
return false;
}
this.lastText = text;
this.lastPos = pos;
return true;
},
highlight: function (text, pos) {
var css;
if (!this.hasChanged(text, pos)) {
return;
}
if (pos === undefined) {
css = this.getCSS(this.parser.parse(text));
} else {
css = this.getCSS(this.parser.parseWithParen(text, pos));
}
if (css === this.lastCSS) {
return;
}
this.lastCSS = css;
requestAnimationFrame(function () {
this.stylesheet.textContent = css;
}.bind(this));
}
};
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) {
if (hasOwn.call(styles, s)) {
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 randomId () {
return 'id' + String(mw.now()).replace(/\D/g, '');
}
function Highlighter (syntax, colors, textarea, paren) {
if (!syntax) {
this.debug = true;
}
this.textarea = textarea;
this.isDiv = (textarea.tagName.toLowerCase() !== 'textarea');
this.initBoxes();
this.basicHighlighter = new BasicHighlighter(syntax, colors, this.highlightbox, (textarea.id || randomId()) + '-');
this.onoff = $.noop;
this.reportTime = $.noop;
this.getPos = $.noop;
this.paren = paren;
if (paren) {
if (this.isDiv) {
this.getPos = function () {
var sel = window.getSelection(), par, i, c, l, pos = 0;
if (!sel.isCollapsed || !sel.anchorNode) {
return false;
}
par = sel.anchorNode.parentNode;
if (!par || par.parentNode !== this.textarea) {
return false;
}
c = this.textarea.children;
l = c.length;
for (i = 0; i < l; i++) {
if (c[i] === par) {
break;
}
pos += c[i].textContent.length + 1;
}
return pos + sel.anchorOffset;
}.bind(this);
} else {
this.$textarea = $(this.textarea);
mw.loader.using('jquery.textSelection').done(function () {
this.getPos = function () {
return this.$textarea.textSelection('getCaretPosition');
}.bind(this);
}.bind(this));
}
}
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: this.isDiv ? 'auto' : 'scroll',
resize: 'none',
whiteSpace: 'pre-wrap',
width: '100%',
MozBoxSizing: 'border-box',
WebkitBoxSizing: 'border-box',
boxSizing: 'border-box'
};
setStyles(this.textarea, $.extend({
backgroundColor: 'transparent',
position: this.isDiv ? 'static' : 'absolute',
left: 0,
top: 0
}, commonStyle));
//set fontSize (workaround for subpixel text positioning in Google Chrome/Opera,
//cf. https://code.google.com/p/chromium/issues/detail?id=395425)
//and lineHeight (at least needed for exotic scripts in Firefox) to pixels
setStyles(this.textarea, getStyles(this.textarea, ['fontSize', 'lineHeight']));
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 (!this.isDiv && profile.layout === 'gecko' && profile.versionNumber < 29) {
bugfixStyle.paddingLeft = addPx(style.paddingLeft, 1);
bugfixStyle.paddingRight = addPx(style.paddingRight, 1);
} else if (this.isDiv) {
bugfixStyle.lineHeight = getStyles(this.textarea.firstChild || this.textarea, ['lineHeight']).lineHeight;
}
setStyles(this.highlightbox, $.extend({
backgroundColor: this.oldStyle.backgroundColor,
borderColor: 'transparent',
borderStyle: 'solid',
color: (this.debug ? 'black' : 'transparent'),
position: this.isDiv ? 'absolute' : 'static',
left: 0,
top: 0,
zIndex: this.isDiv ? -1 : 'auto'
}, 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.isDiv ? 'auto' : 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) {
if (!this.proxyCache) {
this.proxyCache = {};
}
if (!hasOwn.call(this.proxyCache, f)) {
this.proxyCache[f] = function () {
this[f].apply(this);
}.bind(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'), 0);
},
getContent: function () {
return this.isDiv ?
$.map(this.textarea.children, function (line) {
return line.textContent;
}).join('\n') :
this.textarea.value;
},
highlight: function () {
var time = mw.now();
this.basicHighlighter.highlight(this.getContent(), this.getPos());
this.reportTime(mw.now() - time);
}
};
function makeCheckboxVE (label, highlighter) {
/* Doing this "the right way"™ isn't difficult, but would require users
to load the script via ve.addPlugin which is more complicated than a simple
importScript or similar, and in visual mode a usesless deactivated tool for
enabling/disabling syntaxhighlight would waste space, and other small things.
So let's do it quick and dirty and hope for native highlighting in NWE soon. */
var $label, $checkbox, $span;;
$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();
}
});
$('#syntaxhighlight, label[for="syntaxhighlight"]').remove();
$span = $('<span>').append($checkbox).append('\n').append($label);
$('#footer').prepend($span);
mw.hook('ve.deactivate').add(function () {
$span.remove();
});
}
function makeCheckboxEdit (label, highlighter) {
mw.loader.using('oojs-ui-core').done(function () {
var checkbox = new OO.ui.CheckboxInputWidget({
selected: true
}), field = new OO.ui.FieldLayout(checkbox, {
id: 'syntaxhighlight',
label: label,
align: 'inline'
});
highlighter.onoff = function (on) {
checkbox.setSelected(on);
};
checkbox.on('change', function () {
if (checkbox.isSelected()) {
highlighter.enable();
} else {
highlighter.disable();
}
});
$('.editCheckboxes > .oo-ui-layout').append(field.$element);
});
}
function makeHighlighter (syntax, colors, id, label) {
var textarea = id ? document.getElementById(id) : $('.ve-ui-mwWikitextSurface .ve-ce-documentNode')[0],
highlighter;
if (!textarea) {
return;
}
highlighter = new Highlighter(syntax, colors, textarea, true);
if (label) {
if (id) {
makeCheckboxEdit(label, highlighter);
} else {
makeCheckboxVE(label, highlighter);
}
}
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 getId () {
if (mw.config.get('wgCanonicalSpecialPageName') === 'Upload') {
return 'wpUploadDescription';
}
if ($('html.ve-activating, html.ve-active').length) { //class can still be ve-activating when toolbar isn't ready yet
return '';
}
return 'wpTextbox1';
}
function initHighlighter (ext, additional) {
var word, id, debug, colors, syntax, highlighter;
id = getId();
debug = !!mw.util.getParamValue('debugSyntaxhighlight');
if (!debug) {
colors = getColors(ext);
syntax = getSyntax(ext);
}
if (!debug && additional) {
for (word in additional) {
if (hasOwn.call(additional, word)) {
colors['additional-' + word] = additional[word];
additional[word] = makeParserFunctionType('additional-' + word);
}
}
syntax = $.extend({}, additional, syntax);
}
highlighter = makeHighlighter(syntax, colors, id, mw.msg('schnark-syntaxhighlight-enable'));
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.slice(0, sel[0] - 1) + text.slice(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][0] === '\t') {
lines[i] = lines[i].slice(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).done(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');
$('.tool[rel="codeEditor"]').remove();
context.fn.disableCodeEditor();
if (String(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 = $('.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 (ve) {
initL10N({
en: {
'schnark-syntaxhighlight-enable': 'Enable syntax highlighter'
},
de: {
'schnark-syntaxhighlight-enable': 'Syntaxhervorhebung aktivieren'
}
});
if (!$.client.test(map) && !mw.util.getParamValue('ignoreBlacklist')) {
return;
}
if (ve && !mw.user.options.get('userjs-schnark-syntaxhighlight-nwe')) {
return;
}
var modelToExt = {
javascript: 'js',
css: 'css',
Scribunto: 'lua',
wikitext: ''
}, ext, deps = [], hasCodeEditor = 0; //0 - no, 1 - yes, disabled, 2 - yes, enabled
if (!ve && mw.config.get('wgCanonicalSpecialPageName') !== 'Upload' && mw.user.options.get('usebetatoolbar')) {
deps.push('ext.wikiEditor');
mw.util.addCSS('.tool-select .options {z-index: 5;}');
}
ext = hasOwn.call(modelToExt, mw.config.get('wgPageContentModel')) ?
modelToExt[mw.config.get('wgPageContentModel')] : 'json';
if (hasOwn.call(mw.user.options.get('schnark-syntaxhighlight-exclude', {}), ext)) {
return;
}
if (ext) {
mw.loader.using('jquery.textSelection').done(allowTabs);
}
if (ext && mw.user.options.get('usebetatoolbar')) {
if (mw.user.options.exists('usecodeeditor')) {
if (String(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').done(function () {
$(function () {
handleCodeEditor(deps, ext, hasCodeEditor === 2);
});
});
}
if (hasCodeEditor !== 2) {
createHighlighter(deps, ext);
}
}
if (
['edit', 'submit'].indexOf(mw.config.get('wgAction')) !== -1 ||
mw.config.get('wgCanonicalSpecialPageName') === 'Upload'
) {
mw.loader.using(['jquery.client', 'mediawiki.util', 'mediawiki.language', 'user.options']).done(function () {
init(); //make sure no parameters are used
});
}
mw.hook('ve.activationComplete').add(function () {
mw.loader.using(['jquery.client', 'mediawiki.util', 'mediawiki.language', 'user.options']).done(function () {
init(true);
});
});
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);
//</nowiki>