/*
SHJS - Syntax Highlighting in JavaScript
Copyright (C) 2007, 2008 gnombat@users.sourceforge.net
License: http://shjs.sourceforge.net/doc/gplv3.html
*/

if (!this.sh_languages) {
    this.sh_languages = {};
}
var sh_requests = {};

function sh_isEmailAddress(url) {
    if (/^mailto:/.test(url)) {
        return false;
    }
    return url.indexOf('@') !== -1;
}

function sh_setHref(tags, numTags, inputString) {
    var url = inputString.substring(tags[numTags - 2].pos, tags[numTags - 1].pos);
    if (url.length >= 2 && url.charAt(0) === '<' && url.charAt(url.length - 1) === '>') {
        url = url.substr(1, url.length - 2);
    }
    if (sh_isEmailAddress(url)) {
        url = 'mailto:' + url;
    }
    tags[numTags - 2].node.href = url;
}

/*
Konqueror has a bug where the regular expression /$/g will not match at the end
of a line more than once:

  var regex = /$/g;
  var match;

  var line = '1234567890';
  regex.lastIndex = 10;
  match = regex.exec(line);

  var line2 = 'abcde';
  regex.lastIndex = 5;
  match = regex.exec(line2);  // fails
*/
function sh_konquerorExec(s) {
    var result = [ '' ];
    result.index = s.length;
    result.input = s;
    return result;
}

/**
 Highlights all elements containing source code in a text string.  The return
 value is an array of objects, each representing an HTML start or end tag.  Each
 object has a property named pos, which is an integer representing the text
 offset of the tag. Every start tag also has a property named node, which is the
 DOM element started by the tag. End tags do not have this property.
 @param  inputString  a text string
 @param  language  a language definition object
 @return  an array of tag objects
 */
function sh_highlightString(inputString, language) {
    if (/Konqueror/.test(navigator.userAgent)) {
        if (!language.konquered) {
            for (var s = 0; s < language.length; s++) {
                for (var p = 0; p < language[s].length; p++) {
                    var r = language[s][p][0];
                    if (r.source === '$') {
                        r.exec = sh_konquerorExec;
                    }
                }
            }
            language.konquered = true;
        }
    }

    var a = document.createElement('a');
    var span = document.createElement('span');

    // the result
    var tags = [];
    var numTags = 0;

    // each element is a pattern object from language
    var patternStack = [];

    // the current position within inputString
    var pos = 0;

    // the name of the current style, or null if there is no current style
    var currentStyle = null;

    var output = function (s, style) {
        var length = s.length;
        // this is more than just an optimization - we don't want to output empty <span></span> elements
        if (length === 0) {
            return;
        }
        if (!style) {
            var stackLength = patternStack.length;
            if (stackLength !== 0) {
                var pattern = patternStack[stackLength - 1];
                // check whether this is a state or an environment
                if (!pattern[3]) {
                    // it's not a state - it's an environment; use the style for this environment
                    style = pattern[1];
                }
            }
        }
        if (currentStyle !== style) {
            if (currentStyle) {
                tags[numTags++] = { pos: pos };
                if (currentStyle === 'sh_url') {
                    sh_setHref(tags, numTags, inputString);
                }
            }
            if (style) {
                var clone;
                if (style === 'sh_url') {
                    clone = a.cloneNode(false);
                } else {
                    clone = span.cloneNode(false);
                }
                clone.className = style;
                tags[numTags++] = { node: clone, pos: pos };
            }
        }
        pos += length;
        currentStyle = style;
    };

    var endOfLinePattern = /\r\n|\r|\n/g;
    endOfLinePattern.lastIndex = 0;
    var inputStringLength = inputString.length;
    while (pos < inputStringLength) {
        var start = pos;
        var end;
        var startOfNextLine;
        var endOfLineMatch = endOfLinePattern.exec(inputString);
        if (endOfLineMatch === null) {
            end = inputStringLength;
            startOfNextLine = inputStringLength;
        } else {
            end = endOfLineMatch.index;
            startOfNextLine = endOfLinePattern.lastIndex;
        }

        var line = inputString.substring(start, end);

        var matchCache = [];
        for (; ;) {
            var posWithinLine = pos - start;

            var stateIndex;
            var stackLength = patternStack.length;
            if (stackLength === 0) {
                stateIndex = 0;
            } else {
                // get the next state
                stateIndex = patternStack[stackLength - 1][2];
            }

            var state = language[stateIndex];
            var numPatterns = state.length;
            var mc = matchCache[stateIndex];
            if (!mc) {
                mc = matchCache[stateIndex] = [];
            }
            var bestMatch = null;
            var bestPatternIndex = -1;
            for (var i = 0; i < numPatterns; i++) {
                var match;
                if (i < mc.length && (mc[i] === null || posWithinLine <= mc[i].index)) {
                    match = mc[i];
                } else {
                    var regex = state[i][0];
                    regex.lastIndex = posWithinLine;
                    match = regex.exec(line);
                    mc[i] = match;
                }
                if (match !== null && (bestMatch === null || match.index < bestMatch.index)) {
                    bestMatch = match;
                    bestPatternIndex = i;
                    if (match.index === posWithinLine) {
                        break;
                    }
                }
            }

            if (bestMatch === null) {
                output(line.substring(posWithinLine), null);
                break;
            } else {
                // got a match
                if (bestMatch.index > posWithinLine) {
                    output(line.substring(posWithinLine, bestMatch.index), null);
                }

                var pattern = state[bestPatternIndex];

                var newStyle = pattern[1];
                var matchedString;
                if (newStyle instanceof Array) {
                    for (var subexpression = 0; subexpression < newStyle.length; subexpression++) {
                        matchedString = bestMatch[subexpression + 1];
                        output(matchedString, newStyle[subexpression]);
                    }
                } else {
                    matchedString = bestMatch[0];
                    output(matchedString, newStyle);
                }

                switch (pattern[2]) {
                    case -1:
                        // do nothing
                        break;
                    case -2:
                        // exit
                        patternStack.pop();
                        break;
                    case -3:
                        // exitall
                        patternStack.length = 0;
                        break;
                    default:
                        // this was the start of a delimited pattern or a state/environment
                        patternStack.push(pattern);
                        break;
                }
            }
        }

        // end of the line
        if (currentStyle) {
            tags[numTags++] = { pos: pos };
            if (currentStyle === 'sh_url') {
                sh_setHref(tags, numTags, inputString);
            }
            currentStyle = null;
        }
        pos = startOfNextLine;
    }

    return tags;
}

