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