Yesterday and today I've made a very basic syntax highlight.
It creates a function in the window
object that handles part of the job:
(function(window){
var f=window.highlight = function(lang, element){
var lang_defs = f[lang];
if(!lang_defs)
{
throw new TypeError( 'The language "' + lang + '" was not yet defined' );
}
if(!(element instanceof Element))
{
throw new TypeError( 'The 2nd parameter must be an Element' );
}
element.className += ' highlight ' + lang;
for(var i = 0, l = lang_defs.length; i<l; i++)
{
var html = '';
for(var j = 0, k = element.childNodes.length; j<k; j++)
{
if(element.childNodes[j].nodeType === 3)
{
html += element.childNodes[j].nodeValue
.replace(
lang_defs[i].match,
/*shortcut to decide if the lang_defs[i].replace is one of those types
*if so, passes it directly
*otherwise, makes a string matching based on the object
*/
{'string':1, 'function':1}[ typeof lang_defs[i].replace ]
? lang_defs[i].replace
: '<' + lang_defs[i].replace.tag +
' class="' + lang_defs[i]['class'] + '">' +
lang_defs[i].replace.text +
'</' + lang_defs[i].replace.tag + '>'
);
}
else
{
html += element.childNodes[j].outerHTML;
}
}
element.innerHTML = html;
if('function' === typeof lang_defs[i].patch)
{
var returned = lang_defs[i].patch.call( element );
if('string' === typeof returned)
{
element.innerHTML = returned;
}
}
}
};
//default replace object
f.default_replace = {'tag': 'span', 'text': '$1'};
})(Function('return this')());//just be sure that we have the real window
Each language is a property in the function, which is then read (sql example):
(function(window){
if('function' === typeof window.highlight)
{
window.highlight.sql=[
{
'class':'string',
'match':/([bn]?"(?:[^"]|[\\"]")*"|[bn]?'(?:[^']|[\\']')*')(?=[\b\s\(\),;\$#\+\-\*\/]|$)/g,
'replace':window.highlight.default_replace
},
{
'class':'comment',
'match':/((?:\/\/|\-\-\s|#)[^\r\n]*|\/\*(?:[^*]|\*[^\/])*(?:\*\/|$))/g,
'replace':window.highlight.default_replace,
'patch':function(){
return this.innerHTML.replace(
/((?:\/\/|\-\-\s|#)[^\r\n]*|\/\*(?:[^*]|\*[^\/])*(?:\*\/|$))/g,
'$1</span>'
).replace(//matches single-line comments
/<span class="comment">((?:#|-- |\/\/)(?:.|<\/span><span class="[^"]+">([^<])<\/span>)*)([\r\n]|$)/g,
function(_,part1,part2,part3){
return '<span class="comment">'+
//cleans up all spans
((part1||'')+(part2||'')).replace(/<\/?span(?: class="[^"]+")?>/g,'')+
'</span>'+
(part3||'');
}
).replace(//matches multi-line comments
/<span class="comment">(\/\*(?:[^*]|\*[^\/])+(?:\*\/(?:<\/span>)?|$))/g,
function(_,part1){
return '<span class="comment">'+
//cleans up all spans
((part1||'')).replace(/<\/?span(?: class="[^"]+")?>/g,'')+
'</span>';
}
);
}
},
{
/*
* numbers aren't that 'regular' and many edge-cases were left behind
* with the help of @MLM (http://stackoverflow.com/users/796832/mlm),
* we were able to make this work.
* he took over the regex and patched it all up, I did the replace string
*/
'match':/((?:^|\b|\(|\s|,))(?![a-z_]+)([+\-]?\d+(?:\.\d+)?(?:[eE]-?\d+)?)((?=$|\b|\s|\(|\)|,|;))/g,
'replace':'$1<span class="number">$2</span>$3'
},
{
'class':'name',
'match':/(`[^`]+`)/g,
'replace':window.highlight.default_replace
},
{
'class':'var',
'match':/(@@?[a-z_][a-z_\d]*)/g,
'replace':window.highlight.default_replace
},
{
'class':'keyword',
//the keyword replace must have an aditional check (`(?!\()` after the name), due to the function replace()
'match':/\b(accessible|add|all|alter|analyze|and|as|asc|asensitive|before|between|bigint|binary|blob|both|by|call|cascade|case|change|char|character|check|collate|column|condition|constraint|continue|convert|create|cross|current_date|current_time|current_timestamp|current_user|cursor|database|databases|day_hour|day_microsecond|day_minute|day_second|dec|decimal|declare|default|delayed|delete|desc|describe|deterministic|distinct|distinctrow|div|double|drop|dual|each|else|elseif|enclosed|escaped|exists|exit|explain|false|fetch|float|float4|float8|for|force|foreign|from|fulltext|generated|get|grant|group|having|high_priority|hour_microsecond|hour_minute|hour_second|if|ignore|in|index|infile|inner|inout|insensitive|insert|int|int1|int2|int3|int4|int8|integer|interval|into|io_after_gtids|io_before_gtids|is|iterate|join|key|keys|kill|leading|leave|left|like|limit|linear|lines|load|localtime|localtimestamp|lock|long|longblob|longtext|loop|low_priority|master_bind|master_ssl_verify_server_cert|match|maxvalue|mediumblob|mediumint|mediumtext|middleint|minute_microsecond|minute_second|mod|modifies|natural|nonblocking|not|no_write_to_binlog|null|numeric|on|optimize|optimizer_costs|option|optionally|or|order|out|outer|outfile|parse_gcol_expr|partition|precision|primary|procedure|purge|range|read|reads|read_write|real|references|regexp|release|rename|repeat|replace(?!\()|require|resignal|restrict|return|revoke|right|rlike|schema|schemas|second_microsecond|select|sensitive|separator|set|show|signal|smallint|spatial|specific|sql|sqlexception|sqlstate|sqlwarning|sql_big_result|sql_calc_found_rows|sql_small_result|ssl|starting|stored|straight_join|table|terminated|then|tinyblob|tinyint|tinytext|to|trailing|trigger|true|undo|union|unique|unlock|unsigned|update|usage|use|using|utc_date|utc_time|utc_timestamp|values|varbinary|varchar|varcharacter|varying|virtual|when|where|while|with|write|xor|year_month|zerofill)\b/gi,
'replace':window.highlight.default_replace
},
{
'class':'func',
'match':/\b([a-z_][a-z_\d]*)\b(?=\()/gi,
'replace':window.highlight.default_replace
},
{
'class':'name',
'match':/\b([a-z\_][a-z_\d]*)\b/gi,
'replace':window.highlight.default_replace
}
];
}
})(Function('return this')());
There are 2 themes: the default one and then a console-like one.
Example of the execution:
// highlight function, separated file
(function(window){
var f=window.highlight = function(lang, element){
var lang_defs = f[lang];
if(!lang_defs)
{
throw new TypeError( 'The language "' + lang + '" was not yet defined' );
}
if(!(element instanceof Element))
{
throw new TypeError( 'The 2nd parameter must be an Element' );
}
element.className += ' highlight ' + lang;
for(var i = 0, l = lang_defs.length; i<l; i++)
{
var html = '';
for(var j = 0, k = element.childNodes.length; j<k; j++)
{
if(element.childNodes[j].nodeType === 3)
{
html += element.childNodes[j].nodeValue
.replace(
lang_defs[i].match,
/*shortcut to decide if the lang_defs[i].replace is one of those types
*if so, passes it directly
*otherwise, makes a string matching based on the object
*/
{'string':1, 'function':1}[ typeof lang_defs[i].replace ]
? lang_defs[i].replace
: '<' + lang_defs[i].replace.tag +
' class="' + lang_defs[i]['class'] + '">' +
lang_defs[i].replace.text +
'</' + lang_defs[i].replace.tag + '>'
);
}
else
{
html += element.childNodes[j].outerHTML;
}
}
element.innerHTML = html;
if('function' === typeof lang_defs[i].patch)
{
var returned = lang_defs[i].patch.call( element );
if('string' === typeof returned)
{
element.innerHTML = returned;
}
}
}
};
//default replace object
f.default_replace = {'tag': 'span', 'text': '$1'};
})(Function('return this')());//just be sure that we have the real window
//==========================================================
// sql syntax highlight, anothed different file
(function(window){
if('function' === typeof window.highlight)
{
window.highlight.sql=[
{
'class':'string',
'match':/([bn]?"(?:[^"]|[\\"]")*"|[bn]?'(?:[^']|[\\']')*')(?=[\b\s\(\),;\$#\+\-\*\/]|$)/g,
'replace':window.highlight.default_replace
},
{
'class':'comment',
'match':/((?:\/\/|\-\-\s|#)[^\r\n]*|\/\*(?:[^*]|\*[^\/])*(?:\*\/|$))/g,
'replace':window.highlight.default_replace,
'patch':function(){
return this.innerHTML.replace(
/((?:\/\/|\-\-\s|#)[^\r\n]*|\/\*(?:[^*]|\*[^\/])*(?:\*\/|$))/g,
'$1</span>'
).replace(//matches single-line comments
/<span class="comment">((?:#|-- |\/\/)(?:.|<\/span><span class="[^"]+">([^<])<\/span>)*)([\r\n]|$)/g,
function(_,part1,part2,part3){
return '<span class="comment">'+
//cleans up all spans
((part1||'')+(part2||'')).replace(/<\/?span(?: class="[^"]+")?>/g,'')+
'</span>'+
(part3||'');
}
).replace(//matches multi-line comments
/<span class="comment">(\/\*(?:[^*]|\*[^\/])+(?:\*\/(?:<\/span>)?|$))/g,
function(_,part1){
return '<span class="comment">'+
//cleans up all spans
((part1||'')).replace(/<\/?span(?: class="[^"]+")?>/g,'')+
'</span>';
}
);
}
},
{
/*
* numbers aren't that 'regular' and many edge-cases were left behind
* with the help of @MLM (http://stackoverflow.com/users/796832/mlm),
* we were able to make this work.
* he took over the regex and patched it all up, I did the replace string
*/
'match':/((?:^|\b|\(|\s|,))(?![a-z_]+)([+\-]?\d+(?:\.\d+)?(?:[eE]-?\d+)?)((?=$|\b|\s|\(|\)|,|;))/g,
'replace':'$1<span class="number">$2</span>$3'
},
{
'class':'name',
'match':/(`[^`]+`)/g,
'replace':window.highlight.default_replace
},
{
'class':'var',
'match':/(@@?[a-z_][a-z_\d]*)/g,
'replace':window.highlight.default_replace
},
{
'class':'keyword',
//the keyword replace must have an aditional check (`(?!\()` after the name), due to the function replace()
'match':/\b(accessible|add|all|alter|analyze|and|as|asc|asensitive|before|between|bigint|binary|blob|both|by|call|cascade|case|change|char|character|check|collate|column|condition|constraint|continue|convert|create|cross|current_date|current_time|current_timestamp|current_user|cursor|database|databases|day_hour|day_microsecond|day_minute|day_second|dec|decimal|declare|default|delayed|delete|desc|describe|deterministic|distinct|distinctrow|div|double|drop|dual|each|else|elseif|enclosed|escaped|exists|exit|explain|false|fetch|float|float4|float8|for|force|foreign|from|fulltext|generated|get|grant|group|having|high_priority|hour_microsecond|hour_minute|hour_second|if|ignore|in|index|infile|inner|inout|insensitive|insert|int|int1|int2|int3|int4|int8|integer|interval|into|io_after_gtids|io_before_gtids|is|iterate|join|key|keys|kill|leading|leave|left|like|limit|linear|lines|load|localtime|localtimestamp|lock|long|longblob|longtext|loop|low_priority|master_bind|master_ssl_verify_server_cert|match|maxvalue|mediumblob|mediumint|mediumtext|middleint|minute_microsecond|minute_second|mod|modifies|natural|nonblocking|not|no_write_to_binlog|null|numeric|on|optimize|optimizer_costs|option|optionally|or|order|out|outer|outfile|parse_gcol_expr|partition|precision|primary|procedure|purge|range|read|reads|read_write|real|references|regexp|release|rename|repeat|replace(?!\()|require|resignal|restrict|return|revoke|right|rlike|schema|schemas|second_microsecond|select|sensitive|separator|set|show|signal|smallint|spatial|specific|sql|sqlexception|sqlstate|sqlwarning|sql_big_result|sql_calc_found_rows|sql_small_result|ssl|starting|stored|straight_join|table|terminated|then|tinyblob|tinyint|tinytext|to|trailing|trigger|true|undo|union|unique|unlock|unsigned|update|usage|use|using|utc_date|utc_time|utc_timestamp|values|varbinary|varchar|varcharacter|varying|virtual|when|where|while|with|write|xor|year_month|zerofill)\b/gi,
'replace':window.highlight.default_replace
},
{
'class':'func',
'match':/\b([a-z_][a-z_\d]*)\b(?=\()/gi,
'replace':window.highlight.default_replace
},
{
'class':'name',
'match':/\b([a-z\_][a-z_\d]*)\b/gi,
'replace':window.highlight.default_replace
}
];
}
})(Function('return this')());
//=========================================================
// execution example:
window.onload = function(){
highlight('sql', document.getElementsByTagName('pre')[0]);
}
/*console theme, main file*/
.highlight, .highlight *{
background:black;
color:white;
font-family:'Consolas',monospace;
font-size:16px;
word-wrap: break-word;
white-space: pre;
}
/*sql style, different file*/
.highlight.sql .keyword{color:teal;}
.highlight.sql .string{color:red;}
.highlight.sql .func{color:purple;}
.highlight.sql .number{color:#0F0;}
.highlight.sql .name{color:olive;}
.highlight.sql .var{color:green;}
.highlight.sql .comment{color:gray;}
<!-- example code, found somewhere on google, with some edge cases -->
<pre>
CREATE TABLE `shop` (
article INT(4) UNSIGNED ZEROFILL DEFAULT '0000' NOT NULL,
dealer CHAR(20) DEFAULT '' NOT NULL,
price DOUBLE(16,2) DEFAULT '0.00' NOT NULL,
PRIMARY KEY(article, dealer));
INSERT INTO shop VALUES
(1,'A',3.45),(1,'B',3.99),(2,'A',10.99),(3,'B',1.45),
(3,'C',1.69),(3,'D',-1.25),(4,'D',19.95);
#This is an example of sql a = 'b', 'c'
SELECT MAX(article) AS article FROM shop;
SELECT s1.article, s1.dealer, s1.price
FROM shop s1
LEFT JOIN shop s2 ON s1.price < s2.price
WHERE s2.article IS NULL;
SELECT article, MAX(price) AS price
FROM shop
GROUP BY article;
SELECT article, dealer, price
FROM shop s1
WHERE price=(SELECT MAX(s2.price)
FROM shop s2
WHERE s1.article = s2.article);
SELECT @min_price:=MIN(price),@max_price:=MAX(price) FROM shop;
SELECT * FROM shop WHERE price=@min_price OR price=@max_price;
/*
a string =' ',
*/
select '1'#comment
/*''*/ or 2;
</pre>
I'm actually happy with the results, but the comment highlighting is concerning me.
Is there anything I can do to improve the highlight syntax?
What can I do to improve the performance?
Am I handling comments correctly or should I try a different way?