/* 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 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
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); 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 <' + tag + '> element with class="' + htmlClass + '", but no such language exists'; } break; } } } } function sh_highlightDocument(prefix, suffix) { highlight(prefix, suffix, 'code'); highlight(prefix, suffix, 'pre'); }