2 SHJS - Syntax Highlighting in JavaScript
3 Copyright (C) 2007, 2008 gnombat@users.sourceforge.net
4 License: http://shjs.sourceforge.net/doc/gplv3.html
7 if (! this.sh_languages) {
8 this.sh_languages = {};
12 function sh_isEmailAddress(url) {
13 if (/^mailto:/.test(url)) {
16 return url.indexOf('@') !== -1;
19 function sh_setHref(tags, numTags, inputString) {
20 var url = inputString.substring(tags[numTags - 2].pos, tags[numTags - 1].pos);
21 if (url.length >= 2 && url.charAt(0) === '<' && url.charAt(url.length - 1) === '>') {
22 url = url.substr(1, url.length - 2);
24 if (sh_isEmailAddress(url)) {
25 url = 'mailto:' + url;
27 tags[numTags - 2].node.href = url;
31 Konqueror has a bug where the regular expression /$/g will not match at the end
32 of a line more than once:
37 var line = '1234567890';
39 match = regex.exec(line);
43 match = regex.exec(line2); // fails
45 function sh_konquerorExec(s) {
47 result.index = s.length;
53 Highlights all elements containing source code in a text string. The return
54 value is an array of objects, each representing an HTML start or end tag. Each
55 object has a property named pos, which is an integer representing the text
56 offset of the tag. Every start tag also has a property named node, which is the
57 DOM element started by the tag. End tags do not have this property.
58 @param inputString a text string
59 @param language a language definition object
60 @return an array of tag objects
62 function sh_highlightString(inputString, language) {
63 if (/Konqueror/.test(navigator.userAgent)) {
64 if (! language.konquered) {
65 for (var s = 0; s < language.length; s++) {
66 for (var p = 0; p < language[s].length; p++) {
67 var r = language[s][p][0];
68 if (r.source === '$') {
69 r.exec = sh_konquerorExec;
73 language.konquered = true;
77 var a = document.createElement('a');
78 var span = document.createElement('span');
84 // each element is a pattern object from language
85 var patternStack = [];
87 // the current position within inputString
90 // the name of the current style, or null if there is no current style
91 var currentStyle = null;
93 var output = function(s, style) {
94 var length = s.length;
95 // this is more than just an optimization - we don't want to output empty <span></span> elements
100 var stackLength = patternStack.length;
101 if (stackLength !== 0) {
102 var pattern = patternStack[stackLength - 1];
103 // check whether this is a state or an environment
105 // it's not a state - it's an environment; use the style for this environment
110 if (currentStyle !== style) {
112 tags[numTags++] = {pos: pos};
113 if (currentStyle === 'sh_url') {
114 sh_setHref(tags, numTags, inputString);
119 if (style === 'sh_url') {
120 clone = a.cloneNode(false);
123 clone = span.cloneNode(false);
125 clone.className = style;
126 tags[numTags++] = {node: clone, pos: pos};
130 currentStyle = style;
133 var endOfLinePattern = /\r\n|\r|\n/g;
134 endOfLinePattern.lastIndex = 0;
135 var inputStringLength = inputString.length;
136 while (pos < inputStringLength) {
140 var endOfLineMatch = endOfLinePattern.exec(inputString);
141 if (endOfLineMatch === null) {
142 end = inputStringLength;
143 startOfNextLine = inputStringLength;
146 end = endOfLineMatch.index;
147 startOfNextLine = endOfLinePattern.lastIndex;
150 var line = inputString.substring(start, end);
154 var posWithinLine = pos - start;
157 var stackLength = patternStack.length;
158 if (stackLength === 0) {
162 // get the next state
163 stateIndex = patternStack[stackLength - 1][2];
166 var state = language[stateIndex];
167 var numPatterns = state.length;
168 var mc = matchCache[stateIndex];
170 mc = matchCache[stateIndex] = [];
172 var bestMatch = null;
173 var bestPatternIndex = -1;
174 for (var i = 0; i < numPatterns; i++) {
176 if (i < mc.length && (mc[i] === null || posWithinLine <= mc[i].index)) {
180 var regex = state[i][0];
181 regex.lastIndex = posWithinLine;
182 match = regex.exec(line);
185 if (match !== null && (bestMatch === null || match.index < bestMatch.index)) {
187 bestPatternIndex = i;
188 if (match.index === posWithinLine) {
194 if (bestMatch === null) {
195 output(line.substring(posWithinLine), null);
200 if (bestMatch.index > posWithinLine) {
201 output(line.substring(posWithinLine, bestMatch.index), null);
204 var pattern = state[bestPatternIndex];
206 var newStyle = pattern[1];
208 if (newStyle instanceof Array) {
209 for (var subexpression = 0; subexpression < newStyle.length; subexpression++) {
210 matchedString = bestMatch[subexpression + 1];
211 output(matchedString, newStyle[subexpression]);
215 matchedString = bestMatch[0];
216 output(matchedString, newStyle);
219 switch (pattern[2]) {
229 patternStack.length = 0;
232 // this was the start of a delimited pattern or a state/environment
233 patternStack.push(pattern);
241 tags[numTags++] = {pos: pos};
242 if (currentStyle === 'sh_url') {
243 sh_setHref(tags, numTags, inputString);
247 pos = startOfNextLine;
253 ////////////////////////////////////////////////////////////////////////////////
254 // DOM-dependent functions
256 function sh_getClasses(element) {
258 var htmlClass = element.className;
259 if (htmlClass && htmlClass.length > 0) {
260 var htmlClasses = htmlClass.split(' ');
261 for (var i = 0; i < htmlClasses.length; i++) {
262 if (htmlClasses[i].length > 0) {
263 result.push(htmlClasses[i]);
270 function sh_addClass(element, name) {
271 var htmlClasses = sh_getClasses(element);
272 for (var i = 0; i < htmlClasses.length; i++) {
273 if (name.toLowerCase() === htmlClasses[i].toLowerCase()) {
277 htmlClasses.push(name);
278 element.className = htmlClasses.join(' ');
282 Extracts the tags from an HTML DOM NodeList.
283 @param nodeList a DOM NodeList
284 @param result an object with text, tags and pos properties
286 function sh_extractTagsFromNodeList(nodeList, result) {
287 var length = nodeList.length;
288 for (var i = 0; i < length; i++) {
289 var node = nodeList.item(i);
290 switch (node.nodeType) {
292 if (node.nodeName.toLowerCase() === 'br') {
294 if (/MSIE/.test(navigator.userAgent)) {
300 result.text.push(terminator);
304 result.tags.push({node: node.cloneNode(false), pos: result.pos});
305 sh_extractTagsFromNodeList(node.childNodes, result);
306 result.tags.push({pos: result.pos});
311 result.text.push(node.data);
312 result.pos += node.length;
319 Extracts the tags from the text of an HTML element. The extracted tags will be
320 returned as an array of tag objects. See sh_highlightString for the format of
322 @param element a DOM element
323 @param tags an empty array; the extracted tag objects will be returned in it
324 @return the text of the element
325 @see sh_highlightString
327 function sh_extractTags(element, tags) {
332 sh_extractTagsFromNodeList(element.childNodes, result);
333 return result.text.join('');
337 Merges the original tags from an element with the tags produced by highlighting.
338 @param originalTags an array containing the original tags
339 @param highlightTags an array containing the highlighting tags - these must not overlap
340 @result an array containing the merged tags
342 function sh_mergeTags(originalTags, highlightTags) {
343 var numOriginalTags = originalTags.length;
344 if (numOriginalTags === 0) {
345 return highlightTags;
348 var numHighlightTags = highlightTags.length;
349 if (numHighlightTags === 0) {
354 var originalIndex = 0;
355 var highlightIndex = 0;
357 while (originalIndex < numOriginalTags && highlightIndex < numHighlightTags) {
358 var originalTag = originalTags[originalIndex];
359 var highlightTag = highlightTags[highlightIndex];
361 if (originalTag.pos <= highlightTag.pos) {
362 result.push(originalTag);
366 result.push(highlightTag);
367 if (highlightTags[highlightIndex + 1].pos <= originalTag.pos) {
369 result.push(highlightTags[highlightIndex]);
374 result.push({pos: originalTag.pos});
377 highlightTags[highlightIndex] = {node: highlightTag.node.cloneNode(false), pos: originalTag.pos};
382 while (originalIndex < numOriginalTags) {
383 result.push(originalTags[originalIndex]);
387 while (highlightIndex < numHighlightTags) {
388 result.push(highlightTags[highlightIndex]);
396 Inserts tags into text.
397 @param tags an array of tag objects
398 @param text a string representing the text
399 @return a DOM DocumentFragment representing the resulting HTML
401 function sh_insertTags(tags, text) {
404 var result = document.createDocumentFragment();
406 var numTags = tags.length;
408 var textLength = text.length;
409 var currentNode = result;
411 // output one tag or text node every iteration
412 while (textPos < textLength || tagIndex < numTags) {
415 if (tagIndex < numTags) {
416 tag = tags[tagIndex];
423 if (tagPos <= textPos) {
427 var newNode = tag.node;
428 currentNode.appendChild(newNode);
429 currentNode = newNode;
433 currentNode = currentNode.parentNode;
439 currentNode.appendChild(doc.createTextNode(text.substring(textPos, tagPos)));
448 Highlights an element containing source code. Upon completion of this function,
449 the element will have been placed in the "sh_sourceCode" class.
450 @param element a DOM <pre> element containing the source code to be highlighted
451 @param language a language definition object
453 function sh_highlightElement(element, language) {
454 sh_addClass(element, 'sh_sourceCode');
455 var originalTags = [];
456 var inputString = sh_extractTags(element, originalTags);
457 var highlightTags = sh_highlightString(inputString, language);
458 var tags = sh_mergeTags(originalTags, highlightTags);
459 var documentFragment = sh_insertTags(tags, inputString);
460 while (element.hasChildNodes()) {
461 element.removeChild(element.firstChild);
463 element.appendChild(documentFragment);
466 function sh_getXMLHttpRequest() {
467 if (window.ActiveXObject) {
468 return new ActiveXObject('Msxml2.XMLHTTP');
470 else if (window.XMLHttpRequest) {
471 return new XMLHttpRequest();
473 throw 'No XMLHttpRequest implementation available';
476 function sh_load(language, element, prefix, suffix) {
477 if (language in sh_requests) {
478 sh_requests[language].push(element);
481 sh_requests[language] = [element];
482 var request = sh_getXMLHttpRequest();
483 var url = prefix + 'sh_' + language + suffix;
484 request.open('GET', url, true);
485 request.onreadystatechange = function () {
486 if (request.readyState === 4) {
488 if (! request.status || request.status === 200) {
489 eval(request.responseText);
490 var elements = sh_requests[language];
491 for (var i = 0; i < elements.length; i++) {
492 sh_highlightElement(elements[i], sh_languages[language]);
496 throw 'HTTP error: status ' + request.status;
508 Highlights all elements containing source code on the current page. Elements
509 containing source code must be "pre" elements with a "class" attribute of
510 "sh_LANGUAGE", where LANGUAGE is a valid language identifier; e.g., "sh_java"
511 identifies the element as containing "java" language source code.
513 function sh_highlightDocument(prefix, suffix) {
514 var nodeList = document.getElementsByTagName('pre');
515 for (var i = 0; i < nodeList.length; i++) {
516 var element = nodeList.item(i);
517 var htmlClasses = sh_getClasses(element);
518 for (var j = 0; j < htmlClasses.length; j++) {
519 var htmlClass = htmlClasses[j].toLowerCase();
520 if (htmlClass === 'sh_sourcecode') {
523 if (htmlClass.substr(0, 3) === 'sh_') {
524 var language = htmlClass.substring(3);
525 if (language in sh_languages) {
526 sh_highlightElement(element, sh_languages[language]);
528 else if (typeof(prefix) === 'string' && typeof(suffix) === 'string') {
529 sh_load(language, element, prefix, suffix);
532 throw 'Found <pre> element with class="' + htmlClass + '", but no such language exists';