tilde.club/~brennen/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

538 lines
15 KiB

  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. if (! this.sh_languages) {
  7. this.sh_languages = {};
  8. }
  9. var sh_requests = {};
  10. function sh_isEmailAddress(url) {
  11. if (/^mailto:/.test(url)) {
  12. return false;
  13. }
  14. return url.indexOf('@') !== -1;
  15. }
  16. function sh_setHref(tags, numTags, inputString) {
  17. var url = inputString.substring(tags[numTags - 2].pos, tags[numTags - 1].pos);
  18. if (url.length >= 2 && url.charAt(0) === '<' && url.charAt(url.length - 1) === '>') {
  19. url = url.substr(1, url.length - 2);
  20. }
  21. if (sh_isEmailAddress(url)) {
  22. url = 'mailto:' + url;
  23. }
  24. tags[numTags - 2].node.href = url;
  25. }
  26. /*
  27. Konqueror has a bug where the regular expression /$/g will not match at the end
  28. of a line more than once:
  29. var regex = /$/g;
  30. var match;
  31. var line = '1234567890';
  32. regex.lastIndex = 10;
  33. match = regex.exec(line);
  34. var line2 = 'abcde';
  35. regex.lastIndex = 5;
  36. match = regex.exec(line2); // fails
  37. */
  38. function sh_konquerorExec(s) {
  39. var result = [''];
  40. result.index = s.length;
  41. result.input = s;
  42. return result;
  43. }
  44. /**
  45. Highlights all elements containing source code in a text string. The return
  46. value is an array of objects, each representing an HTML start or end tag. Each
  47. object has a property named pos, which is an integer representing the text
  48. offset of the tag. Every start tag also has a property named node, which is the
  49. DOM element started by the tag. End tags do not have this property.
  50. @param inputString a text string
  51. @param language a language definition object
  52. @return an array of tag objects
  53. */
  54. function sh_highlightString(inputString, language) {
  55. if (/Konqueror/.test(navigator.userAgent)) {
  56. if (! language.konquered) {
  57. for (var s = 0; s < language.length; s++) {
  58. for (var p = 0; p < language[s].length; p++) {
  59. var r = language[s][p][0];
  60. if (r.source === '$') {
  61. r.exec = sh_konquerorExec;
  62. }
  63. }
  64. }
  65. language.konquered = true;
  66. }
  67. }
  68. var a = document.createElement('a');
  69. var span = document.createElement('span');
  70. // the result
  71. var tags = [];
  72. var numTags = 0;
  73. // each element is a pattern object from language
  74. var patternStack = [];
  75. // the current position within inputString
  76. var pos = 0;
  77. // the name of the current style, or null if there is no current style
  78. var currentStyle = null;
  79. var output = function(s, style) {
  80. var length = s.length;
  81. // this is more than just an optimization - we don't want to output empty <span></span> elements
  82. if (length === 0) {
  83. return;
  84. }
  85. if (! style) {
  86. var stackLength = patternStack.length;
  87. if (stackLength !== 0) {
  88. var pattern = patternStack[stackLength - 1];
  89. // check whether this is a state or an environment
  90. if (! pattern[3]) {
  91. // it's not a state - it's an environment; use the style for this environment
  92. style = pattern[1];
  93. }
  94. }
  95. }
  96. if (currentStyle !== style) {
  97. if (currentStyle) {
  98. tags[numTags++] = {pos: pos};
  99. if (currentStyle === 'sh_url') {
  100. sh_setHref(tags, numTags, inputString);
  101. }
  102. }
  103. if (style) {
  104. var clone;
  105. if (style === 'sh_url') {
  106. clone = a.cloneNode(false);
  107. }
  108. else {
  109. clone = span.cloneNode(false);
  110. }
  111. clone.className = style;
  112. tags[numTags++] = {node: clone, pos: pos};
  113. }
  114. }
  115. pos += length;
  116. currentStyle = style;
  117. };
  118. var endOfLinePattern = /\r\n|\r|\n/g;
  119. endOfLinePattern.lastIndex = 0;
  120. var inputStringLength = inputString.length;
  121. while (pos < inputStringLength) {
  122. var start = pos;
  123. var end;
  124. var startOfNextLine;
  125. var endOfLineMatch = endOfLinePattern.exec(inputString);
  126. if (endOfLineMatch === null) {
  127. end = inputStringLength;
  128. startOfNextLine = inputStringLength;
  129. }
  130. else {
  131. end = endOfLineMatch.index;
  132. startOfNextLine = endOfLinePattern.lastIndex;
  133. }
  134. var line = inputString.substring(start, end);
  135. var matchCache = [];
  136. for (;;) {
  137. var posWithinLine = pos - start;
  138. var stateIndex;
  139. var stackLength = patternStack.length;
  140. if (stackLength === 0) {
  141. stateIndex = 0;
  142. }
  143. else {
  144. // get the next state
  145. stateIndex = patternStack[stackLength - 1][2];
  146. }
  147. var state = language[stateIndex];
  148. var numPatterns = state.length;
  149. var mc = matchCache[stateIndex];
  150. if (! mc) {
  151. mc = matchCache[stateIndex] = [];
  152. }
  153. var bestMatch = null;
  154. var bestPatternIndex = -1;
  155. for (var i = 0; i < numPatterns; i++) {
  156. var match;
  157. if (i < mc.length && (mc[i] === null || posWithinLine <= mc[i].index)) {
  158. match = mc[i];
  159. }
  160. else {
  161. var regex = state[i][0];
  162. regex.lastIndex = posWithinLine;
  163. match = regex.exec(line);
  164. mc[i] = match;
  165. }
  166. if (match !== null && (bestMatch === null || match.index < bestMatch.index)) {
  167. bestMatch = match;
  168. bestPatternIndex = i;
  169. if (match.index === posWithinLine) {
  170. break;
  171. }
  172. }
  173. }
  174. if (bestMatch === null) {
  175. output(line.substring(posWithinLine), null);
  176. break;
  177. }
  178. else {
  179. // got a match
  180. if (bestMatch.index > posWithinLine) {
  181. output(line.substring(posWithinLine, bestMatch.index), null);
  182. }
  183. var pattern = state[bestPatternIndex];
  184. var newStyle = pattern[1];
  185. var matchedString;
  186. if (newStyle instanceof Array) {
  187. for (var subexpression = 0; subexpression < newStyle.length; subexpression++) {
  188. matchedString = bestMatch[subexpression + 1];
  189. output(matchedString, newStyle[subexpression]);
  190. }
  191. }
  192. else {
  193. matchedString = bestMatch[0];
  194. output(matchedString, newStyle);
  195. }
  196. switch (pattern[2]) {
  197. case -1:
  198. // do nothing
  199. break;
  200. case -2:
  201. // exit
  202. patternStack.pop();
  203. break;
  204. case -3:
  205. // exitall
  206. patternStack.length = 0;
  207. break;
  208. default:
  209. // this was the start of a delimited pattern or a state/environment
  210. patternStack.push(pattern);
  211. break;
  212. }
  213. }
  214. }
  215. // end of the line
  216. if (currentStyle) {
  217. tags[numTags++] = {pos: pos};
  218. if (currentStyle === 'sh_url') {
  219. sh_setHref(tags, numTags, inputString);
  220. }
  221. currentStyle = null;
  222. }
  223. pos = startOfNextLine;
  224. }
  225. return tags;
  226. }
  227. ////////////////////////////////////////////////////////////////////////////////
  228. // DOM-dependent functions
  229. function sh_getClasses(element) {
  230. var result = [];
  231. var htmlClass = element.className;
  232. if (htmlClass && htmlClass.length > 0) {
  233. var htmlClasses = htmlClass.split(' ');
  234. for (var i = 0; i < htmlClasses.length; i++) {
  235. if (htmlClasses[i].length > 0) {
  236. result.push(htmlClasses[i]);
  237. }
  238. }
  239. }
  240. return result;
  241. }
  242. function sh_addClass(element, name) {
  243. var htmlClasses = sh_getClasses(element);
  244. for (var i = 0; i < htmlClasses.length; i++) {
  245. if (name.toLowerCase() === htmlClasses[i].toLowerCase()) {
  246. return;
  247. }
  248. }
  249. htmlClasses.push(name);
  250. element.className = htmlClasses.join(' ');
  251. }
  252. /**
  253. Extracts the tags from an HTML DOM NodeList.
  254. @param nodeList a DOM NodeList
  255. @param result an object with text, tags and pos properties
  256. */
  257. function sh_extractTagsFromNodeList(nodeList, result) {
  258. var length = nodeList.length;
  259. for (var i = 0; i < length; i++) {
  260. var node = nodeList.item(i);
  261. switch (node.nodeType) {
  262. case 1:
  263. if (node.nodeName.toLowerCase() === 'br') {
  264. var terminator;
  265. if (/MSIE/.test(navigator.userAgent)) {
  266. terminator = '\r';
  267. }
  268. else {
  269. terminator = '\n';
  270. }
  271. result.text.push(terminator);
  272. result.pos++;
  273. }
  274. else {
  275. result.tags.push({node: node.cloneNode(false), pos: result.pos});
  276. sh_extractTagsFromNodeList(node.childNodes, result);
  277. result.tags.push({pos: result.pos});
  278. }
  279. break;
  280. case 3:
  281. case 4:
  282. result.text.push(node.data);
  283. result.pos += node.length;
  284. break;
  285. }
  286. }
  287. }
  288. /**
  289. Extracts the tags from the text of an HTML element. The extracted tags will be
  290. returned as an array of tag objects. See sh_highlightString for the format of
  291. the tag objects.
  292. @param element a DOM element
  293. @param tags an empty array; the extracted tag objects will be returned in it
  294. @return the text of the element
  295. @see sh_highlightString
  296. */
  297. function sh_extractTags(element, tags) {
  298. var result = {};
  299. result.text = [];
  300. result.tags = tags;
  301. result.pos = 0;
  302. sh_extractTagsFromNodeList(element.childNodes, result);
  303. return result.text.join('');
  304. }
  305. /**
  306. Merges the original tags from an element with the tags produced by highlighting.
  307. @param originalTags an array containing the original tags
  308. @param highlightTags an array containing the highlighting tags - these must not overlap
  309. @result an array containing the merged tags
  310. */
  311. function sh_mergeTags(originalTags, highlightTags) {
  312. var numOriginalTags = originalTags.length;
  313. if (numOriginalTags === 0) {
  314. return highlightTags;
  315. }
  316. var numHighlightTags = highlightTags.length;
  317. if (numHighlightTags === 0) {
  318. return originalTags;
  319. }
  320. var result = [];
  321. var originalIndex = 0;
  322. var highlightIndex = 0;
  323. while (originalIndex < numOriginalTags && highlightIndex < numHighlightTags) {
  324. var originalTag = originalTags[originalIndex];
  325. var highlightTag = highlightTags[highlightIndex];
  326. if (originalTag.pos <= highlightTag.pos) {
  327. result.push(originalTag);
  328. originalIndex++;
  329. }
  330. else {
  331. result.push(highlightTag);
  332. if (highlightTags[highlightIndex + 1].pos <= originalTag.pos) {
  333. highlightIndex++;
  334. result.push(highlightTags[highlightIndex]);
  335. highlightIndex++;
  336. }
  337. else {
  338. // new end tag
  339. result.push({pos: originalTag.pos});
  340. // new start tag
  341. highlightTags[highlightIndex] = {node: highlightTag.node.cloneNode(false), pos: originalTag.pos};
  342. }
  343. }
  344. }
  345. while (originalIndex < numOriginalTags) {
  346. result.push(originalTags[originalIndex]);
  347. originalIndex++;
  348. }
  349. while (highlightIndex < numHighlightTags) {
  350. result.push(highlightTags[highlightIndex]);
  351. highlightIndex++;
  352. }
  353. return result;
  354. }
  355. /**
  356. Inserts tags into text.
  357. @param tags an array of tag objects
  358. @param text a string representing the text
  359. @return a DOM DocumentFragment representing the resulting HTML
  360. */
  361. function sh_insertTags(tags, text) {
  362. var doc = document;
  363. var result = document.createDocumentFragment();
  364. var tagIndex = 0;
  365. var numTags = tags.length;
  366. var textPos = 0;
  367. var textLength = text.length;
  368. var currentNode = result;
  369. // output one tag or text node every iteration
  370. while (textPos < textLength || tagIndex < numTags) {
  371. var tag;
  372. var tagPos;
  373. if (tagIndex < numTags) {
  374. tag = tags[tagIndex];
  375. tagPos = tag.pos;
  376. }
  377. else {
  378. tagPos = textLength;
  379. }
  380. if (tagPos <= textPos) {
  381. // output the tag
  382. if (tag.node) {
  383. // start tag
  384. var newNode = tag.node;
  385. currentNode.appendChild(newNode);
  386. currentNode = newNode;
  387. }
  388. else {
  389. // end tag
  390. currentNode = currentNode.parentNode;
  391. }
  392. tagIndex++;
  393. }
  394. else {
  395. // output text
  396. currentNode.appendChild(doc.createTextNode(text.substring(textPos, tagPos)));
  397. textPos = tagPos;
  398. }
  399. }
  400. return result;
  401. }
  402. /**
  403. Highlights an element containing source code. Upon completion of this function,
  404. the element will have been placed in the "sh_sourceCode" class.
  405. @param element a DOM <pre> element containing the source code to be highlighted
  406. @param language a language definition object
  407. */
  408. function sh_highlightElement(element, language) {
  409. sh_addClass(element, 'sh_sourceCode');
  410. var originalTags = [];
  411. var inputString = sh_extractTags(element, originalTags);
  412. var highlightTags = sh_highlightString(inputString, language);
  413. var tags = sh_mergeTags(originalTags, highlightTags);
  414. var documentFragment = sh_insertTags(tags, inputString);
  415. while (element.hasChildNodes()) {
  416. element.removeChild(element.firstChild);
  417. }
  418. element.appendChild(documentFragment);
  419. }
  420. function sh_getXMLHttpRequest() {
  421. if (window.ActiveXObject) {
  422. return new ActiveXObject('Msxml2.XMLHTTP');
  423. }
  424. else if (window.XMLHttpRequest) {
  425. return new XMLHttpRequest();
  426. }
  427. throw 'No XMLHttpRequest implementation available';
  428. }
  429. function sh_load(language, element, prefix, suffix) {
  430. if (language in sh_requests) {
  431. sh_requests[language].push(element);
  432. return;
  433. }
  434. sh_requests[language] = [element];
  435. var request = sh_getXMLHttpRequest();
  436. var url = prefix + 'sh_' + language + suffix;
  437. request.open('GET', url, true);
  438. request.onreadystatechange = function () {
  439. if (request.readyState === 4) {
  440. try {
  441. if (! request.status || request.status === 200) {
  442. eval(request.responseText);
  443. var elements = sh_requests[language];
  444. for (var i = 0; i < elements.length; i++) {
  445. sh_highlightElement(elements[i], sh_languages[language]);
  446. }
  447. }
  448. else {
  449. throw 'HTTP error: status ' + request.status;
  450. }
  451. }
  452. finally {
  453. request = null;
  454. }
  455. }
  456. };
  457. request.send(null);
  458. }
  459. /**
  460. Highlights all elements containing source code on the current page. Elements
  461. containing source code must be "pre" elements with a "class" attribute of
  462. "sh_LANGUAGE", where LANGUAGE is a valid language identifier; e.g., "sh_java"
  463. identifies the element as containing "java" language source code.
  464. */
  465. function sh_highlightDocument(prefix, suffix) {
  466. var nodeList = document.getElementsByTagName('pre');
  467. for (var i = 0; i < nodeList.length; i++) {
  468. var element = nodeList.item(i);
  469. var htmlClasses = sh_getClasses(element);
  470. for (var j = 0; j < htmlClasses.length; j++) {
  471. var htmlClass = htmlClasses[j].toLowerCase();
  472. if (htmlClass === 'sh_sourcecode') {
  473. continue;
  474. }
  475. if (htmlClass.substr(0, 3) === 'sh_') {
  476. var language = htmlClass.substring(3);
  477. if (language in sh_languages) {
  478. sh_highlightElement(element, sh_languages[language]);
  479. }
  480. else if (typeof(prefix) === 'string' && typeof(suffix) === 'string') {
  481. sh_load(language, element, prefix, suffix);
  482. }
  483. else {
  484. throw 'Found <pre> element with class="' + htmlClass + '", but no such language exists';
  485. }
  486. break;
  487. }
  488. }
  489. }
  490. }