Added discussion feature to site.
[Assignment-Trapper.git] / sh_main.js
blobc83fd1b0400549ee637735c814768f5ff4f45c1c
1 /*
2 SHJS - Syntax Highlighting in JavaScript
3 Copyright (C) 2007, 2008 gnombat@users.sourceforge.net
4 License: http://shjs.sourceforge.net/doc/gplv3.html
5 */
7 if (! this.sh_languages) {
8   this.sh_languages = {};
10 var sh_requests = {};
12 function sh_isEmailAddress(url) {
13   if (/^mailto:/.test(url)) {
14     return false;
15   }
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);
23   }
24   if (sh_isEmailAddress(url)) {
25     url = 'mailto:' + url;
26   }
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:
34   var regex = /$/g;
35   var match;
37   var line = '1234567890';
38   regex.lastIndex = 10;
39   match = regex.exec(line);
41   var line2 = 'abcde';
42   regex.lastIndex = 5;
43   match = regex.exec(line2);  // fails
45 function sh_konquerorExec(s) {
46   var result = [''];
47   result.index = s.length;
48   result.input = s;
49   return result;
52 /**
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;
70           }
71         }
72       }
73       language.konquered = true;
74     }
75   }
77   var a = document.createElement('a');
78   var span = document.createElement('span');
80   // the result
81   var tags = [];
82   var numTags = 0;
84   // each element is a pattern object from language
85   var patternStack = [];
87   // the current position within inputString
88   var pos = 0;
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
96     if (length === 0) {
97       return;
98     }
99     if (! style) {
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
104         if (! pattern[3]) {
105           // it's not a state - it's an environment; use the style for this environment
106           style = pattern[1];
107         }
108       }
109     }
110     if (currentStyle !== style) {
111       if (currentStyle) {
112         tags[numTags++] = {pos: pos};
113         if (currentStyle === 'sh_url') {
114           sh_setHref(tags, numTags, inputString);
115         }
116       }
117       if (style) {
118         var clone;
119         if (style === 'sh_url') {
120           clone = a.cloneNode(false);
121         }
122         else {
123           clone = span.cloneNode(false);
124         }
125         clone.className = style;
126         tags[numTags++] = {node: clone, pos: pos};
127       }
128     }
129     pos += length;
130     currentStyle = style;
131   };
133   var endOfLinePattern = /\r\n|\r|\n/g;
134   endOfLinePattern.lastIndex = 0;
135   var inputStringLength = inputString.length;
136   while (pos < inputStringLength) {
137     var start = pos;
138     var end;
139     var startOfNextLine;
140     var endOfLineMatch = endOfLinePattern.exec(inputString);
141     if (endOfLineMatch === null) {
142       end = inputStringLength;
143       startOfNextLine = inputStringLength;
144     }
145     else {
146       end = endOfLineMatch.index;
147       startOfNextLine = endOfLinePattern.lastIndex;
148     }
150     var line = inputString.substring(start, end);
152     var matchCache = [];
153     for (;;) {
154       var posWithinLine = pos - start;
156       var stateIndex;
157       var stackLength = patternStack.length;
158       if (stackLength === 0) {
159         stateIndex = 0;
160       }
161       else {
162         // get the next state
163         stateIndex = patternStack[stackLength - 1][2];
164       }
166       var state = language[stateIndex];
167       var numPatterns = state.length;
168       var mc = matchCache[stateIndex];
169       if (! mc) {
170         mc = matchCache[stateIndex] = [];
171       }
172       var bestMatch = null;
173       var bestPatternIndex = -1;
174       for (var i = 0; i < numPatterns; i++) {
175         var match;
176         if (i < mc.length && (mc[i] === null || posWithinLine <= mc[i].index)) {
177           match = mc[i];
178         }
179         else {
180           var regex = state[i][0];
181           regex.lastIndex = posWithinLine;
182           match = regex.exec(line);
183           mc[i] = match;
184         }
185         if (match !== null && (bestMatch === null || match.index < bestMatch.index)) {
186           bestMatch = match;
187           bestPatternIndex = i;
188           if (match.index === posWithinLine) {
189             break;
190           }
191         }
192       }
194       if (bestMatch === null) {
195         output(line.substring(posWithinLine), null);
196         break;
197       }
198       else {
199         // got a match
200         if (bestMatch.index > posWithinLine) {
201           output(line.substring(posWithinLine, bestMatch.index), null);
202         }
204         var pattern = state[bestPatternIndex];
206         var newStyle = pattern[1];
207         var matchedString;
208         if (newStyle instanceof Array) {
209           for (var subexpression = 0; subexpression < newStyle.length; subexpression++) {
210             matchedString = bestMatch[subexpression + 1];
211             output(matchedString, newStyle[subexpression]);
212           }
213         }
214         else {
215           matchedString = bestMatch[0];
216           output(matchedString, newStyle);
217         }
219         switch (pattern[2]) {
220         case -1:
221           // do nothing
222           break;
223         case -2:
224           // exit
225           patternStack.pop();
226           break;
227         case -3:
228           // exitall
229           patternStack.length = 0;
230           break;
231         default:
232           // this was the start of a delimited pattern or a state/environment
233           patternStack.push(pattern);
234           break;
235         }
236       }
237     }
239     // end of the line
240     if (currentStyle) {
241       tags[numTags++] = {pos: pos};
242       if (currentStyle === 'sh_url') {
243         sh_setHref(tags, numTags, inputString);
244       }
245       currentStyle = null;
246     }
247     pos = startOfNextLine;
248   }
250   return tags;
253 ////////////////////////////////////////////////////////////////////////////////
254 // DOM-dependent functions
256 function sh_getClasses(element) {
257   var result = [];
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]);
264       }
265     }
266   }
267   return result;
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()) {
274       return;
275     }
276   }
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) {
291     case 1:
292       if (node.nodeName.toLowerCase() === 'br') {
293         var terminator;
294         if (/MSIE/.test(navigator.userAgent)) {
295           terminator = '\r';
296         }
297         else {
298           terminator = '\n';
299         }
300         result.text.push(terminator);
301         result.pos++;
302       }
303       else {
304         result.tags.push({node: node.cloneNode(false), pos: result.pos});
305         sh_extractTagsFromNodeList(node.childNodes, result);
306         result.tags.push({pos: result.pos});
307       }
308       break;
309     case 3:
310     case 4:
311       result.text.push(node.data);
312       result.pos += node.length;
313       break;
314     }
315   }
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
321 the tag objects.
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) {
328   var result = {};
329   result.text = [];
330   result.tags = tags;
331   result.pos = 0;
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;
346   }
348   var numHighlightTags = highlightTags.length;
349   if (numHighlightTags === 0) {
350     return originalTags;
351   }
353   var result = [];
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);
363       originalIndex++;
364     }
365     else {
366       result.push(highlightTag);
367       if (highlightTags[highlightIndex + 1].pos <= originalTag.pos) {
368         highlightIndex++;
369         result.push(highlightTags[highlightIndex]);
370         highlightIndex++;
371       }
372       else {
373         // new end tag
374         result.push({pos: originalTag.pos});
376         // new start tag
377         highlightTags[highlightIndex] = {node: highlightTag.node.cloneNode(false), pos: originalTag.pos};
378       }
379     }
380   }
382   while (originalIndex < numOriginalTags) {
383     result.push(originalTags[originalIndex]);
384     originalIndex++;
385   }
387   while (highlightIndex < numHighlightTags) {
388     result.push(highlightTags[highlightIndex]);
389     highlightIndex++;
390   }
392   return result;
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) {
402   var doc = document;
404   var result = document.createDocumentFragment();
405   var tagIndex = 0;
406   var numTags = tags.length;
407   var textPos = 0;
408   var textLength = text.length;
409   var currentNode = result;
411   // output one tag or text node every iteration
412   while (textPos < textLength || tagIndex < numTags) {
413     var tag;
414     var tagPos;
415     if (tagIndex < numTags) {
416       tag = tags[tagIndex];
417       tagPos = tag.pos;
418     }
419     else {
420       tagPos = textLength;
421     }
423     if (tagPos <= textPos) {
424       // output the tag
425       if (tag.node) {
426         // start tag
427         var newNode = tag.node;
428         currentNode.appendChild(newNode);
429         currentNode = newNode;
430       }
431       else {
432         // end tag
433         currentNode = currentNode.parentNode;
434       }
435       tagIndex++;
436     }
437     else {
438       // output text
439       currentNode.appendChild(doc.createTextNode(text.substring(textPos, tagPos)));
440       textPos = tagPos;
441     }
442   }
444   return result;
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);
462   }
463   element.appendChild(documentFragment);
466 function sh_getXMLHttpRequest() {
467   if (window.ActiveXObject) {
468     return new ActiveXObject('Msxml2.XMLHTTP');
469   }
470   else if (window.XMLHttpRequest) {
471     return new XMLHttpRequest();
472   }
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);
479     return;
480   }
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) {
487       try {
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]);
493           }
494         }
495         else {
496           throw 'HTTP error: status ' + request.status;
497         }
498       }
499       finally {
500         request = null;
501       }
502     }
503   };
504   request.send(null);
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') {
521         continue;
522       }
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]);
527         }
528         else if (typeof(prefix) === 'string' && typeof(suffix) === 'string') {
529           sh_load(language, element, prefix, suffix);
530         }
531         else {
532           throw 'Found <pre> element with class="' + htmlClass + '", but no such language exists';
533         }
534         break;
535       }
536     }
537   }