| /* | |
| 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 sh_highlightDocument(prefix, suffix) { | |
| var nodeList = document.getElementsByTagName('pre'); | |
| for (var i = 0; i < nodeList.length; i++) { | |
| var element = nodeList.item(i); | |
| var htmlClasses = sh_getClasses(element); | |
| for (var j = 0; j < htmlClasses.length; j++) { | |
| var htmlClass = htmlClasses[j].toLowerCase(); | |
| if (htmlClass === 'sh_sourcecode') { | |
| continue; | |
| } | |
| if (htmlClass.substr(0, 3) === 'sh_') { | |
| var language = htmlClass.substring(3); | |
| if (language in sh_languages) { | |
| sh_highlightElement(element, sh_languages[language]); | |
| } | |
| else if (typeof(prefix) === 'string' && typeof(suffix) === 'string') { | |
| sh_load(language, element, prefix, suffix); | |
| } | |
| else { | |
| throw 'Found <pre> element with class="' + htmlClass + '", but no such language exists'; | |
| } | |
| break; | |
| } | |
| } | |
| } | |
| } |