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