Source: highlight.js

/**
* Inner class for the matches that have been made
* @author Mark Hillman <mark@markhillman.info>
*
*   @class
*   @param {number} index - The position that this match was found at in the
*       main text
*   @param {string} classes - A string which will be added to the class variable
*       in the span tag, this should also include the type
*   @param {string} type - This is the match type, e.g keyword, wrapping
*   @param {number} length - This is the length of the match
*   @param {string} match - This is the actual contents of the match
*   @param {number} precedence - This is the precedence level of the match type
*/
var Match = function(index, classes, type, length, match, precedence) {
    this.index = index;
    this.classes = classes;
    this.type = type;
    this.length = length;
    this.match = match;
    this.precedence = precedence;
}

/**
* A class for the Regex Highlighter
* @author Mark Hillman <mark@markhillman.info>
* @class
*
*   @param {string} [returnClassName] - The class name to add to the span tags
*       once a pattern has been matched
*/
var RegexHighlighter = function(returnClassName) {
    this.returnClassName = returnClassName;
    if (typeof this.returnClassName === "undefined")
        this.returnClassName = "regex-highlight";
}

/**
* A function to sort the values of the passed in array, first by their indicies
* then by their precedence.
* @author Mark Hillman <mark@markhillman.info>
*
*   @param {Match[]} array - Array of regex match objects
*/
RegexHighlighter.prototype.sortArrayByObjectsIndex = function(array) {
    array.sort(function compareObj(a, b) {
        if (a.index == b.index)
            return a.precedence - b.precedence;
        else
            return a.index - b.index;
    });
}

/**
* Remove duplicate objects from the array using a function passed into this
* function. The array should contain Regex match objects and the shouldRemove
* function should work in a similar way to a compare function in most
* imperative languages. This means that <0 will remove the item on the left,
* >0 will remove the item on the right. 0 will not remove anything
* @author Mark Hillman <mark@markhillman.info>
*
*   @param {Match[]} array - The array which will have its duplicate items
*       removed from
*   @param {function} shouldRemove - A callback function to decide what will be removed
*/
RegexHighlighter.prototype.removeDuplicateObjectsFromArray = function(array, shouldRemove) {
    for (var i = 1; i < array.length; i++) {
        var side = shouldRemove(array[i-1], array[i]);
        if (side != 0) {
            if (side < 0) {
                array.splice(i-1, 1);
                i--;
            }
            else {
                array.splice(i, 1);
                i--;
            }
        }
    }
}

/**
* Wraps a given piece of text with a span tag that has a class.
* @author Mark Hillman <mark@markhillman.info>
*
*   @param {string} text - The text as a whole
*   @param {string} [classes] - The classes to give the wrapping
*   @param {number} [start] - The start point of the wrapping
*   @param {number} [end] - The end point of the wrapping
*/
RegexHighlighter.prototype.wrapTextWithSpan = function(text, classes, start, end) {
    if (typeof text === "undefined") return false;
    if (typeof classes === "undefined") classes = "";
    if (typeof start === "undefined") start = 0;
    if (typeof end === "undefined") end = text.length;

    // Get the text at different points
    var beginning = text.substring(0, start);
    var middle = text.substring(start, end);
    var ending = text.substring(end);

    // Wrap the match with a span
    return beginning + "<span class='" + classes + "'>" + middle + "</span>" + ending;
}

/**
* This function will return all of the matches that an object which contains
* different regex patterns will match with a string
* @author Mark Hillman <mark@markhillman.info>
*
*   @param {Object} regexObject - This is the object which contains all of
*       the regex information. It can be loaded from a file, see JSON
*       examples for how to create these.
*   @param {string} string - This is the text that needs to be converted to
*       its highlighted form
*/
RegexHighlighter.prototype.getMatchesArrayFromRegex = function(regexObject, string) {
    var matchesArray = [];
    var precedenceCounter = 1;
    for (var index = 0; index < regexObject.length; index++) {
        var matchObject = regexObject[index];
        var type = matchObject["type"];
        var regexes = matchObject["regexes"];
        var precedence;

        // Check if the precedence option has been set in the syntax
        if (matchObject.precedence) {
            if (isNaN(matchObject.precedence)) {
                var found = false;
                for (var i = 0; i < matchesArray.length; i++)
                    if (matchObject.precedence ==  matchesArray[i].type) {
                        precedence = matchesArray[i].precedence;
                        found = true;
                        break;
                    }
                if (!found)
                    precedence = precedenceCounter;
            }
            else
                precedence = parseInt(matchObject.precedence);
            precedenceCounter--;
        }
        else
            precedence = precedenceCounter;

        // loop the individual regex
        for (var i = 0; i < regexes.length; i++) {
            var regexString, captureGroup;
            if (typeof regexes[i] === "string") { // Just a single regex
                regexString = regexes[i];
                captureGroup = 0;
            }
            else { // Regex object provided
                regexString = regexes[i]["regexString"];
                captureGroup = regexes[i]["captureGroup"];
            }
            var matches = this.findRegexMatches(string, regexString, captureGroup,
                type, precedence);
            matchesArray.push.apply(matchesArray, matches);
        }
        precedenceCounter++;
    }
    return matchesArray;
}

