VariousHighlight
Within this web site, programming code are shown using highlighted text. To deliver this code in a well formatted manner three parts are neccessary:- PHP which includes parts or whole source code files into the web page.
- Javascript which transforms plain text into html code for highlighting.
- CSS which colors the text based on the html.
First lets look at the PHP file to understand how the code will be imported into a web page:
<?php
require_once(filter_input(INPUT_SERVER, 'DOCUMENT_ROOT') . "/../php/utils/indentation.php");
class VariousHighlight {
private static $currentLanguage = "";
public static function includeFile($language, $file) {
self::start($language);
$lines = file($file);
echo implode("", $lines);
self::end();
}
public static function includeLines($language, $file, $startLine, $endLine) {
self::start($language);
$lines = file($file);
echo implode("", array_slice($lines, $startLine-1, 1+$endLine-$startLine));
self::end();
}
// Needs error handling when starting this twice
public static function start($language) {
self::$currentLanguage = $language;
ob_start();
}
public static function end() {
$content = ob_get_contents();
ob_end_clean();
$encodedContent = str_replace(">", ">", str_replace("<", "<", str_replace("&", "&", $content)));
echo "<code class=\"vh " . self::$currentLanguage . "\">\r\n";
echo Indentation::indentText($encodedContent, 1);
echo "</code>\r\n";
}
}
?>
The main functions IncludeFile and IncludeLines are the important ones.
These will include the files located in the server without manually write the code in the web page.
You also notice that the end function prints the whole thing within <code> tags using class name "vh language".
On another note you will also notice that an indentation call is made. The reason for the indentation call is to keep the generated html code as clean as possible. As you will see if you look at the source code of this web page, the html is correctly indented at all times, even if some parts are automatically generated. To ensure correct indentation the following PHP script is used:
<?php
class Indentation {
private static $DEFAULT_INDENTATION = " ";
public static function getIndentation() {
return self::$DEFAULT_INDENTATION;
}
public static function getIndents($count) {
return str_repeat(self::$DEFAULT_INDENTATION, $count);
}
public static function indentText($text, $indents) {
$indentation = self::getIndents($indents);
if (substr($text, -2) == "\r\n") {
$text = substr_replace($text, "", -2);
}
return str_replace("\r\n", "\r\n" . $indentation, $indentation . $text) . "\r\n";
}
}
?>
When the web page has been generated, then it is time to highlight the code.
This is done using javascript, and the reason for that is to avoid doing too much calculations on the server.
The engine has one dependency to jQuery, and that is to deep clone variables.
The javascript is a bit complicated but we will go through it step by step.
Here is the full javascript code:
/*
* VariousHighlight JavaScript Library v0.2.0
* https://variousdevelopment.com/projects/varioushighlight
*
* Depends on jQuery extend function for cloning objects
*
* Copyright © Various Development
* Date: 2017-03-22
*/
(function() {
var VariousHighlight = function() {
// Object functions
this.clone = function (object) {
return jQuery.extend(true, {}, object);
};
// Is functions
this.is = function(obj, type) { return Object.prototype.toString.call(obj) === type; };
this.isFunction = function(obj) { return vh.is(obj, "[object Function]"); };
this.isArray = function(obj) { return vh.is(obj, "[object Array]"); };
this.isObject = function(obj) { return vh.is(obj, "[object Object]"); };
this.isString = function(obj) { return vh.is(obj, "[object String]"); };
this.isNumber = function(obj) { return vh.is(obj, "[object Number]"); };
this.isRegExp = function(obj) { return vh.is(obj, "[object RegExp]"); };
this.isUndefined = function(obj) { return vh.is(obj, "[object Undefined]"); };
this.isNull = function(obj) { return vh.is(obj, "[object Null]"); };
// String functions
this.htmlEncode = function(str) { return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); };
this.htmlDecode = function(str) { return str.replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&"); };
this.matchExact = function(str, regexp) {
var match = str.match(regexp);
return match !== null && str === match[0];
};
this.matchWords = function(str, words) {
var wordsRegexp = new RegExp("\\b" + words.join("\\b|\\b") + "\\b", "gi");
var word = wordsRegexp.exec(str);
if(word === null) return null;
return word[0];
};
// Format functions
this.trimEmptyLines = function(code) {
return code.replace(/[\s]*(^[\s]*[^\s])/m, "$1").replace(/[\s]*$/, "");
};
this.shiftIndent = function(code) {
var findIndentRegexp = /[\s]*^([\s]*)[^\s]/;
var indent = findIndentRegexp.exec(code);
var removeIndentRegexp = new RegExp("^" + indent[1], "gm");
return code.replace(removeIndentRegexp, "");
};
/// Highlight section
this.languages = {};
this.getLanguage = function(node) {
var language = vh.matchWords(node.className, Object.keys(vh.languages));
if(language === null) return null;
return vh.languages[language.toLowerCase()];
};
this.createHighlightNode = function(value, type, highlight) {
if(highlight === undefined) highlight = "vh-" + type;
return {value:value, type:type, highlight:highlight};
};
this.changeHighlightNode = function(highlightNode, type, highlight) {
if(highlight === undefined) highlight = "vh-" + type;
highlightNode.type = type;
highlightNode.highlight = highlight;
};
this.lex = function(code, lexers) {
var keys = Object.keys(lexers).sort(function(a,b){ return lexers[a].order - lexers[b].order; });
var index = 0;
var highlightTree = [];
while(index < code.length) {
var success = false;
for(var keyIndex=0; keyIndex < keys.length; ++keyIndex) {
var key = keys[keyIndex];
var lexer = lexers[key];
var match = lexer.pattern.exec(code.substring(index));
if(match === null || match[0].length === 0) continue;
var resultString = match[0];
index += resultString.length;
var highlight = lexer.highlight;
if(highlight === undefined) highlight = "vh-" + key;
var type = lexer.type;
if(type === undefined) type = key;
var result = {type:type, highlight:highlight, value:resultString};
highlightTree.push(result);
success = true;
}
if(!success) {
var result = {type:"undefined", highlight:null, value:code[index++]};
highlightTree.push(result);
}
}
return highlightTree;
};
this.convert = function(highlightTree, converters) {
for(var converter in converters) {
if(!vh.isFunction(converters[converter])) continue;
for(var i=0; i<highlightTree.length; ++i) {
converters[converter](highlightTree, i);
}
}
};
this.nextHighlight = function(highlightTree, index, steps) {
if(steps === undefined) steps = 1;
for(var i=index + 1; i<highlightTree.length && steps >=0; ++i) {
if(highlightTree[i].highlight !== null) {
if(--steps === 0)
return highlightTree[i];
}
}
return null;
};
this.prevHighlight = function(highlightTree, index, steps) {
if(steps === undefined) steps = 1;
for(var i=index - 1; i>=0 && steps >=0; --i) {
if(highlightTree[i].highlight !== null) {
if(--steps === 0)
return highlightTree[i];
}
}
return null;
};
this.generateHighlightTree = function(code, language) {
var highlightTree = vh.lex(code, language.lexers);
vh.convert(highlightTree, language.converters);
return highlightTree;
};
this.generateHighlightCode = function(highlightTree) {
var str = "";
for(var i=0; i<highlightTree.length; ++i) {
var node = highlightTree[i];
if(node.highlight !== null) str += "<span class=\""+node.highlight+"\">";
if(vh.isArray(node.value)) str += vh.generateHighlightCode(node.value);
else str += vh.htmlEncode(node.value);
if(node.highlight !== null) str += "</span>";
}
return str;
};
this.highlightCode = function(code, language) {
var highlightTree = vh.generateHighlightTree(code, language);
var highlightCode = vh.generateHighlightCode(highlightTree);
return highlightCode;
};
this.highlight = function(node, language) {
var t0 = performance.now();
var code = vh.htmlDecode(node.innerHTML);
code = vh.trimEmptyLines(code);
if(code.length === 0) return;
code = vh.shiftIndent(code);
node.innerHTML = vh.highlightCode(code, language);
var t1 = performance.now();
console.log("Highlight duration: " + (Math.round((t1 - t0) * 100) / 100) + " ms.");
};
this.highlightAll = function() {
$(".vh").each(function(_, node) {
var language = vh.getLanguage(node);
if(language !== null)
vh.highlight(node, language);
});
};
};
window.vh = new VariousHighlight();
})();
vh.languages.generic = {
lexers:{
// Lexers are not allowed to have global or multiline
// Lexers need to start with ^ to represent start of string
// Lexers should not end with $ unless that really is the wanted behavior
// Each lexer can contain: type, highlight, pattern and order
// However, pattern and order are mandatory
// Valid formats:
// Key:{pattern:/pattern/, order:10}
// Key:{highlight:"vh-highlight", pattern:/pattern/, order:10}
// Key:{type:"type", highlight:"vh-highlight", pattern:/pattern/, order:10}
// Type is automatically the key, unless manually set
// Highlight is automatically the key with prefix "vh-", unless manually set
// Avoid using "vh-" as key, use variable name conventions for keys
whitespace:{highlight:null, pattern:/^[\s]+/, order:10},
string:{pattern:/^""|^"[^]*?[^\\]"|^"[^"]*|^''|^'[^]*?[^\\]'|^'[^']*/, order:20},
comment:{pattern:/^\/\/[^\n]*\n|^\/\*[^]*?\*\/|^\/\*[^]*/, order:30},
identifier:{pattern:/^[a-z_][a-z0-9_]*/i, order:40},
number:{pattern:/^[0-9]+\.[0-9]+f{0,1}|^[0-9]+L{0,1}/, order:50},
symbol:{pattern:/^[!@#$%^&*()_+\-=\[\]{};':\\|,.<>\/?]/, order:60}
},
converters:{
// TODO: Converters need an order
operators:function(highlightTree, index) {
var node = highlightTree[index];
if(vh.matchExact(node.value, this.convertRules.operator)) {
vh.changeHighlightNode(node, "operator");
}
},
keywords:function(highlightTree, index) {
var node = highlightTree[index];
if(vh.matchExact(node.value, this.convertRules.keyword)) {
vh.changeHighlightNode(node, "keyword");
}
},
type:function(highlightTree, index) {
var node = highlightTree[index];
var node2 = vh.nextHighlight(highlightTree, index);
if(node.type === "identifier" && node2 !== null && node2.type === "identifier") {
vh.changeHighlightNode(node, "type");
}
},
classType:function(highlightTree, index) {
var node = highlightTree[index];
var node2 = vh.prevHighlight(highlightTree, index);
if(node.type === "identifier" && node2 !== null && vh.matchExact(node2.value, /class|interface|struct/)) {
vh.changeHighlightNode(node, "type");
}
},
methodType:function(highlightTree, index) {
var node = highlightTree[index];
var node2 = vh.nextHighlight(highlightTree, index);
if(node.type === "identifier" && node2 !== null && (node2.value === "(" || node2.value === "()")) {
vh.changeHighlightNode(node, "method");
}
},
genericType:function(highlightTree, index) {
var node = highlightTree[index];
var node2 = vh.nextHighlight(highlightTree, index);
var node3 = vh.nextHighlight(highlightTree, index, 2);
var node4 = vh.nextHighlight(highlightTree, index, 3);
if(node2 === null || node3 === null || node4 === null) return;
if(node.type === "identifier" && node2.value === "<" && node3.type === "identifier" && vh.matchExact(node4.value, /<|\,|>/)) {
vh.changeHighlightNode(node, "type");
}
},
convertRules:{
keyword:/abstract|as|base|bool|break|byte|case|catch|char|checked|class|const|continue|decimal|default|delegate|double|do|else|enum|event|explicit|extern|false|finally|fixed|float|foreach|for|goto|if|implicit|interface|internal|int|in|is|lock|long|namespace|new|null|object|operator|out|override|params|private|protected|public|readonly|ref|return|sbyte|sealed|short|sizeof|stackalloc|static|string|struct|switch|this|throw|true|try|typeof|uint|ulong|unchecked|unsafe|ushort|using|var|virtual|void|volatile|while|where/,
operator:/=>|={1,2}|!={0,1}|\>=|\>{1,2}|<=|<{1,2}|\+=|\+{1,2}|-=|-{1,2}|\|\||\|={0,1}|&=|&{1,2}|\?{1,2}|%={0,1}|\^={0,1}|:|;|\(|\)|\{|\}/
}
}
};
vh.languages.xml = {
lexers:{
whitespace:{highlight:null, pattern:/^[^<]+/, order:10},
comment:{pattern:/^<!--[^$]*?-->|^<!--[^$]*/, order:20},
internalTag:{highlight:null, pattern:/^<[^>]*>{0,1}/, order:30}
},
converters:{
tagInternal:function(highlightTree, index) {
var node = highlightTree[index];
if(node.type === "internalTag") {
node.value = vh.lex(node.value, vh.languages.xml.internalLexers);
}
}
},
internalLexers:{
whitespace:{highlight:null, pattern:/^[\s]+/, order:10},
tag:{pattern:/^<\/{0,1}[a-z0-9_]*>{0,1}/i, order:20},
string:{pattern:vh.languages.generic.lexers.string.pattern, order:30},
number:{pattern:vh.languages.generic.lexers.number.pattern, order:40},
keyword:{pattern:/^[a-z_][a-z0-9_]*/i, order:50},
symbol:{pattern:/^[^\s<"0-9a-z]+/i, order:60}
}
};
vh.languages.cplusplus = vh.clone(vh.languages.generic);
vh.languages.c = vh.clone(vh.languages.generic);
vh.languages.java = vh.clone(vh.languages.generic);
vh.languages.javascript = vh.clone(vh.languages.generic);
vh.languages.javascript.lexers.identifier.pattern = /^[a-z_\$][a-z0-9_\$]*/i;
vh.languages.javascript.lexers.regexp = {highlight:"vh-tag", pattern:/^\/[^\n]*?[^\\]\/[\w]*/, order:35};
vh.languages.php = vh.clone(vh.languages.javascript);
vh.languages.css = vh.clone(vh.languages.generic);
vh.languages.html = vh.clone(vh.languages.xml);
vh.languages.csharp = vh.clone(vh.languages.generic);
vh.languages.csharp.lexers.internalDoc = {highlight:null, pattern:/^\/\/\/[^$]*?\n/, order:25};
vh.languages.csharp.converters.doc = function(highlightTree, index) {
var node = highlightTree[index];
if(node.type === "internalDoc") {
var internalHighlightTree = vh.lex(node.value.substring(3), vh.languages.xml.lexers);
internalHighlightTree.unshift(vh.createHighlightNode("///", "doc"));
for(var i=0; i<internalHighlightTree.length; ++i) {
var internalNode = internalHighlightTree[i];
if(internalNode.type === "whitespace") {
vh.changeHighlightNode(internalNode, "comment");
}
else if(internalNode.type === "internalTag") {
vh.changeHighlightNode(internalNode, "doc");
}
}
node.value = internalHighlightTree;
}
};
In order to use this highlighting engine, the highlightAll function needs to be called.
This must be done after the document has loaded, which can be done easiest with jQuery:
<head>
<script>$(vh.highlightAll);</script>
</head>
Finally after the html code has been generated by varioushighlight, then a stylesheet is necessary to color it.
This css file is relatively simple:
kbd {
background-color: #CCC;
}
.vh {
display: block;
overflow: auto;
max-height: 250px;
padding: 5px;
background-color: #EEE;
color: #000;
white-space: pre;
}
@media screen and (min-height: 600px){
.vh {
max-height: 300px;
}
}
@media screen and (min-height: 800px){
.vh {
max-height: 500px;
}
}
.vh-identifier { color: #000; }
.vh-keyword { color: #00F; }
.vh-type { color: #3AA; }
.vh-method { color: #000; }
.vh-variable { color: #000; }
.vh-string, .vh-char { color: #A33; }
.vh-number { color: #AA0; }
.vh-operator { color: #000; }
.vh-symbol { color: #000; }
.vh-comment { color: #3A3; }
.vh-doc { color: #AAA; }
.vh-tag { color: #A0C; }
And thats it!
just to make it a bit weird, I will now show you the full PHP code of this web page that you are seeing. This is automatically included and here is the source-code of this page:
<?php
require_once(filter_input(INPUT_SERVER, 'DOCUMENT_ROOT') . "/../php/main-page/main-page.php");
require_once(filter_input(INPUT_SERVER, 'DOCUMENT_ROOT') . "/../php/code/varioushighlight.php");
$mainPage = new MainPage();
$mainPage->setPageTitle("VariousHighlight");
$mainPage->setNavInfo("Projects - VariousHighlight");
$mainPage->addDependency("<script src=\"/lib/varioushighlight.js\"></script>");
$mainPage->addDependency("<link href=\"/lib/code.css\" rel=\"stylesheet\">");
$mainPage->addDependency("<script>$(vh.highlightAll);</script>");
$mainPage->printTop();
?>
<h3>VariousHighlight</h3>
Within this web site, programming code are shown using highlighted text.
To deliver this code in a well formatted manner three parts are neccessary:
<ul>
<li>PHP which includes parts or whole source code files into the web page.</li>
<li>Javascript which transforms plain text into html code for highlighting.</li>
<li>CSS which colors the text based on the html.</li>
</ul>
<br>
First lets look at the PHP file to understand how the code will be imported into a web page:
<?php VariousHighlight::includeFile("php", filter_input(INPUT_SERVER, 'DOCUMENT_ROOT') . "/../php/code/varioushighlight.php") ?>
The main functions IncludeFile and IncludeLines are the important ones.
These will include the files located in the server without manually write the code in the web page.
You also notice that the end function prints the whole thing within <code> tags using class name "vh language".
<br>
On another note you will also notice that an indentation call is made.
The reason for the indentation call is to keep the generated html code as clean as possible.
As you will see if you look at the source code of this web page, the html is correctly indented at all times, even if some parts are automatically generated.
To ensure correct indentation the following PHP script is used:
<?php VariousHighlight::includeFile("php", filter_input(INPUT_SERVER, 'DOCUMENT_ROOT') . "/../php/utils/indentation.php") ?>
When the web page has been generated, then it is time to highlight the code.
This is done using javascript, and the reason for that is to avoid doing too much calculations on the server.
The engine has one dependency to jQuery, and that is to deep clone variables.
The javascript is a bit complicated but we will go through it step by step.
Here is the full javascript code:
<?php VariousHighlight::includeFile("javascript", filter_input(INPUT_SERVER, 'DOCUMENT_ROOT') . "/lib/varioushighlight.js") ?>
In order to use this highlighting engine, the highlightAll function needs to be called.
This must be done after the document has loaded, which can be done easiest with jQuery:
<?php VariousHighlight::start("html") ?>
<head>
<script>$(vh.highlightAll);</script>
</head>
<?php VariousHighlight::end() ?>
Finally after the html code has been generated by varioushighlight, then a stylesheet is necessary to color it.
This css file is relatively simple:
<?php VariousHighlight::includeFile("css", filter_input(INPUT_SERVER, 'DOCUMENT_ROOT') . "/lib/code.css") ?>
And thats it!
<br><br>
just to make it a bit weird, I will now show you the full PHP code of this web page that you are seeing.
This is automatically included and here is the source-code of this page:
<?php VariousHighlight::includeFile("html", "index.php") ?>
<?php
$mainPage->printBottom();
?>