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:
154 | // or the HTML5 style:
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, '>').replace(/&(?![\w\#]+;)/g, '&');
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 '' + code + '';
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 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.
772 | Rainbow["onHighlight"] = Rainbow.onHighlight;
773 | Rainbow["addClass"] = Rainbow.addClass;
774 |
--------------------------------------------------------------------------------