This web based syntax highlighting tool is written in TypeScript. It can be easily extended to have more languages and color schemes.
To add a new language simply inherit from the BaseLanguage class and override the isKeyword method. To add a new color scheme implement
the IColorScheme interface to return the desired colors for each class.
Although this tool can create static HTML it is recommended to apply syntax highlighting at page load:
<div class="code-block" syntax-hl="JavaScript,Dark">[Code goes here.]</div>
$(document).ready(() => {
// Highlight all code marked with code-block css class and "syntax-hl" attribute:
var shl = new BC.SyntaxHL.SyntaxHighLighter();
shl.highlightCodeBlocks();
});
This will highlight any code block on page marked with "code-block" class and "syntax-hl" attribute.
ex: <div id="divToHighlight" class="code-block" syntax-hl="CSharp,Dark">
namespace BC.SyntaxHL {
interface ILanguage {
applyHtml(code: string): string;
}
interface IColorScheme {
getCss(): string;
}
export class SyntaxHighLighter {
/**
* Returns code with HTML tags insert to provide syntax highlighting.
* @param code
* @param languageName
*/
public highlightCode(code: string, languageName: string): string {
let language = this.getInstance(languageName);
if (language === null || language === undefined) {
console.error(`Unable to instantiate language class ${languageName}`);
return code;
}
let retVal = language.applyHtml(code);
return retVal;
}
/**
* Applies syntax highlighting to the innerHTML of any element with the class "code-block" and
* attribute "syntax-hl" containing language and colorscheme. Call this on window.onload.
*/
public highlightCodeBlocks(): void {
let codeBlocks = Array.from(document.getElementsByClassName("code-block"));
for (let elem of codeBlocks) {
let attrib = elem.getAttribute("syntax-hl");
if (attrib === null || attrib.indexOf(",") < 0) continue;
let tokens = attrib.split(",", 2);
let language = tokens[0].trim();
elem.innerHTML = this.highlightCode(elem.textContent.trim(), language);
let colorScheme = tokens[1].trim();
}
}
/**
* Returns the CSS for the color scheme.
* @param colorScheme
*/
public getCss(colorScheme: string) {
let cs = this.getInstance(colorScheme);
return cs.getCss();
}
/**
* Instantiate an object by class name
* @param className Just class name, assumes this name space
*/
private getInstance(className: string): T {
if (className === undefined || className === null || className.length === 0) return null;
let instance = new BC.SyntaxHL[className]();
return instance;
}
}
class Comment {
public start:string;
public end: string;
constructor(start: string, end: string) {
this.start = start;
this.end = end;
}
}
/**
* Provides base functionality to syntax highlighting.
* Language just need to extend and override anything required, usually at least isKeyword()
*/
class LanguageBase implements ILanguage {
protected keywords: string[];
protected comments: Comment[];
protected symbols: number[];
constructor() {
this.comments = [new Comment("//", "\n"), new Comment("/*", "*/")];
this.symbols = [
33, 35, 36, 37, 38, 40, 41, 42, 43, 44, 45, 46, 47, 58, 59, 60, 61,
62, 63, 64, 91, 92, 93, 94, 123, 124, 125, 126
];
}
/**
* Parses code adding span tags with class attributes.
* @param code
*/
public applyHtml(code: string): string {
let retVal: string[] = [];
let classes: string[] = [];
let enums: string[] = [];
let interfaces: string[] = [];
this.getDefinitionNames(code, classes, enums, interfaces);
let i = 0;
while (i < code.length) {
let c = code.charCodeAt(i);
let codeBlockStart = i;
let commentType: Comment = null;
if (this.isWhiteSpace(c)) {
i = this.findEndOfWhiteSpace(code, i);
retVal.push(code.substr(codeBlockStart, i - codeBlockStart));
}
else if (this.isAlpha(c)) { // Start of an identifier:
i = this.findEndOfIdentifier(code, i);
let identifier = code.substr(codeBlockStart, i - codeBlockStart);
if (this.isKeyword(identifier)) // Keyword:
retVal.push(this.getSpan("keyword", identifier));
else if (classes.indexOf(identifier) > -1) // Class:
retVal.push(this.getSpan("class", identifier));
else if (interfaces.indexOf(identifier) > -1) // Interface:
retVal.push(this.getSpan("interface", identifier));
else if (enums.indexOf(identifier) > -1) // Enum:
retVal.push(this.getSpan("enum", identifier));
else // Variable:
retVal.push(this.getSpan("identifier", identifier));
}
else if (this.isNumeric(c)) { // Start of a number:
i = this.findEndOfNumber(code, i);
retVal.push(this.getSpan("numeric", code.substr(codeBlockStart, i - codeBlockStart)));
}
else if ((commentType = this.isComment(code, i)) !== null) { // Comment:
i = this.findEndOfComment(code, i, commentType);
retVal.push(this.getSpan("comment", code.substr(codeBlockStart, i - codeBlockStart)));
}
else if (this.isQuotes(c)) { // Start of a string:
i = this.findEndOfString(code, i, c);
retVal.push(this.getSpan("string", code.substr(codeBlockStart, i - codeBlockStart)));
}
else if (this.isSymbol(c)) { // Symbol:
i = this.findEndOfSymbols(code, i);
retVal.push(this.getSpan("symbol", code.substr(codeBlockStart, i - codeBlockStart)));
} else {
// Everything else just add it as is:
retVal.push(code.charAt(i));
i++;
}
}
return retVal.join("");
}
/**
* Prescan the provided code to get a list of classes, interfaces and enums so we can highlight them properly.
* @param code
* @param classes
* @param enums
* @param interfaces
*/
protected getDefinitionNames(code: string, classes: string[], enums: string[], interfaces: string[]): void {
let i = 0;
while (i < code.length) {
let c = code.charCodeAt(i);
let codeBlockStart = i;
let commentType: Comment;
if (this.isAlpha(c)) { // Start of an identifier:
i = this.findEndOfIdentifier(code, i);
let identifier = code.substr(codeBlockStart, i - codeBlockStart);
if (identifier === "class" || identifier === "enum" || identifier === "interface" || identifier === "new") {
i = this.findEndOfWhiteSpace(code, i); // Skip white space to next identifier
codeBlockStart = i;
// get the identifier name:
i = this.findEndOfIdentifier(code, i);
let name = code.substr(codeBlockStart, i - codeBlockStart);
// Add name to corresponding array:
if (identifier === "class")
classes.push(name);
else if (identifier === "enum")
enums.push(name);
else if (identifier === "interface")
interfaces.push(name);
}
}
// Skip comments:
else if ((commentType = this.isComment(code, i)) !== null) { // Comment:
i = this.findEndOfComment(code, i, commentType);
}
// Skip strings:
else if (this.isQuotes(c)) { // Start of a string:
i = this.findEndOfString(code, i, c);
} else { // Skip everything else:
i++;
}
}
}
protected findEndOfSymbols(code: string, i: number): number {
i++;
while (i < code.length) {
let c = code.charCodeAt(i);
if (!this.isSymbol(c) && !this.isWhiteSpace(c) || this.isComment(code, i)) return i;
i++;
}
return i;
}
protected findEndOfIdentifier(code: string, i: number): number {
i++;
while (i < code.length) {
let c = code.charCodeAt(i);
if (!this.isNumeric(c) && !this.isAlpha(c)) return i;
i++;
}
return i;
}
protected findEndOfComment(code: string, i: number, commentType: Comment): number {
i += commentType.start.length; // Move past the comment opener
while (i + commentType.end.length < code.length) {
let cc = code.substring(i, i + commentType.end.length);
if (commentType.end === cc)
return i + commentType.end.length;
i++;
}
return i;
}
protected findEndOfString(code: string, i: number, quote: number): number {
i++;
while (i < code.length) {
let c = code.charCodeAt(i);
if (c === 92) { // Escape character
i += 2; // Ignore this character AND the next one.
continue;
}
// If we found a quote matching the open quote end of string!
if (c === quote) return i + 1;
i++;
}
return i;
}
protected findEndOfNumber(code: string, i: number): number {
i++;
while (i < code.length) {
if (!this.isNumeric(code.charCodeAt(i))) return i;
i++;
}
return i;
}
protected findEndOfWhiteSpace(code: string, i: number): number {
i++;
while (i < code.length) {
if (!this.isWhiteSpace(code.charCodeAt(i))) return i;
i++;
}
return i;
}
protected getSpan(className: string, innerHtml: string) {
return `${innerHtml}`;
}
/**
* Safely gets the next char code at the specified index + 1. Returns -1 if end of string
* @param text
* @param i Current index
*/
protected nextCharCode(code: string, i: number): number {
return i + 1 < code.length ? code.charCodeAt(i + 1) : -1;
}
/**
* Returns true if character is lowercase, uppercase or underscore.
* @param charCode
*/
protected isAlpha(charCode: number): boolean {
return ((charCode >= 65 && charCode <= 90)
|| (charCode >= 97 && charCode <= 122)
|| charCode === 95);
}
/**
* Checks if code at the current location marks the start of a comment. Returns Comment type if match.
* @param code
* @param i
*/
protected isComment(code: string, i: number): Comment {
for (let comment of this.comments) { // Check each of the comment designators
if (i + comment.start.length >= code.length) continue; // past end of code
if (comment.start === code.substr(i, comment.start.length)) return comment; // we have a comment
}
return null;
}
protected isNumeric(charCode: number): boolean {
return (charCode >= 48 && charCode <= 57);
}
protected isSymbol(charCode: number): boolean {
return (this.symbols.indexOf(charCode) > -1);
}
protected isWhiteSpace(charCode: number): boolean {
let whiteSpace = [9, 10, 13, 32];
return (whiteSpace.indexOf(charCode) > -1);
}
protected isKeyword(identifier: string): boolean {
return (this.keywords.indexOf(identifier) > -1);
}
protected isQuotes(charCode: number): boolean {
let quotes = [34, 39, 96];
return (quotes.indexOf(charCode) > -1);
}
}
export class TypeScript extends LanguageBase {
constructor() {
super();
this.keywords = [
"async", "await", "break", "case", "catch", "class", "const", "continue", "debugger", "default",
"delete", "do", "else", "enum", "export", "extends", "false", "finally", "for", "function", "if",
"import", "in", "instanceof", "namespace", "new", "null", "return", "super", "switch", "this", "throw",
"true", "try", "typeof", "var", "void", "while", "with", "as", "implements", "interface", "let",
"package", "private", "protected", "public", "static", "yield", "any", "boolean", "constructor",
"declare", "get", "module", "require", "number", "set", "string", "symbol", "type", "from", "of"
];
}
}
export class CSharp extends LanguageBase {
constructor() {
super();
this.keywords = [
"abstract", "as", "base", "bool", "break", "byte", "case", "catch", "char", "checked", "class", "const",
"continue", "decimal", "default", "delegate", "do", "double", "else", "enum", "event", "explicit",
"extern", "false", "finally", "fixed", "float", "for", "foreach", "goto", "if", "implicit", "in", "int",
"interface", "internal", "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", "using static", "virtual", "void",
"volatile", "while", "add", "alias", "ascending", "async", "await", "by", "descending", "dynamic",
"equals", "from", "get", "global", "group", "into", "join", "let", "nameof", "on", "orderby", "partial",
"partial", "remove", "select", "set", "unmanaged", "value", "var", "when", "where", "where", "yield"
];
}
}
export class CPlusPlus extends LanguageBase {
constructor() {
super();
this.keywords = [
"asm", "else", "new", "this", "auto", "enum", "operator", "throw", "bool", "explicit", "private",
"true", "break", "export", "protected", "try", "case", "extern", "public", "typedef", "catch", "false",
"register", "typeid", "char", "float", "reinterpret_cast", "typename", "class", "for", "return",
"union", "const", "friend", "short", "unsigned", "const_cast", "goto", "signed", "using", "continue",
"if", "sizeof", "virtual", "default", "inline", "static", "void", "delete", "int", "static_cast",
"volatile", "do", "long", "struct", "wchar_t", "double", "mutable", "switch", "while", "dynamic_cast",
"namespace", "template"
];
}
}
export class JavaScript extends LanguageBase {
constructor() {
super();
this.keywords = [
"abstract", "arguments", "boolean", "break", "byte", "case", "catch", "char", "const", "continue",
"debugger", "default", "delete", "do", "double", "else", "eval", "false", "final", "finally", "float",
"for", "function", "goto", "if", "implements", "in", "instanceof", "int", "interface", "let", "long",
"native", "new", "null", "package", "private", "protected", "public", "return", "short", "static",
"switch", "synchronized", "this", "throw", "throws", "transient", "true", "try", "typeof", "var",
"void", "volatile", "while", "with", "yield"
];
}
}
export class Python extends LanguageBase {
constructor() {
super();
this.keywords = [
"False", "await", "else", "import", "pass", "None", "break", "except", "in", "raise", "True", "class",
"finally", "is", "return", "and", "continue", "for", "lambda", "try", "as", "def", "from", "nonlocal",
"while", "assert", "del", "global", "not", "with", "async", "elif", "if", "or", "yield"
];
this.comments = [new Comment("#", "\n"), new Comment("\"\"\"", "\"\"\"")];
}
}
export class Dark implements IColorScheme {
public getCss(): string {
return `
.code-block .class { color: #37927a; }
.code-block .comment { color: #00bc0c; }
.code-block .enum { color: #b8cf7a; }
.code-block .interface { color: #b8cf7a; }
.code-block .identifier { color: #bcdcdc; }
.code-block .keyword { color: #4e9cd6; }
.code-block .numeric { color: #b2b770; }
.code-block .string { color: #b76ce7; }
.code-block .symbol { color: #bcdcdc; }
.code-block {
white-space: pre;
overflow-y:scroll;
max-height:300px;
padding:5px;
font-family: monospace;
background-color: #1e1e1e;
color: #bcdcdc;
}
`;
}
}
export class Light implements IColorScheme {
public getCss(): string {
return `
.code-block .class { color: #2b91af; }
.code-block .comment { color: #00bc0c; }
.code-block .enum { color: #b8cf7a; }
.code-block .interface { color: #788f3a; }
.code-block .identifier { color: #1f377f; }
.code-block .keyword { color: #0000ff; }
.code-block .numeric { color: #000000; }
.code-block .string { color: #a3151a; }
.code-block .symbol { color: #000000; }
.code-block {
white-space: pre;
overflow-y:scroll;
max-height:300px;
padding:5px;
font-family: monospace;
background-color: #ffffff;
color: #black;
}
`;
}
}
}