mediawiki-extensions-Visual.../modules/syntaxhighlight/helpers/ve.ce.MWSyntaxHighlightHighlighter.js

408 lines
10 KiB
JavaScript
Raw Normal View History

/*!
* 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 = '<div>',
numberContainer = '<div>',
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 += '</div>';
lineContainer += '</div>';
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 += '<span class="'+token.class+'">'+token.text+'</span>';
// 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 '<pre class="ve-ui-simplesurface-line"><span>'+number+'</span></pre>';
};
/**
* 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 (
'<span class="'+
tokenObject.class+
'" idx='+
tokenObject.idx+
' col='+
tokenObject.col+
' '+
title+ph+tb+
'>'+
tokenObject.text+
'</span>'
);
};
/**
* Build line HTML
*
* @method
* @param {Object} lineObject Line
* @returns {string}
*/
ve.ce.MWSyntaxHighlightHighlighter.prototype.buildLine = function ( lineObject ){
return '<pre class="ve-ui-simplesurface-line" m='+lineObject.m+' n='+lineObject.n+' length='+lineObject.length+'>'+lineObject.text+'</pre>';
};
/**
* 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] )];
}
};