////////////////////////////////////////////////////////////////////////////////
// DOM-dependent functions

function sh_getClasses(element) {
    var result = [];
    var htmlClass = element.className;
    if (htmlClass && htmlClass.length > 0) {
        var htmlClasses = htmlClass.split(' ');
        for (var i = 0; i < htmlClasses.length; i++) {
            if (htmlClasses[i].length > 0) {
                result.push(htmlClasses[i]);
            }
        }
    }
    return result;
}

function sh_addClass(element, name) {
    var htmlClasses = sh_getClasses(element);
    for (var i = 0; i < htmlClasses.length; i++) {
        if (name.toLowerCase() === htmlClasses[i].toLowerCase()) {
            return;
        }
    }
    htmlClasses.push(name);
    element.className = htmlClasses.join(' ');
}

/**
 Extracts the tags from an HTML DOM NodeList.
 @param  nodeList  a DOM NodeList
 @param  result  an object with text, tags and pos properties
 */
function sh_extractTagsFromNodeList(nodeList, result) {
    var length = nodeList.length;
    for (var i = 0; i < length; i++) {
        var node = nodeList.item(i);
        switch (node.nodeType) {
            case 1:
                if (node.nodeName.toLowerCase() === 'br') {
                    var terminator;
                    if (/MSIE/.test(navigator.userAgent)) {
                        terminator = '\r';
                    } else {
                        terminator = '\n';
                    }
                    result.text.push(terminator);
                    result.pos++;
                } else {
                    result.tags.push({ node: node.cloneNode(false), pos: result.pos });
                    sh_extractTagsFromNodeList(node.childNodes, result);
                    result.tags.push({ pos: result.pos });
                }
                break;
            case 3:
            case 4:
                result.text.push(node.data);
                result.pos += node.length;
                break;
        }
    }
}

/**
 Extracts the tags from the text of an HTML element. The extracted tags will be
 returned as an array of tag objects. See sh_highlightString for the format of
 the tag objects.
 @param  element  a DOM element
 @param  tags  an empty array; the extracted tag objects will be returned in it
 @return  the text of the element
 @see  sh_highlightString
 */
function sh_extractTags(element, tags) {
    var result = {};
    result.text = [];
    result.tags = tags;
    result.pos = 0;
    sh_extractTagsFromNodeList(element.childNodes, result);
    return result.text.join('');
}

/**
 Merges the original tags from an element with the tags produced by highlighting.
 @param  originalTags  an array containing the original tags
 @param  highlightTags  an array containing the highlighting tags - these must not overlap
 @result  an array containing the merged tags
 */
function sh_mergeTags(originalTags, highlightTags) {
    var numOriginalTags = originalTags.length;
    if (numOriginalTags === 0) {
        return highlightTags;
    }

    var numHighlightTags = highlightTags.length;
    if (numHighlightTags === 0) {
        return originalTags;
    }

    var result = [];
    var originalIndex = 0;
    var highlightIndex = 0;

    while (originalIndex < numOriginalTags && highlightIndex < numHighlightTags) {
        var originalTag = originalTags[originalIndex];
        var highlightTag = highlightTags[highlightIndex];

        if (originalTag.pos <= highlightTag.pos) {
            result.push(originalTag);
            originalIndex++;
        } else {
            result.push(highlightTag);
            if (highlightTags[highlightIndex + 1].pos <= originalTag.pos) {
                highlightIndex++;
                result.push(highlightTags[highlightIndex]);
                highlightIndex++;
            } else {
                // new end tag
                result.push({ pos: originalTag.pos });

                // new start tag
                highlightTags[highlightIndex] = { node: highlightTag.node.cloneNode(false), pos: originalTag.pos };
            }
        }
    }

    while (originalIndex < numOriginalTags) {
        result.push(originalTags[originalIndex]);
        originalIndex++;
    }

    while (highlightIndex < numHighlightTags) {
        result.push(highlightTags[highlightIndex]);
        highlightIndex++;
    }

    return result;
}

