sh_main.js (15298B)
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 */ 6 7 if (! this.sh_languages) { 8 this.sh_languages = {}; 9 } 10 var sh_requests = {}; 11 12 function sh_isEmailAddress(url) { 13 if (/^mailto:/.test(url)) { 14 return false; 15 } 16 return url.indexOf('@') !== -1; 17 } 18 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; 28 } 29 30 /* 31 Konqueror has a bug where the regular expression /$/g will not match at the end 32 of 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 */ 45 function sh_konquerorExec(s) { 46 var result = ['']; 47 result.index = s.length; 48 result.input = s; 49 return result; 50 } 51 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 61 */ 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 } 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 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; 268 } 269 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(' '); 279 } 280 281 /** 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 285 */ 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 } 316 } 317 318 /** 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 326 */ 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(''); 334 } 335 336 /** 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 341 */ 342 function 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 /** 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 400 */ 401 function 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 /** 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 452 */ 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); 464 } 465 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'; 474 } 475 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); 505 } 506 507 /** 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. 512 */ 513 function 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 549 function sh_highlightDocument(prefix, suffix) { 550 highlight(prefix, suffix, 'tt'); 551 highlight(prefix, suffix, 'code'); 552 highlight(prefix, suffix, 'pre'); 553 }