/* * Copyright 2013 Craig Campbell * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Rainbow is a simple code syntax highlighter * * @preserve @version 1.2 * @url rainbowco.de */ window['Rainbow'] = (function() { /** * array of replacements to process at the end * * @type {Object} */ var replacements = {}, /** * an array of start and end positions of blocks to be replaced * * @type {Object} */ replacement_positions = {}, /** * an array of the language patterns specified for each language * * @type {Object} */ language_patterns = {}, /** * an array of languages and whether they should bypass the default patterns * * @type {Object} */ bypass_defaults = {}, /** * processing level * * replacements are stored at this level so if there is a sub block of code * (for example php inside of html) it runs at a different level * * @type {number} */ CURRENT_LEVEL = 0, /** * constant used to refer to the default language * * @type {number} */ DEFAULT_LANGUAGE = 0, /** * used as counters so we can selectively call setTimeout * after processing a certain number of matches/replacements * * @type {number} */ match_counter = 0, /** * @type {number} */ replacement_counter = 0, /** * @type {null|string} */ global_class, /** * @type {null|Function} */ onHighlight; /** * cross browser get attribute for an element * * @see http://stackoverflow.com/questions/3755227/cross-browser-javascript-getattribute-method * * @param {Node} el * @param {string} attr attribute you are trying to get * @returns {string|number} */ function _attr(el, attr, attrs, i) { var result = (el.getAttribute && el.getAttribute(attr)) || 0; if (!result) { attrs = el.attributes; for (i = 0; i < attrs.length; ++i) { if (attrs[i].nodeName === attr) { return attrs[i].nodeValue; } } } return result; } /** * adds a class to a given code block * * @param {Element} el * @param {string} class_name class name to add * @returns void */ function _addClass(el, class_name) { el.className += el.className ? ' ' + class_name : class_name; } /** * checks if a block has a given class * * @param {Element} el * @param {string} class_name class name to check for * @returns {boolean} */ function _hasClass(el, class_name) { return (' ' + el.className + ' ').indexOf(' ' + class_name + ' ') > -1; } /** * gets the language for this block of code * * @param {Element} block * @returns {string|null} */ function _getLanguageForBlock(block) { // if this doesn't have a language but the parent does then use that // this means if for example you have: <pre data-language="php"> // with a bunch of <code> blocks inside then you do not have // to specify the language for each block var language = _attr(block, 'data-language') || _attr(block.parentNode, 'data-language'); // this adds support for specifying language via a css class // you can use the Google Code Prettify style: <pre class="lang-php"> // or the HTML5 style: <pre><code class="language-php"> if (!language) { var pattern = /\blang(?:uage)?-(\w+)/, match = block.className.match(pattern) || block.parentNode.className.match(pattern); if (match) { language = match[1]; } } return language; } /** * makes sure html entities are always used for tags * * @param {string} code * @returns {string} */ function _htmlEntities(code) { return code.replace(/</g, '<').replace(/>/g, '>').replace(/&(?![\w\#]+;)/g, '&'); } /** * determines if a new match intersects with an existing one * * @param {number} start1 start position of existing match * @param {number} end1 end position of existing match * @param {number} start2 start position of new match * @param {number} end2 end position of new match * @returns {boolean} */ function _intersects(start1, end1, start2, end2) { if (start2 >= start1 && start2 < end1) { return true; } return end2 > start1 && end2 < end1; } /** * determines if two different matches have complete overlap with each other * * @param {number} start1 start position of existing match * @param {number} end1 end position of existing match * @param {number} start2 start position of new match * @param {number} end2 end position of new match * @returns {boolean} */ function _hasCompleteOverlap(start1, end1, start2, end2) { // if the starting and end positions are exactly the same // then the first one should stay and this one should be ignored if (start2 == start1 && end2 == end1) { return false; } return start2 <= start1 && end2 >= end1; } /** * determines if the match passed in falls inside of an existing match * this prevents a regex pattern from matching inside of a bigger pattern * * @param {number} start - start position of new match * @param {number} end - end position of new match * @returns {boolean} */ function _matchIsInsideOtherMatch(start, end) { for (var key in replacement_positions[CURRENT_LEVEL]) { key = parseInt(key, 10); // if this block completely overlaps with another block // then we should remove the other block and return false if (_hasCompleteOverlap(key, replacement_positions[CURRENT_LEVEL][key], start, end)) { delete replacement_positions[CURRENT_LEVEL][key]; delete replacements[CURRENT_LEVEL][key]; } if (_intersects(key, replacement_positions[CURRENT_LEVEL][key], start, end)) { return true; } } return false; } /** * takes a string of code and wraps it in a span tag based on the name * * @param {string} name name of the pattern (ie keyword.regex) * @param {string} code block of code to wrap * @returns {string} */ function _wrapCodeInSpan(name, code) { return '<span class="' + name.replace(/\./g, ' ') + (global_class ? ' ' + global_class : '') + '">' + code + '</span>'; } /** * finds out the position of group match for a regular expression * * @see http://stackoverflow.com/questions/1985594/how-to-find-index-of-groups-in-match * * @param {Object} match * @param {number} group_number * @returns {number} */ function _indexOfGroup(match, group_number) { var index = 0, i; for (i = 1; i < group_number; ++i) { if (match[i]) { index += match[i].length; } } return index; } /** * matches a regex pattern against a block of code * finds all matches that should be processed and stores the positions * of where they should be replaced within the string * * this is where pretty much all the work is done but it should not * be called directly * * @param {RegExp} pattern * @param {string} code * @returns void */ function _processPattern(regex, pattern, code, callback) { var match = regex.exec(code); if (!match) { return callback(); } ++match_counter; // treat match 0 the same way as name if (!pattern['name'] && typeof pattern['matches'][0] == 'string') { pattern['name'] = pattern['matches'][0]; delete pattern['matches'][0]; } var replacement = match[0], start_pos = match.index, end_pos = match[0].length + start_pos, /** * callback to process the next match of this pattern */ processNext = function() { var nextCall = function() { _processPattern(regex, pattern, code, callback); }; // every 100 items we process let's call set timeout // to let the ui breathe a little return match_counter % 100 > 0 ? nextCall() : setTimeout(nextCall, 0); }; // if this is not a child match and it falls inside of another // match that already happened we should skip it and continue processing if (_matchIsInsideOtherMatch(start_pos, end_pos)) { return processNext(); } /** * callback for when a match was successfully processed * * @param {string} replacement * @returns void */ var onMatchSuccess = function(replacement) { // if this match has a name then wrap it in a span tag if (pattern['name']) { replacement = _wrapCodeInSpan(pattern['name'], replacement); } // console.log('LEVEL', CURRENT_LEVEL, 'replace', match[0], 'with', replacement, 'at position', start_pos, 'to', end_pos); // store what needs to be replaced with what at this position if (!replacements[CURRENT_LEVEL]) { replacements[CURRENT_LEVEL] = {}; replacement_positions[CURRENT_LEVEL] = {}; } replacements[CURRENT_LEVEL][start_pos] = { 'replace': match[0], 'with': replacement }; // store the range of this match so we can use it for comparisons // with other matches later replacement_positions[CURRENT_LEVEL][start_pos] = end_pos; // process the next match processNext(); }, // if this pattern has sub matches for different groups in the regex // then we should process them one at a time by rerunning them through // this function to generate the new replacement // // we run through them backwards because the match position of earlier // matches will not change depending on what gets replaced in later // matches group_keys = keys(pattern['matches']), /** * callback for processing a sub group * * @param {number} i * @param {Array} group_keys * @param {Function} callback */ processGroup = function(i, group_keys, callback) { if (i >= group_keys.length) { return callback(replacement); } var processNextGroup = function() { processGroup(++i, group_keys, callback); }, block = match[group_keys[i]]; // if there is no match here then move on if (!block) { return processNextGroup(); } var group = pattern['matches'][group_keys[i]], language = group['language'], /** * process group is what group we should use to actually process * this match group * * for example if the subgroup pattern looks like this * 2: { * 'name': 'keyword', * 'pattern': /true/g * } * * then we use that as is, but if it looks like this * * 2: { * 'name': 'keyword', * 'matches': { * 'name': 'special', * 'pattern': /whatever/g * } * } * * we treat the 'matches' part as the pattern and keep * the name around to wrap it with later */ process_group = group['name'] && group['matches'] ? group['matches'] : group, /** * takes the code block matched at this group, replaces it * with the highlighted block, and optionally wraps it with * a span with a name * * @param {string} block * @param {string} replace_block * @param {string|null} match_name */ _replaceAndContinue = function(block, replace_block, match_name) { replacement = _replaceAtPosition(_indexOfGroup(match, group_keys[i]), block, match_name ? _wrapCodeInSpan(match_name, replace_block) : replace_block, replacement); processNextGroup(); }; // if this is a sublanguage go and process the block using that language if (language) { return _highlightBlockForLanguage(block, language, function(code) { _replaceAndContinue(block, code); }); } // if this is a string then this match is directly mapped to selector // so all we have to do is wrap it in a span and continue if (typeof group === 'string') { return _replaceAndContinue(block, block, group); } // the process group can be a single pattern or an array of patterns // _processCodeWithPatterns always expects an array so we convert it here _processCodeWithPatterns(block, process_group.length ? process_group : [process_group], function(code) { _replaceAndContinue(block, code, group['matches'] ? group['name'] : 0); }); }; processGroup(0, group_keys, onMatchSuccess); } /** * should a language bypass the default patterns? * * if you call Rainbow.extend() and pass true as the third argument * it will bypass the defaults */ function _bypassDefaultPatterns(language) { return bypass_defaults[language]; } /** * returns a list of regex patterns for this language * * @param {string} language * @returns {Array} */ function _getPatternsForLanguage(language) { var patterns = language_patterns[language] || [], default_patterns = language_patterns[DEFAULT_LANGUAGE] || []; return _bypassDefaultPatterns(language) ? patterns : patterns.concat(default_patterns); } /** * substring replace call to replace part of a string at a certain position * * @param {number} position the position where the replacement should happen * @param {string} replace the text we want to replace * @param {string} replace_with the text we want to replace it with * @param {string} code the code we are doing the replacing in * @returns {string} */ function _replaceAtPosition(position, replace, replace_with, code) { var sub_string = code.substr(position); return code.substr(0, position) + sub_string.replace(replace, replace_with); } /** * sorts an object by index descending * * @param {Object} object * @return {Array} */ function keys(object) { var locations = [], replacement, pos; for(var location in object) { if (object.hasOwnProperty(location)) { locations.push(location); } } // numeric descending return locations.sort(function(a, b) { return b - a; }); } /** * processes a block of code using specified patterns * * @param {string} code * @param {Array} patterns * @returns void */ function _processCodeWithPatterns(code, patterns, callback) { // we have to increase the level here so that the // replacements will not conflict with each other when // processing sub blocks of code ++CURRENT_LEVEL; // patterns are processed one at a time through this function function _workOnPatterns(patterns, i) { // still have patterns to process, keep going if (i < patterns.length) { return _processPattern(patterns[i]['pattern'], patterns[i], code, function() { _workOnPatterns(patterns, ++i); }); } // we are done processing the patterns // process the replacements and update the DOM _processReplacements(code, function(code) { // when we are done processing replacements // we are done at this level so we can go back down delete replacements[CURRENT_LEVEL]; delete replacement_positions[CURRENT_LEVEL]; --CURRENT_LEVEL; callback(code); }); } _workOnPatterns(patterns, 0); } /** * process replacements in the string of code to actually update the markup * * @param {string} code the code to process replacements in * @param {Function} onComplete what to do when we are done processing * @returns void */ function _processReplacements(code, onComplete) { /** * processes a single replacement * * @param {string} code * @param {Array} positions * @param {number} i * @param {Function} onComplete * @returns void */ function _processReplacement(code, positions, i, onComplete) { if (i < positions.length) { ++replacement_counter; var pos = positions[i], replacement = replacements[CURRENT_LEVEL][pos]; code = _replaceAtPosition(pos, replacement['replace'], replacement['with'], code); // process next function var next = function() { _processReplacement(code, positions, ++i, onComplete); }; // use a timeout every 250 to not freeze up the UI return replacement_counter % 250 > 0 ? next() : setTimeout(next, 0); } onComplete(code); } var string_positions = keys(replacements[CURRENT_LEVEL]); _processReplacement(code, string_positions, 0, onComplete); } /** * takes a string of code and highlights it according to the language specified * * @param {string} code * @param {string} language * @param {Function} onComplete * @returns void */ function _highlightBlockForLanguage(code, language, onComplete) { var patterns = _getPatternsForLanguage(language); _processCodeWithPatterns(_htmlEntities(code), patterns, onComplete); } /** * highlight an individual code block * * @param {Array} code_blocks * @param {number} i * @returns void */ function _highlightCodeBlock(code_blocks, i, onComplete) { if (i < code_blocks.length) { var block = code_blocks[i], language = _getLanguageForBlock(block); if (!_hasClass(block, 'rainbow') && language) { language = language.toLowerCase(); _addClass(block, 'rainbow'); return _highlightBlockForLanguage(block.innerHTML, language, function(code) { block.innerHTML = code; // reset the replacement arrays replacements = {}; replacement_positions = {}; // if you have a listener attached tell it that this block is now highlighted if (onHighlight) { onHighlight(block, language); } // process the next block setTimeout(function() { _highlightCodeBlock(code_blocks, ++i, onComplete); }, 0); }); } return _highlightCodeBlock(code_blocks, ++i, onComplete); } if (onComplete) { onComplete(); } } /** * start highlighting all the code blocks * * @returns void */ function _highlight(node, onComplete) { // the first argument can be an Event or a DOM Element // I was originally checking instanceof Event but that makes it break // when using mootools // // @see https://github.com/ccampbell/rainbow/issues/32 // node = node && typeof node.getElementsByTagName == 'function' ? node : document; var pre_blocks = node.getElementsByTagName('pre'), code_blocks = node.getElementsByTagName('code'), i, final_pre_blocks = [], final_code_blocks = []; // first loop through all pre blocks to find which ones to highlight // also strip whitespace for (i = 0; i < pre_blocks.length; ++i) { // strip whitespace around code tags when they are inside of a pre tag // this makes the themes look better because you can't accidentally // add extra linebreaks at the start and end // // when the pre tag contains a code tag then strip any extra whitespace // for example // <pre> // <code>var foo = true;</code> // </pre> // // will become // <pre><code>var foo = true;</code></pre> // // if you want to preserve whitespace you can use a pre tag on its own // without a code tag inside of it if (pre_blocks[i].getElementsByTagName('code').length) { pre_blocks[i].innerHTML = pre_blocks[i].innerHTML.replace(/^\s+/, '').replace(/\s+$/, ''); continue; } // if the pre block has no code blocks then we are going to want to // process it directly final_pre_blocks.push(pre_blocks[i]); } // @see http://stackoverflow.com/questions/2735067/how-to-convert-a-dom-node-list-to-an-array-in-javascript // we are going to process all <code> blocks for (i = 0; i < code_blocks.length; ++i) { final_code_blocks.push(code_blocks[i]); } _highlightCodeBlock(final_code_blocks.concat(final_pre_blocks), 0, onComplete); } /** * public methods */ return { /** * extends the language pattern matches * * @param {*} language name of language * @param {*} patterns array of patterns to add on * @param {boolean|null} bypass if true this will bypass the default language patterns */ extend: function(language, patterns, bypass) { // if there is only one argument then we assume that we want to // extend the default language rules if (arguments.length == 1) { patterns = language; language = DEFAULT_LANGUAGE; } bypass_defaults[language] = bypass; language_patterns[language] = patterns.concat(language_patterns[language] || []); }, /** * call back to let you do stuff in your app after a piece of code has been highlighted * * @param {Function} callback */ onHighlight: function(callback) { onHighlight = callback; }, /** * method to set a global class that will be applied to all spans * * @param {string} class_name */ addClass: function(class_name) { global_class = class_name; }, /** * starts the magic rainbow * * @returns void */ color: function() { // if you want to straight up highlight a string you can pass the string of code, // the language, and a callback function if (typeof arguments[0] == 'string') { return _highlightBlockForLanguage(arguments[0], arguments[1], arguments[2]); } // if you pass a callback function then we rerun the color function // on all the code and call the callback function on complete if (typeof arguments[0] == 'function') { return _highlight(0, arguments[0]); } // otherwise we use whatever node you passed in with an optional // callback function as the second parameter _highlight(arguments[0], arguments[1]); } }; }) (); /** * adds event listener to start highlighting */ (function() { if (document.addEventListener) { return document.addEventListener('DOMContentLoaded', Rainbow.color, false); } window.attachEvent('onload', Rainbow.color); }) (); // When using Google closure compiler in advanced mode some methods // get renamed. This keeps a public reference to these methods so they can // still be referenced from outside this library. Rainbow["onHighlight"] = Rainbow.onHighlight; Rainbow["addClass"] = Rainbow.addClass;