/*! * VisualEditor user interface MWSyntaxHighlightDialog class. * * @copyright 2011-2014 VisualEditor Team and others; see AUTHORS.txt * @license The MIT License (MIT); see LICENSE.txt */ /** * MWSyntaxHighlight highlighter * * @constructor * @param {string} lang The language it is working on */ ve.ce.MWSyntaxHighlightHighlighter = function VeCeMWSyntaxHighlightHighlighter( lang ) { // Highlighter rule cache this.ruleset = {}; this.delimiter = /\n/; // Go to the following page for explanations on language rule files // https://www.mediawiki.org/wiki/Extension:SyntaxHighlight_GeSHi/VisualEditor this.rulePath = ve.init.platform.getModulesUrl() + '/syntaxhighlight/rules/'; this.lang = lang; this.validators = new ve.ce.MWSyntaxHighlightValidator(); this.roster = this.validators.getRoster(); // Properties // 'languageName' : [boolean, string];set to false to disable support, string for language name this.langSupport = { 'text' : [true, 'Plain text'], 'javascript' : [true, 'JavaScript'], 'bf' : [true, 'Brainfuck'], 'ruby' : [true, 'Ruby'], 'python' : [true, 'Python'], 'mysql' : [true, 'MySQL'], 'jquery' : [true, 'jQuery'] }; }; /* Methods */ /** * Initialization * * @method */ ve.ce.MWSyntaxHighlightHighlighter.prototype.initialize = function () { this.loadRules( this.lang ); }; /** * Style each token, based on loaded rules * * @method * @param {Array} tokens Tokens * @param {string} dataString Model data string * @param {boolean} validating Whether to use validator's ruleset instead of highlighter's * @returns {Array} Tokens with stying */ ve.ce.MWSyntaxHighlightHighlighter.prototype.mark = function ( tokens, dataString, validating ) { var regex, match, capture, range, regexGroup, except, within, outside, decision, markRange, pickedRules = validating ? this.ruleset.validator : this.ruleset.highlighter, i, j, k; if ( !validating ){ for ( i = 0; i < pickedRules.length; i++){ regex = this.parseRegex( pickedRules[i].match ); while ( (match = regex.exec( dataString )) !== null ){ // Capturing group; all chars in the group need marking capture = match[1]; // Range of capturing group, [a, b); relative to whole string range = [ match.index + match[0].indexOf(capture), match.index + match[0].indexOf(capture) + capture.length]; // Mark based on indices markRange = this.findToken(tokens, range); for ( j = markRange[0]; j <= markRange[1]; j++ ){ tokens[j].mark.push( pickedRules[i].style ); } } } } else { this.validators.setData( dataString ); for ( i = 0; i < pickedRules.length; i++){ regexGroup = []; except = []; within = []; outside = []; // Initialize ranges for ( j = 0; j < pickedRules[i].except.length; j++ ){ except.push(this.parseRegex( pickedRules[i].except[j] )); } for ( j = 0; j < pickedRules[i].within.length; j++ ){ within.push(this.parseRegex( pickedRules[i].within[j] )); } for ( j = 0; j < pickedRules[i].outside.length; j++ ){ outside.push(this.parseRegex( pickedRules[i].outside[j] )); } this.validators.initRange(except, within, outside); // Parse rule regex for ( j = 0; j < pickedRules[i].match.length; j++ ){ regexGroup.push(this.parseRegex( pickedRules[i].match[j] )); } // Make decision decision = this.roster[pickedRules[i].decisionMaker]( regexGroup, this.validators ); // Mark if (decision.needMarks){ for ( j = 0; j < decision.matches.length; j++ ){ match = decision.matches[j]; capture = match[1]; // Range of capturing group, [a, b); relative to whole string range = [ match.index, match.index + capture.length]; // Mark based on indices markRange = this.findToken(tokens, range); for ( k = markRange[0]; k <= markRange[1]; k++ ){ tokens[k].mark.push( pickedRules[i].style ); tokens[k].tip.push( pickedRules[i].tip ); } } } } } return tokens; }; /** * Convert tokens to DOM tree * * @method * @param {Array} tokens Tokens * @returns {string} HTML of entire tree */ ve.ce.MWSyntaxHighlightHighlighter.prototype.display = function ( tokens ) { var lineContainer = '
', numberContainer = '
', line = { 'text':'', 'm':0, 'n':0, 'length':0 }, token = { 'text':'', 'class':'', 'idx':0, 'col':0, 'title':'', 'ph':false, 'tb':false }, lineCount = 1, lineLength = 0, column = 0, trueLength = 0, title = '', i; numberContainer += this.buildNumber( lineCount++ ); // first line line.m = trueLength; for ( i = 0; i < tokens.length; i++ ){ token.text = tokens[i].text; // Styling token.class = tokens[i].mark.join(' '); // Error tips title = tokens[i].tip.join('\n'); if ( title !== '' ){ token.title = title; title = ''; } // Attributes token.idx = tokens[i].index; token.col = column; if (tokens[i].hasOwnProperty('phantom')){ token.ph = true; } if (tokens[i].hasOwnProperty('tab')){ token.tb = true; token.text = ' '; } lineLength += token.text.length; column += token.text.length; trueLength += tokens[i].text.length; line.text += this.buildToken(token); // Reset token = { 'text':'', 'class':'', 'idx':0, 'col':0, 'title':'', 'ph':false, 'tb':false }; // Wrap a line, start a new line if ( this.delimiter.test( tokens[i].text ) ){ line.length = lineLength; line.n = trueLength; lineContainer += this.buildLine(line); // Reset line = { 'text':'', 'm': trueLength, 'n':0, 'length':0 }; lineLength = 0; column = 0; numberContainer += this.buildNumber( lineCount++ ); // New line number } } // Last unwrapped line if ( line.text.length > 0 ){ line.length = lineLength; line.n = trueLength; lineContainer += this.buildLine(line); } // Wrap container numberContainer += '
'; lineContainer += '
'; return { 'tokenDisplay' : lineContainer, 'lineNumber' : numberContainer }; }; /** * Convert tokens to DOM tree for ve.ce node preview * * @method * @param {Array} tokens Tokens * @returns {string} HTML of entire tree (simplified DOM structure) */ ve.ce.MWSyntaxHighlightHighlighter.prototype.displaySimple = function ( tokens ) { var lineContainer = '', token = { 'text':'', 'class':'' }, i; for ( i = 0; i < tokens.length; i++ ){ token.text = tokens[i].text; // Styling token.class = tokens[i].mark.join(' '); // Attributes if (tokens[i].hasOwnProperty('tab')){ token.text = ' '; } lineContainer += ''+token.text+''; // Reset token = { 'text':'', 'class':'' }; } return lineContainer; }; /** * Build line number HTML * * @method * @param {int} number Line number * @returns {string} */ ve.ce.MWSyntaxHighlightHighlighter.prototype.buildNumber = function ( number ){ return '
'+number+'
'; }; /** * Build token HTML * * @method * @param {Object} tokenObject Token * @returns {string} */ ve.ce.MWSyntaxHighlightHighlighter.prototype.buildToken = function ( tokenObject ){ var ph = '', tb = '', title = ''; if (tokenObject.ph){ ph = 'ph = " " ';} if (tokenObject.tb){ tb = 'tb = " " ';} if (tokenObject.title !== ''){ title = 'title = "'+tokenObject.title+'" ';} return ( ''+ tokenObject.text+ '' ); }; /** * Build line HTML * * @method * @param {Object} lineObject Line * @returns {string} */ ve.ce.MWSyntaxHighlightHighlighter.prototype.buildLine = function ( lineObject ){ return '
'+lineObject.text+'
'; }; /** * Parse pre-defined JSON file * * @method * @param {string} lang The language (as filename) */ ve.ce.MWSyntaxHighlightHighlighter.prototype.loadRules = function ( lang ) { var dataClosure = null; jQuery.ajax({ 'async': false, 'global': false, 'url': this.rulePath + lang + '.json', 'dataType': 'json', 'success': function (data) { dataClosure = data;} }); this.ruleset = dataClosure; }; /** * Parse string to RegExp object * * @method * @param {string} regexString * @returns {Object} RegExp object */ ve.ce.MWSyntaxHighlightHighlighter.prototype.parseRegex = function ( regexString ) { var regex = regexString.match(/^\/(.*)\/[igm]*$/)[1], modifier = regexString.match(/^\/.*\/([igm]*$)/)[1]; return new RegExp( regex, modifier ); }; /** * Get the list of supported languages * * @method * @returns {Object} Object describing all supported languages */ ve.ce.MWSyntaxHighlightHighlighter.prototype.getSupportedLanguages = function () { return this.langSupport; }; /** * Get the name of this language * * @method * @param {String} (Optional) Language to be referenced * @returns {String} Language name */ ve.ce.MWSyntaxHighlightHighlighter.prototype.getLanguageName = function ( lang ) { if (lang === undefined){ return this.langSupport[ this.lang ][1]; } else { return this.langSupport[ lang ][1]; } }; /** * Check whether the language is supported * * @method * @returns {boolean} */ ve.ce.MWSyntaxHighlightHighlighter.prototype.isSupportedLanguage = function () { return this.langSupport.hasOwnProperty( this.lang ); }; /** * Check whether the support for the language is enabled * * @method * @returns {boolean} */ ve.ce.MWSyntaxHighlightHighlighter.prototype.isEnabledLanguage = function () { return this.langSupport[ this.lang ][0]; }; /** * Find all tokens that overlap with the given range * * @method * @param {Array} tokens Tokens * @param {Array} range Array describing model range * @returns {Array} Range of token indices */ ve.ce.MWSyntaxHighlightHighlighter.prototype.findToken = function ( tokens, range ) { var items = [], i; for ( i = 0; i < tokens.length; i++ ){ if ( ( tokens[i].index <= range[0] && tokens[i].index + tokens[i].text.length > range[0] ) || ( tokens[i].index > range[0] && tokens[i].index + tokens[i].text.length < range[1] ) || ( tokens[i].index < range[1] && tokens[i].index + tokens[i].text.length >= range[1] ) ){ items.push(tokens[i]); } } if (items.length === 1){ i = tokens.indexOf( items[0] ); return [i, i]; } else { return [tokens.indexOf( items[0] ), tokens.indexOf( items[items.length - 1] )]; } };