/**
* This function finds all of the regex matches in a string from a regex string,
* it will collect only the elements in the specified capture group. An array
* containing Match Objects will be returned.
* @author Mark Hillman <mark@markhillman.info>
*
*   @param {string} string - The string to search
*   @param {string} regexString - The regex string to use
*   @param {number} captureGroup - The capture group to collect
*   @param {string} type - The type class that will given to the Match object
*       so that it can be individually styled
*   @param {number} precedence - The precedence to be given to the Match Object
*/
RegexHighlighter.prototype.findRegexMatches = function(string, regexString, captureGroup, type, precedence) {
    var matchesArray = [];
    var reg = new RegExp(regexString, "gm");
    while (match = reg.exec(string)) {
        var index = match.index;
        if (captureGroup >= match.length)
            captureGroup = 0;

        // Compensate for captureGroup moving the start of match
        var matchText = match[captureGroup];
        var offset = match[0].indexOf(matchText);

        // Save the results into an object array
        matchObject = new Match(index + offset, this.returnClassName + " " + type,
            type, matchText.length, matchText, precedence);
        matchesArray.push(matchObject);
    }
    return matchesArray;
}

/**
* Default duplicate function for the {@link insertSyntaxHighlighting} function
* @author Mark Hillman <mark@markhillman.info>
*
*   @param {Match} a - This is the left regex match
*   @param {Match} b - This is the right regex match
*/
RegexHighlighter.prototype.defaultDuplicateFunction = function(a, b) {
    if (a.index == b.index) {
        if (a.precedence == b.precedence) {
            if (a.length == b.length)
                return -1;
            else
                return a.length - b.length;
        }
        else
            return a.precedence - b.precedence;
    }

    // If b completely contained within a, remove b
    else if (b.index > a.index && (b.index + b.length) < (a.index + a.length))
        return 1;

    // If b starts inside a, but continues past the end of a
    else if (b.index > a.index && b.index < a.index + a.length &&
            (b.index + b.length) >= (a.index + a.length)) {
        if (a.precedence != b.precedence)
            return a.precedence - b.precedence;
        else if (a.length != b.length)
            return a.length - b.length;
        else
            return -1;
    }
    return 0;
}

/**
* This function inserts the span tags into the string and returns a new string
* which can be added to the page or printed to console. This is the main function
* for where all of the sub functions are called. As well as where the main duplicateFunction
* is defined if one is not passed in.
* @author Mark Hillman <mark@markhillman.info>
*
*   @param {Object} regexObject - This is the object which contains all of
*       the regex information. It can be loaded from a file, see JSON examples
*       for how to create these.
*   @param {string} string - This is the text that needs to be converted to
*       its highlighted form.
*   @param {function} [duplicateFunction] - This is the function which will be
*       used to remove any duplicate matches from the highlighting.
*/
RegexHighlighter.prototype.insertSyntaxHighlighting = function(regexObject, string, duplicateFunction) {
    if (typeof duplicateFunction === "undefined") {
        duplicateFunction = this.defaultDuplicateFunction;
    }

    // Finds all of the matches and stores them into an array
    var matchesArray = this.getMatchesArrayFromRegex(regexObject, string);

    // Sort and remove latter matches so its top priority
    this.sortArrayByObjectsIndex(matchesArray);

    // Remove objects which are direct matches and if they are inside a wrapping
    // pattern match
    // < is remove left
    // > is remove right
    // 0 is dont remove
    this.removeDuplicateObjectsFromArray(matchesArray, duplicateFunction);

    // Return the new string with its matches wrapped in span tags
    return this.assembleNewStringFromMatchArray(string, matchesArray);
}

