UNPKG

15.3 kBJavaScriptView Raw
1/*
2SHJS - Syntax Highlighting in JavaScript
3Copyright (C) 2007, 2008 gnombat@users.sourceforge.net
4License: http://shjs.sourceforge.net/doc/gplv3.html
5*/
6
7if (! this.sh_languages) {
8 this.sh_languages = {};
9}
10var sh_requests = {};
11
12function sh_isEmailAddress(url) {
13 if (/^mailto:/.test(url)) {
14 return false;
15 }
16 return url.indexOf('@') !== -1;
17}
18
19function 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;
28}
29
30/*
31Konqueror has a bug where the regular expression /$/g will not match at the end
32of a line more than once:
33
34 var regex = /$/g;
35 var match;
36
37 var line = '1234567890';
38 regex.lastIndex = 10;
39 match = regex.exec(line);
40
41 var line2 = 'abcde';
42 regex.lastIndex = 5;
43 match = regex.exec(line2); // fails
44*/
45function sh_konquerorExec(s) {
46 var result = [''];
47 result.index = s.length;
48 result.input = s;
49 return result;
50}
51
52/**
53Highlights all elements containing source code in a text string. The return
54value is an array of objects, each representing an HTML start or end tag. Each
55object has a property named pos, which is an integer representing the text
56offset of the tag. Every start tag also has a property named node, which is the
57DOM 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
61*/
62function 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 }
76
77 var a = document.createElement('a');
78 var span = document.createElement('span');
79
80 // the result
81 var tags = [];
82 var numTags = 0;
83
84 // each element is a pattern object from language
85 var patternStack = [];
86
87 // the current position within inputString
88 var pos = 0;
89
90 // the name of the current style, or null if there is no current style
91 var currentStyle = null;
92
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 };
132
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 }
149
150 var line = inputString.substring(start, end);
151
152 var matchCache = [];
153 for (;;) {
154 var posWithinLine = pos - start;
155
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 }
165
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 }
193
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 }
203
204 var pattern = state[bestPatternIndex];
205
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 }
218
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 }
238
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 }
249
250 return tags;
251}
252
253////////////////////////////////////////////////////////////////////////////////
254// DOM-dependent functions
255
256function 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;
268}
269
270function 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(' ');
279}
280
281/**
282Extracts the tags from an HTML DOM NodeList.
283@param nodeList a DOM NodeList
284@param result an object with text, tags and pos properties
285*/
286function 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 }
316}
317
318/**
319Extracts the tags from the text of an HTML element. The extracted tags will be
320returned as an array of tag objects. See sh_highlightString for the format of
321the 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
326*/
327function 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('');
334}
335
336/**
337Merges 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
341*/
342function sh_mergeTags(originalTags, highlightTags) {
343 var numOriginalTags = originalTags.length;
344 if (numOriginalTags === 0) {
345 return highlightTags;
346 }
347
348 var numHighlightTags = highlightTags.length;
349 if (numHighlightTags === 0) {
350 return originalTags;
351 }
352
353 var result = [];
354 var originalIndex = 0;
355 var highlightIndex = 0;
356
357 while (originalIndex < numOriginalTags && highlightIndex < numHighlightTags) {
358 var originalTag = originalTags[originalIndex];
359 var highlightTag = highlightTags[highlightIndex];
360
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});
375
376 // new start tag
377 highlightTags[highlightIndex] = {node: highlightTag.node.cloneNode(false), pos: originalTag.pos};
378 }
379 }
380 }
381
382 while (originalIndex < numOriginalTags) {
383 result.push(originalTags[originalIndex]);
384 originalIndex++;
385 }
386
387 while (highlightIndex < numHighlightTags) {
388 result.push(highlightTags[highlightIndex]);
389 highlightIndex++;
390 }
391
392 return result;
393}
394
395/**
396Inserts 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
400*/
401function sh_insertTags(tags, text) {
402 var doc = document;
403
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;
410
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 }
422
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 }
443
444 return result;
445}
446
447/**
448Highlights an element containing source code. Upon completion of this function,
449the 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
452*/
453function 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);
464}
465
466function 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';
474}
475
476function 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);
505}
506
507/**
508Highlights all elements containing source code on the current page. Elements
509containing 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"
511identifies the element as containing "java" language source code.
512*/
513function highlight(prefix, suffix, tag) {
514 var nodeList = document.getElementsByTagName(tag);
515 for (var i = 0; i < nodeList.length; i++) {
516 var element = nodeList.item(i);
517 var htmlClasses = sh_getClasses(element);
518 var highlighted = false;
519 var donthighlight = false;
520 for (var j = 0; j < htmlClasses.length; j++) {
521 var htmlClass = htmlClasses[j].toLowerCase();
522 if (htmlClass === 'sh_none') {
523 donthighlight = true
524 continue;
525 }
526 if (htmlClass.substr(0, 3) === 'sh_') {
527 var language = htmlClass.substring(3);
528 if (language in sh_languages) {
529 sh_highlightElement(element, sh_languages[language]);
530 highlighted = true;
531 }
532 else if (typeof(prefix) === 'string' && typeof(suffix) === 'string') {
533 sh_load(language, element, prefix, suffix);
534 }
535 else {
536 throw 'Found <' + tag + '> element with class="' + htmlClass + '", but no such language exists';
537 }
538 break;
539 }
540 }
541 if (highlighted === false && donthighlight == false) {
542 sh_highlightElement(element, sh_languages["javascript"]);
543 }
544 }
545}
546
547
548
549function sh_highlightDocument(prefix, suffix) {
550 highlight(prefix, suffix, 'tt');
551 highlight(prefix, suffix, 'code');
552 highlight(prefix, suffix, 'pre');
553}