rainbow.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774
  1. /**
  2. * Copyright 2012 Craig Campbell
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. *
  16. * Rainbow is a simple code syntax highlighter
  17. *
  18. * @preserve @version 1.1.8
  19. * @url rainbowco.de
  20. */
  21. window['Rainbow'] = (function() {
  22. /**
  23. * array of replacements to process at the end
  24. *
  25. * @type {Object}
  26. */
  27. var replacements = {},
  28. /**
  29. * an array of start and end positions of blocks to be replaced
  30. *
  31. * @type {Object}
  32. */
  33. replacement_positions = {},
  34. /**
  35. * an array of the language patterns specified for each language
  36. *
  37. * @type {Object}
  38. */
  39. language_patterns = {},
  40. /**
  41. * an array of languages and whether they should bypass the default patterns
  42. *
  43. * @type {Object}
  44. */
  45. bypass_defaults = {},
  46. /**
  47. * processing level
  48. *
  49. * replacements are stored at this level so if there is a sub block of code
  50. * (for example php inside of html) it runs at a different level
  51. *
  52. * @type {number}
  53. */
  54. CURRENT_LEVEL = 0,
  55. /**
  56. * constant used to refer to the default language
  57. *
  58. * @type {number}
  59. */
  60. DEFAULT_LANGUAGE = 0,
  61. /**
  62. * used as counters so we can selectively call setTimeout
  63. * after processing a certain number of matches/replacements
  64. *
  65. * @type {number}
  66. */
  67. match_counter = 0,
  68. /**
  69. * @type {number}
  70. */
  71. replacement_counter = 0,
  72. /**
  73. * @type {null|string}
  74. */
  75. global_class,
  76. /**
  77. * @type {null|Function}
  78. */
  79. onHighlight;
  80. /**
  81. * cross browser get attribute for an element
  82. *
  83. * @see http://stackoverflow.com/questions/3755227/cross-browser-javascript-getattribute-method
  84. *
  85. * @param {Node} el
  86. * @param {string} attr attribute you are trying to get
  87. * @returns {string|number}
  88. */
  89. function _attr(el, attr, attrs, i) {
  90. var result = (el.getAttribute && el.getAttribute(attr)) || 0;
  91. if (!result) {
  92. attrs = el.attributes;
  93. for (i = 0; i < attrs.length; ++i) {
  94. if (attrs[i].nodeName === attr) {
  95. return attrs[i].nodeValue;
  96. }
  97. }
  98. }
  99. return result;
  100. }
  101. /**
  102. * adds a class to a given code block
  103. *
  104. * @param {Element} el
  105. * @param {string} class_name class name to add
  106. * @returns void
  107. */
  108. function _addClass(el, class_name) {
  109. el.className += el.className ? ' ' + class_name : class_name;
  110. }
  111. /**
  112. * checks if a block has a given class
  113. *
  114. * @param {Element} el
  115. * @param {string} class_name class name to check for
  116. * @returns {boolean}
  117. */
  118. function _hasClass(el, class_name) {
  119. return (' ' + el.className + ' ').indexOf(' ' + class_name + ' ') > -1;
  120. }
  121. /**
  122. * gets the language for this block of code
  123. *
  124. * @param {Element} block
  125. * @returns {string|null}
  126. */
  127. function _getLanguageForBlock(block) {
  128. // if this doesn't have a language but the parent does then use that
  129. // this means if for example you have: <pre data-language="php">
  130. // with a bunch of <code> blocks inside then you do not have
  131. // to specify the language for each block
  132. var language = _attr(block, 'data-language') || _attr(block.parentNode, 'data-language');
  133. // this adds support for specifying language via a css class
  134. // you can use the Google Code Prettify style: <pre class="lang-php">
  135. // or the HTML5 style: <pre><code class="language-php">
  136. if (!language) {
  137. var pattern = /\blang(?:uage)?-(\w+)/,
  138. match = block.className.match(pattern) || block.parentNode.className.match(pattern);
  139. if (match) {
  140. language = match[1];
  141. }
  142. }
  143. return language;
  144. }
  145. /**
  146. * makes sure html entities are always used for tags
  147. *
  148. * @param {string} code
  149. * @returns {string}
  150. */
  151. function _htmlEntities(code) {
  152. return code.replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/&(?![\w\#]+;)/g, '&amp;');
  153. }
  154. /**
  155. * determines if a new match intersects with an existing one
  156. *
  157. * @param {number} start1 start position of existing match
  158. * @param {number} end1 end position of existing match
  159. * @param {number} start2 start position of new match
  160. * @param {number} end2 end position of new match
  161. * @returns {boolean}
  162. */
  163. function _intersects(start1, end1, start2, end2) {
  164. if (start2 >= start1 && start2 < end1) {
  165. return true;
  166. }
  167. return end2 > start1 && end2 < end1;
  168. }
  169. /**
  170. * determines if two different matches have complete overlap with each other
  171. *
  172. * @param {number} start1 start position of existing match
  173. * @param {number} end1 end position of existing match
  174. * @param {number} start2 start position of new match
  175. * @param {number} end2 end position of new match
  176. * @returns {boolean}
  177. */
  178. function _hasCompleteOverlap(start1, end1, start2, end2) {
  179. // if the starting and end positions are exactly the same
  180. // then the first one should stay and this one should be ignored
  181. if (start2 == start1 && end2 == end1) {
  182. return false;
  183. }
  184. return start2 <= start1 && end2 >= end1;
  185. }
  186. /**
  187. * determines if the match passed in falls inside of an existing match
  188. * this prevents a regex pattern from matching inside of a bigger pattern
  189. *
  190. * @param {number} start - start position of new match
  191. * @param {number} end - end position of new match
  192. * @returns {boolean}
  193. */
  194. function _matchIsInsideOtherMatch(start, end) {
  195. for (var key in replacement_positions[CURRENT_LEVEL]) {
  196. key = parseInt(key, 10);
  197. // if this block completely overlaps with another block
  198. // then we should remove the other block and return false
  199. if (_hasCompleteOverlap(key, replacement_positions[CURRENT_LEVEL][key], start, end)) {
  200. delete replacement_positions[CURRENT_LEVEL][key];
  201. delete replacements[CURRENT_LEVEL][key];
  202. }
  203. if (_intersects(key, replacement_positions[CURRENT_LEVEL][key], start, end)) {
  204. return true;
  205. }
  206. }
  207. return false;
  208. }
  209. /**
  210. * takes a string of code and wraps it in a span tag based on the name
  211. *
  212. * @param {string} name name of the pattern (ie keyword.regex)
  213. * @param {string} code block of code to wrap
  214. * @returns {string}
  215. */
  216. function _wrapCodeInSpan(name, code) {
  217. return '<span class="' + name.replace(/\./g, ' ') + (global_class ? ' ' + global_class : '') + '">' + code + '</span>';
  218. }
  219. /**
  220. * finds out the position of group match for a regular expression
  221. *
  222. * @see http://stackoverflow.com/questions/1985594/how-to-find-index-of-groups-in-match
  223. *
  224. * @param {Object} match
  225. * @param {number} group_number
  226. * @returns {number}
  227. */
  228. function _indexOfGroup(match, group_number) {
  229. var index = 0,
  230. i;
  231. for (i = 1; i < group_number; ++i) {
  232. if (match[i]) {
  233. index += match[i].length;
  234. }
  235. }
  236. return index;
  237. }
  238. /**
  239. * matches a regex pattern against a block of code
  240. * finds all matches that should be processed and stores the positions
  241. * of where they should be replaced within the string
  242. *
  243. * this is where pretty much all the work is done but it should not
  244. * be called directly
  245. *
  246. * @param {RegExp} pattern
  247. * @param {string} code
  248. * @returns void
  249. */
  250. function _processPattern(regex, pattern, code, callback)
  251. {
  252. var match = regex.exec(code);
  253. if (!match) {
  254. return callback();
  255. }
  256. ++match_counter;
  257. // treat match 0 the same way as name
  258. if (!pattern['name'] && typeof pattern['matches'][0] == 'string') {
  259. pattern['name'] = pattern['matches'][0];
  260. delete pattern['matches'][0];
  261. }
  262. var replacement = match[0],
  263. start_pos = match.index,
  264. end_pos = match[0].length + start_pos,
  265. /**
  266. * callback to process the next match of this pattern
  267. */
  268. processNext = function() {
  269. var nextCall = function() {
  270. _processPattern(regex, pattern, code, callback);
  271. };
  272. // every 100 items we process let's call set timeout
  273. // to let the ui breathe a little
  274. return match_counter % 100 > 0 ? nextCall() : setTimeout(nextCall, 0);
  275. };
  276. // if this is not a child match and it falls inside of another
  277. // match that already happened we should skip it and continue processing
  278. if (_matchIsInsideOtherMatch(start_pos, end_pos)) {
  279. return processNext();
  280. }
  281. /**
  282. * callback for when a match was successfully processed
  283. *
  284. * @param {string} replacement
  285. * @returns void
  286. */
  287. var onMatchSuccess = function(replacement) {
  288. // if this match has a name then wrap it in a span tag
  289. if (pattern['name']) {
  290. replacement = _wrapCodeInSpan(pattern['name'], replacement);
  291. }
  292. // console.log('LEVEL', CURRENT_LEVEL, 'replace', match[0], 'with', replacement, 'at position', start_pos, 'to', end_pos);
  293. // store what needs to be replaced with what at this position
  294. if (!replacements[CURRENT_LEVEL]) {
  295. replacements[CURRENT_LEVEL] = {};
  296. replacement_positions[CURRENT_LEVEL] = {};
  297. }
  298. replacements[CURRENT_LEVEL][start_pos] = {
  299. 'replace': match[0],
  300. 'with': replacement
  301. };
  302. // store the range of this match so we can use it for comparisons
  303. // with other matches later
  304. replacement_positions[CURRENT_LEVEL][start_pos] = end_pos;
  305. // process the next match
  306. processNext();
  307. },
  308. // if this pattern has sub matches for different groups in the regex
  309. // then we should process them one at a time by rerunning them through
  310. // this function to generate the new replacement
  311. //
  312. // we run through them backwards because the match position of earlier
  313. // matches will not change depending on what gets replaced in later
  314. // matches
  315. group_keys = keys(pattern['matches']),
  316. /**
  317. * callback for processing a sub group
  318. *
  319. * @param {number} i
  320. * @param {Array} group_keys
  321. * @param {Function} callback
  322. */
  323. processGroup = function(i, group_keys, callback) {
  324. if (i >= group_keys.length) {
  325. return callback(replacement);
  326. }
  327. var processNextGroup = function() {
  328. processGroup(++i, group_keys, callback);
  329. },
  330. block = match[group_keys[i]];
  331. // if there is no match here then move on
  332. if (!block) {
  333. return processNextGroup();
  334. }
  335. var group = pattern['matches'][group_keys[i]],
  336. language = group['language'],
  337. /**
  338. * process group is what group we should use to actually process
  339. * this match group
  340. *
  341. * for example if the subgroup pattern looks like this
  342. * 2: {
  343. * 'name': 'keyword',
  344. * 'pattern': /true/g
  345. * }
  346. *
  347. * then we use that as is, but if it looks like this
  348. *
  349. * 2: {
  350. * 'name': 'keyword',
  351. * 'matches': {
  352. * 'name': 'special',
  353. * 'pattern': /whatever/g
  354. * }
  355. * }
  356. *
  357. * we treat the 'matches' part as the pattern and keep
  358. * the name around to wrap it with later
  359. */
  360. process_group = group['name'] && group['matches'] ? group['matches'] : group,
  361. /**
  362. * takes the code block matched at this group, replaces it
  363. * with the highlighted block, and optionally wraps it with
  364. * a span with a name
  365. *
  366. * @param {string} block
  367. * @param {string} replace_block
  368. * @param {string|null} match_name
  369. */
  370. _replaceAndContinue = function(block, replace_block, match_name) {
  371. replacement = _replaceAtPosition(_indexOfGroup(match, group_keys[i]), block, match_name ? _wrapCodeInSpan(match_name, replace_block) : replace_block, replacement);
  372. processNextGroup();
  373. };
  374. // if this is a sublanguage go and process the block using that language
  375. if (language) {
  376. return _highlightBlockForLanguage(block, language, function(code) {
  377. _replaceAndContinue(block, code);
  378. });
  379. }
  380. // if this is a string then this match is directly mapped to selector
  381. // so all we have to do is wrap it in a span and continue
  382. if (typeof group === 'string') {
  383. return _replaceAndContinue(block, block, group);
  384. }
  385. // the process group can be a single pattern or an array of patterns
  386. // _processCodeWithPatterns always expects an array so we convert it here
  387. _processCodeWithPatterns(block, process_group.length ? process_group : [process_group], function(code) {
  388. _replaceAndContinue(block, code, group['matches'] ? group['name'] : 0);
  389. });
  390. };
  391. processGroup(0, group_keys, onMatchSuccess);
  392. }
  393. /**
  394. * should a language bypass the default patterns?
  395. *
  396. * if you call Rainbow.extend() and pass true as the third argument
  397. * it will bypass the defaults
  398. */
  399. function _bypassDefaultPatterns(language)
  400. {
  401. return bypass_defaults[language];
  402. }
  403. /**
  404. * returns a list of regex patterns for this language
  405. *
  406. * @param {string} language
  407. * @returns {Array}
  408. */
  409. function _getPatternsForLanguage(language) {
  410. var patterns = language_patterns[language] || [],
  411. default_patterns = language_patterns[DEFAULT_LANGUAGE] || [];
  412. return _bypassDefaultPatterns(language) ? patterns : patterns.concat(default_patterns);
  413. }
  414. /**
  415. * substring replace call to replace part of a string at a certain position
  416. *
  417. * @param {number} position the position where the replacement should happen
  418. * @param {string} replace the text we want to replace
  419. * @param {string} replace_with the text we want to replace it with
  420. * @param {string} code the code we are doing the replacing in
  421. * @returns {string}
  422. */
  423. function _replaceAtPosition(position, replace, replace_with, code) {
  424. var sub_string = code.substr(position);
  425. return code.substr(0, position) + sub_string.replace(replace, replace_with);
  426. }
  427. /**
  428. * sorts an object by index descending
  429. *
  430. * @param {Object} object
  431. * @return {Array}
  432. */
  433. function keys(object) {
  434. var locations = [],
  435. replacement,
  436. pos;
  437. for(var location in object) {
  438. if (object.hasOwnProperty(location)) {
  439. locations.push(location);
  440. }
  441. }
  442. // numeric descending
  443. return locations.sort(function(a, b) {
  444. return b - a;
  445. });
  446. }
  447. /**
  448. * processes a block of code using specified patterns
  449. *
  450. * @param {string} code
  451. * @param {Array} patterns
  452. * @returns void
  453. */
  454. function _processCodeWithPatterns(code, patterns, callback)
  455. {
  456. // we have to increase the level here so that the
  457. // replacements will not conflict with each other when
  458. // processing sub blocks of code
  459. ++CURRENT_LEVEL;
  460. // patterns are processed one at a time through this function
  461. function _workOnPatterns(patterns, i)
  462. {
  463. // still have patterns to process, keep going
  464. if (i < patterns.length) {
  465. return _processPattern(patterns[i]['pattern'], patterns[i], code, function() {
  466. _workOnPatterns(patterns, ++i);
  467. });
  468. }
  469. // we are done processing the patterns
  470. // process the replacements and update the DOM
  471. _processReplacements(code, function(code) {
  472. // when we are done processing replacements
  473. // we are done at this level so we can go back down
  474. delete replacements[CURRENT_LEVEL];
  475. delete replacement_positions[CURRENT_LEVEL];
  476. --CURRENT_LEVEL;
  477. callback(code);
  478. });
  479. }
  480. _workOnPatterns(patterns, 0);
  481. }
  482. /**
  483. * process replacements in the string of code to actually update the markup
  484. *
  485. * @param {string} code the code to process replacements in
  486. * @param {Function} onComplete what to do when we are done processing
  487. * @returns void
  488. */
  489. function _processReplacements(code, onComplete) {
  490. /**
  491. * processes a single replacement
  492. *
  493. * @param {string} code
  494. * @param {Array} positions
  495. * @param {number} i
  496. * @param {Function} onComplete
  497. * @returns void
  498. */
  499. function _processReplacement(code, positions, i, onComplete) {
  500. if (i < positions.length) {
  501. ++replacement_counter;
  502. var pos = positions[i],
  503. replacement = replacements[CURRENT_LEVEL][pos];
  504. code = _replaceAtPosition(pos, replacement['replace'], replacement['with'], code);
  505. // process next function
  506. var next = function() {
  507. _processReplacement(code, positions, ++i, onComplete);
  508. };
  509. // use a timeout every 250 to not freeze up the UI
  510. return replacement_counter % 250 > 0 ? next() : setTimeout(next, 0);
  511. }
  512. onComplete(code);
  513. }
  514. var string_positions = keys(replacements[CURRENT_LEVEL]);
  515. _processReplacement(code, string_positions, 0, onComplete);
  516. }
  517. /**
  518. * takes a string of code and highlights it according to the language specified
  519. *
  520. * @param {string} code
  521. * @param {string} language
  522. * @param {Function} onComplete
  523. * @returns void
  524. */
  525. function _highlightBlockForLanguage(code, language, onComplete) {
  526. var patterns = _getPatternsForLanguage(language);
  527. _processCodeWithPatterns(_htmlEntities(code), patterns, onComplete);
  528. }
  529. /**
  530. * highlight an individual code block
  531. *
  532. * @param {Array} code_blocks
  533. * @param {number} i
  534. * @returns void
  535. */
  536. function _highlightCodeBlock(code_blocks, i, onComplete) {
  537. if (i < code_blocks.length) {
  538. var block = code_blocks[i],
  539. language = _getLanguageForBlock(block);
  540. if (!_hasClass(block, 'rainbow') && language) {
  541. language = language.toLowerCase();
  542. _addClass(block, 'rainbow');
  543. return _highlightBlockForLanguage(block.innerHTML, language, function(code) {
  544. block.innerHTML = code;
  545. // reset the replacement arrays
  546. replacements = {};
  547. replacement_positions = {};
  548. // if you have a listener attached tell it that this block is now highlighted
  549. if (onHighlight) {
  550. onHighlight(block, language);
  551. }
  552. // process the next block
  553. setTimeout(function() {
  554. _highlightCodeBlock(code_blocks, ++i, onComplete);
  555. }, 0);
  556. });
  557. }
  558. return _highlightCodeBlock(code_blocks, ++i, onComplete);
  559. }
  560. if (onComplete) {
  561. onComplete();
  562. }
  563. }
  564. /**
  565. * start highlighting all the code blocks
  566. *
  567. * @returns void
  568. */
  569. function _highlight(node, onComplete) {
  570. // the first argument can be an Event or a DOM Element
  571. // I was originally checking instanceof Event but that makes it break
  572. // when using mootools
  573. //
  574. // @see https://github.com/ccampbell/rainbow/issues/32
  575. //
  576. node = node && typeof node.getElementsByTagName == 'function' ? node : document;
  577. var pre_blocks = node.getElementsByTagName('pre'),
  578. code_blocks = node.getElementsByTagName('code'),
  579. i,
  580. final_blocks = [];
  581. // @see http://stackoverflow.com/questions/2735067/how-to-convert-a-dom-node-list-to-an-array-in-javascript
  582. // we are going to process all <code> blocks
  583. for (i = 0; i < code_blocks.length; ++i) {
  584. final_blocks.push(code_blocks[i]);
  585. }
  586. // loop through the pre blocks to see which ones we should add
  587. for (i = 0; i < pre_blocks.length; ++i) {
  588. // if the pre block has no code blocks then process it directly
  589. if (!pre_blocks[i].getElementsByTagName('code').length) {
  590. final_blocks.push(pre_blocks[i]);
  591. }
  592. }
  593. _highlightCodeBlock(final_blocks, 0, onComplete);
  594. }
  595. /**
  596. * public methods
  597. */
  598. return {
  599. /**
  600. * extends the language pattern matches
  601. *
  602. * @param {*} language name of language
  603. * @param {*} patterns array of patterns to add on
  604. * @param {boolean|null} bypass if true this will bypass the default language patterns
  605. */
  606. extend: function(language, patterns, bypass) {
  607. // if there is only one argument then we assume that we want to
  608. // extend the default language rules
  609. if (arguments.length == 1) {
  610. patterns = language;
  611. language = DEFAULT_LANGUAGE;
  612. }
  613. bypass_defaults[language] = bypass;
  614. language_patterns[language] = patterns.concat(language_patterns[language] || []);
  615. },
  616. /**
  617. * call back to let you do stuff in your app after a piece of code has been highlighted
  618. *
  619. * @param {Function} callback
  620. */
  621. onHighlight: function(callback) {
  622. onHighlight = callback;
  623. },
  624. /**
  625. * method to set a global class that will be applied to all spans
  626. *
  627. * @param {string} class_name
  628. */
  629. addClass: function(class_name) {
  630. global_class = class_name;
  631. },
  632. /**
  633. * starts the magic rainbow
  634. *
  635. * @returns void
  636. */
  637. color: function() {
  638. // if you want to straight up highlight a string you can pass the string of code,
  639. // the language, and a callback function
  640. if (typeof arguments[0] == 'string') {
  641. return _highlightBlockForLanguage(arguments[0], arguments[1], arguments[2]);
  642. }
  643. // if you pass a callback function then we rerun the color function
  644. // on all the code and call the callback function on complete
  645. if (typeof arguments[0] == 'function') {
  646. return _highlight(0, arguments[0]);
  647. }
  648. // otherwise we use whatever node you passed in with an optional
  649. // callback function as the second parameter
  650. _highlight(arguments[0], arguments[1]);
  651. }
  652. };
  653. }) ();
  654. /**
  655. * adds event listener to start highlighting
  656. */
  657. (function() {
  658. if (window.addEventListener) {
  659. return window.addEventListener('load', Rainbow.color, false);
  660. }
  661. window.attachEvent('onload', Rainbow.color);
  662. }) ();
  663. // When using Google closure compiler in advanced mode some methods
  664. // get renamed. This keeps a public reference to these methods so they can
  665. // still be referenced from outside this library.
  666. Rainbow["onHighlight"] = Rainbow.onHighlight;
  667. Rainbow["addClass"] = Rainbow.addClass;