/**
* Loads elements from the document via a className like the
* {@link loadSyntaxHighlightingByClass} but this function will use a
* regexObject directly from code, instead of loading one from a directory.
* If no className is given to load the elements, then it will use the
* default of 'regex-color'.
* @author Mark Hillman <mark@markhillman.info>
*
*   @param {string} regexObject - This is the object that will be used to
*       highlight the text page elements need to be highlighted.
*   @param {string} [className] - This is the className that will be used
*       to identify what elements on the page need to be highlighted
*/
RegexHighlighter.prototype.insertSyntaxHighlightingByClass = function(regexObject, className) {
    if (typeof regexObject === "undefined") {
        return false;
    }
    if (typeof className === "undefined") {
        className = "regex-color";
    }

    // Get the blocks with the correct class
    var codeBlocks = document.getElementsByClassName(className);
    var matchesArray = [];

    // Loop through the codeblocks and wrap the matches
    for (var i = 0; i < codeBlocks.length; i++) {
        var codeBlock = codeBlocks[i];
        var code = codeBlock.innerHTML;
        result = this.insertSyntaxHighlighting(regexObject, code);
        if (result) {
            codeBlock.innerHTML = result;
        }
    }
    return true;
}

/**
* The main function to add regex-highlighting to any element on the page. The
* elements will be loaded by a class supplied to the function, then they will
* have their innerHTML highlighted by inserting span tags with the the default
* class of 'regex-color'. The regex languages will be searched for in the specified
* languagesFolderPath variable, if no path is given, then the default path will be
* './languages/'.
* @author Mark Hillman <mark@markhillman.info>
*
*   @param {string} [className] - The className that will be used to identify which
*       page elements need to be highlighted.
*   @param {string} [languagesFolderPath] - This is the path to the languages
*       folder, so that any languages that can be used, will be found
*/
RegexHighlighter.prototype.loadSyntaxHighlightingByClass = function(className,
        languagesFolderPath) {
    if (typeof languagesFolderPath === "undefined")
        languagesFolderPath = "languages/";
    if (typeof className === "undefined")
        className = "regex-color";
    var elements = document.getElementsByClassName(className);

    // Get the second class as it should be the language name
    for (var i = 0; i < elements.length; i++) {
        var element = elements[i];
        var classes = element.className.split(" ");
        if (classes.length > 1) {
            var language = classes[1];

            // Load the file from highlight folder and insert async
            ajaxGET(languagesFolderPath + language + ".json", function (response, passedElement) {
                var syntax = JSON.parse(response);
                var result = this.insertSyntaxHighlighting(syntax, passedElement.innerHTML);
                if (result) {
                    passedElement.innerHTML = result;
                }
            }.bind(this), element);
        }
    }
};

/**
* Produces a string which contains the new highlighting, by wrapping matches
* in the passed array in a span tag with the corresponding class. In order
* to make sure this functions correctly, it uses an offset and a pre-built
* match array. When actually wrapping a match with a span, the method
* {@link wrapTextWithSpan} is used.
* @author Mark Hillman <mark@markhillman.info>
*
*   @param {string} string - The string which will be highlighted via the
*       match array
*   @param {Match[]} array - The array which contains all the match information
*/
RegexHighlighter.prototype.assembleNewStringFromMatchArray = function(string, array) {
    var offset = 0;
    for (var i = 0; i < array.length; i++) {
        var match = array[i];
        var index = match.index + offset;
        var classes = match.classes;
        var length = match.length;
        string = this.wrapTextWithSpan(string, classes, index, index + length);

        // Update the offset
        offset += ("<span class=''></span>" + classes).length;
    }
    return string;
};

/**
* Retrieves files and data via a HTTP GET call using the XMLHttpRequest
* class. This function has support for a callback function when it is finished
* as well as passing a bundle object back to the callback function when
* everything is complete. This function is asynchronous!
* @author Mark Hillman <mark@markhillman.info>
*
*   @param {string} url - the url of the resource
*   @param {function} [callback] - A callback to be made when the resource has
*       been retrieved.
*   @param {Object} [bundle] - A bundle object to be passed to the callback
*/
function ajaxGET(url, callback, bundle) {
    var xhttp = new XMLHttpRequest();
    xhttp.onreadystatechange = function() {
        if(xhttp.readyState == 4 && xhttp.status == 200) {
            callback(xhttp.responseText, bundle);
        }
    }
    xhttp.open("GET", url, true);
    xhttp.send();
}