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';