/**
 Inserts tags into text.
 @param  tags  an array of tag objects
 @param  text  a string representing the text
 @return  a DOM DocumentFragment representing the resulting HTML
 */
function sh_insertTags(tags, text) {
    var doc = document;

    var result = document.createDocumentFragment();
    var tagIndex = 0;
    var numTags = tags.length;
    var textPos = 0;
    var textLength = text.length;
    var currentNode = result;

    // output one tag or text node every iteration
    while (textPos < textLength || tagIndex < numTags) {
        var tag;
        var tagPos;
        if (tagIndex < numTags) {
            tag = tags[tagIndex];
            tagPos = tag.pos;
        } else {
            tagPos = textLength;
        }

        if (tagPos <= textPos) {
            // output the tag
            if (tag.node) {
                // start tag
                var newNode = tag.node;
                currentNode.appendChild(newNode);
                currentNode = newNode;
            } else {
                // end tag
                currentNode = currentNode.parentNode;
            }
            tagIndex++;
        } else {
            // output text
            currentNode.appendChild(doc.createTextNode(text.substring(textPos, tagPos)));
            textPos = tagPos;
        }
    }

    return result;
}

/**
 Highlights an element containing source code.  Upon completion of this function,
 the element will have been placed in the "sh_sourceCode" class.
 @param  element  a DOM <pre> element containing the source code to be highlighted
 @param  language  a language definition object
 */
function sh_highlightElement(element, language) {
    sh_addClass(element, 'sh_sourceCode');
    var originalTags = [];
    var inputString = sh_extractTags(element, originalTags);
    var highlightTags = sh_highlightString(inputString, language);
    var tags = sh_mergeTags(originalTags, highlightTags);
    var documentFragment = sh_insertTags(tags, inputString);
    while (element.hasChildNodes()) {
        element.removeChild(element.firstChild);
    }
    element.appendChild(documentFragment);
}

function sh_getXMLHttpRequest() {
    if (window.ActiveXObject) {
        return new ActiveXObject('Msxml2.XMLHTTP');
    } else if (window.XMLHttpRequest) {
        return new XMLHttpRequest();
    }
    throw 'No XMLHttpRequest implementation available';
}

function sh_load(language, element, prefix, suffix) {
    if (language in sh_requests) {
        sh_requests[language].push(element);
        return;
    }
    sh_requests[language] = [ element ];
    var request = sh_getXMLHttpRequest();
    var url = prefix + 'sh_' + language + suffix;
    request.open('GET', url, true);
    request.onreadystatechange = function () {
        if (request.readyState === 4) {
            try {
                if (!request.status || request.status === 200) {
                    eval(request.responseText);
                    var elements = sh_requests[language];
                    for (var i = 0; i < elements.length; i++) {
                        sh_highlightElement(elements[i], sh_languages[language]);
                    }
                } else {
                    throw 'HTTP error: status ' + request.status;
                }
            } finally {
                request = null;
            }
        }
    };
    request.send(null);
}

/**
 Highlights all elements containing source code on the current page. Elements
 containing source code must be "pre" elements with a "class" attribute of
 "sh_LANGUAGE", where LANGUAGE is a valid language identifier; e.g., "sh_java"
 identifies the element as containing "java" language source code.
 */
function highlight(prefix, suffix, tag) {
    var nodeList = document.getElementsByTagName(tag);
    for (var i = 0; i < nodeList.length; i++) {
        var element = nodeList.item(i);

        var wrapper = document.createElement('div');
        wrapper.classList.add('codeWrapper');
        //
        element.parentNode.insertBefore(wrapper, element);
        wrapper.appendChild(element);

        var htmlClasses = sh_getClasses(element);
        var highlighted = false;
        var donthighlight = false;
        for (var j = 0; j < htmlClasses.length; j++) {
            var htmlClass = htmlClasses[j].toLowerCase();
            if (htmlClass === 'sh_none') {
                donthighlight = true;
                continue;
            }
            if (htmlClass.substr(0, 3) === 'sh_') {
                var language = htmlClass.substring(3);
                if (language in sh_languages) {
                    sh_highlightElement(element, sh_languages[language]);
                    highlighted = true;
                } else if (typeof (prefix) === 'string' && typeof (suffix) === 'string') {
                    sh_load(language, element, prefix, suffix);
                } else {
                    throw 'Found <' + tag + '> element with class="' + htmlClass + '", but no such language exists';
                }
                break;
            }
        }
        if (highlighted === false && donthighlight == false) {
            sh_highlightElement(element, sh_languages['java']);
            sh_highlightElement(element, sh_languages['javascript']);
            sh_highlightElement(element, sh_languages['badjs']);
        }
    }
}


function sh_highlightDocument(prefix, suffix) {
    highlight(prefix, suffix, 'tt');
    highlight(prefix, suffix, 'code');
    highlight(prefix, suffix, 'pre');
}
