| /* |
| 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 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["javascript"]); |
| } |
| } |
| } |
| |
| |
| |
| function sh_highlightDocument(prefix, suffix) { |
| highlight(prefix, suffix, 'tt'); |
| highlight(prefix, suffix, 'code'); |
| highlight(prefix, suffix, 'pre'); |
| } |