diff --git a/Gemfile b/Gemfile index 335a4375..298c2d3a 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,7 @@ source 'http://rubygems.org' ruby '2.1.1' -gem 'rails', '~> 4.1.0.rc2' +gem 'rails', '~> 4.1.0' gem 'json' gem 'thin' diff --git a/Gemfile.lock b/Gemfile.lock index 0c7c41ee..dc9879dc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -10,34 +10,33 @@ GIT GEM remote: http://rubygems.org/ specs: - actionmailer (4.1.0.rc2) - actionpack (= 4.1.0.rc2) - actionview (= 4.1.0.rc2) + actionmailer (4.1.0) + actionpack (= 4.1.0) + actionview (= 4.1.0) mail (~> 2.5.4) - actionpack (4.1.0.rc2) - actionview (= 4.1.0.rc2) - activesupport (= 4.1.0.rc2) + actionpack (4.1.0) + actionview (= 4.1.0) + activesupport (= 4.1.0) rack (~> 1.5.2) rack-test (~> 0.6.2) - actionview (4.1.0.rc2) - activesupport (= 4.1.0.rc2) + actionview (4.1.0) + activesupport (= 4.1.0) builder (~> 3.1) erubis (~> 2.7.0) - activemodel (4.1.0.rc2) - activesupport (= 4.1.0.rc2) + activemodel (4.1.0) + activesupport (= 4.1.0) builder (~> 3.1) - activerecord (4.1.0.rc2) - activemodel (= 4.1.0.rc2) - activesupport (= 4.1.0.rc2) + activerecord (4.1.0) + activemodel (= 4.1.0) + activesupport (= 4.1.0) arel (~> 5.0.0) - activesupport (4.1.0.rc2) + activesupport (4.1.0) i18n (~> 0.6, >= 0.6.9) json (~> 1.7, >= 1.7.7) minitest (~> 5.1) thread_safe (~> 0.1) tzinfo (~> 1.1) arel (5.0.0) - atomic (1.1.16) bootstrap-sass (3.1.1.0) sass (~> 3.2) builder (3.2.2) @@ -62,19 +61,19 @@ GEM rack (1.5.2) rack-test (0.6.2) rack (>= 1.0) - rails (4.1.0.rc2) - actionmailer (= 4.1.0.rc2) - actionpack (= 4.1.0.rc2) - actionview (= 4.1.0.rc2) - activemodel (= 4.1.0.rc2) - activerecord (= 4.1.0.rc2) - activesupport (= 4.1.0.rc2) + rails (4.1.0) + actionmailer (= 4.1.0) + actionpack (= 4.1.0) + actionview (= 4.1.0) + activemodel (= 4.1.0) + activerecord (= 4.1.0) + activesupport (= 4.1.0) bundler (>= 1.3.0, < 2.0) - railties (= 4.1.0.rc2) - sprockets-rails (~> 2.0.0) - railties (4.1.0.rc2) - actionpack (= 4.1.0.rc2) - activesupport (= 4.1.0.rc2) + railties (= 4.1.0) + sprockets-rails (~> 2.0) + railties (4.1.0) + actionpack (= 4.1.0) + activesupport (= 4.1.0) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) rake (10.2.2) @@ -99,8 +98,7 @@ GEM eventmachine (>= 1.0.0) rack (>= 1.0.0) thor (0.19.1) - thread_safe (0.3.1) - atomic (>= 1.1.7, < 2) + thread_safe (0.3.3) tilt (1.4.1) treetop (1.4.15) polyglot @@ -119,7 +117,7 @@ DEPENDENCIES jquery-rails json pg - rails (~> 4.1.0.rc2) + rails (~> 4.1.0) sass-rails (~> 4.0.2) simple_form (~> 3.0.0)! sqlite3 diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 51c445bc..161c2050 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -7,4 +7,6 @@ //= require jquery //= require jquery_ujs //= require bootstrap +//= require rainbow +//= require ruby // diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index ed6a599e..dbb0d29f 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -1,5 +1,6 @@ /* *= require bootstrap + *= require github */ body { diff --git a/app/controllers/documentation_controller.rb b/app/controllers/documentation_controller.rb new file mode 100644 index 00000000..c0a678de --- /dev/null +++ b/app/controllers/documentation_controller.rb @@ -0,0 +1,5 @@ +class DocumentationController < ApplicationController + def index + @user_basic = User.new + end +end diff --git a/app/views/documentation/form_example.html.erb b/app/views/documentation/form_example.html.erb new file mode 100644 index 00000000..2790789e --- /dev/null +++ b/app/views/documentation/form_example.html.erb @@ -0,0 +1,22 @@ + +<%%= simple_form_for @user_basic, url: create_basic_examples_url, as: 'user_basic' do |f| %> + + <%%= f.input :email, placeholder: 'Enter email' %> + + <%%= f.input :password, placeholder: 'Password' %> + + <%%= f.input :file, as: :file, wrapper: :vertical_file_input %> + + <%%= f.input :active, wrapper: :vertical_boolean %> + + <%%= f.input :choices, as: :check_boxes, + collection: [ + 'Option one is this and that—be sure to include why it\'s great', + 'Option two can be something else and selecting it will deselect option one'], + wrapper: :vertical_radio_and_checkboxes %> + + <%%= f.input :sex, as: :radio_buttons, + collection: ['Male', 'Female'], wrapper: :vertical_radio_and_checkboxes %> + + <%%= f.button :submit %> +<%% end %> diff --git a/app/views/documentation/form_example_with_wrapper_mappings.html.erb b/app/views/documentation/form_example_with_wrapper_mappings.html.erb new file mode 100644 index 00000000..3a17d5dc --- /dev/null +++ b/app/views/documentation/form_example_with_wrapper_mappings.html.erb @@ -0,0 +1,27 @@ + +<%%= simple_form_for @user_basic, url: create_basic_examples_url, as: 'user_basic', + wrapper_mappings: { + check_boxes: :vertical_radio_and_checkboxes, + radio_buttons: :vertical_radio_and_checkboxes, + file: :vertical_file_input, + boolean: :vertical_boolean + } do |f| %> + + <%%= f.input :email, placeholder: 'Enter email' %> + + <%%= f.input :password, placeholder: 'Password' %> + + <%%= f.input :file, as: :file %> + + <%%= f.input :active %> + + <%%= f.input :choices, as: :check_boxes, + collection: [ + 'Option one is this and that—be sure to include why it\'s great', + 'Option two can be something else and selecting it will deselect option one'] %> + + <%%= f.input :sex, as: :radio_buttons, + collection: ['Male', 'Female'] %> + + <%%= f.button :submit %> +<%% end %> diff --git a/app/views/documentation/index.html.erb b/app/views/documentation/index.html.erb new file mode 100644 index 00000000..98db7bcf --- /dev/null +++ b/app/views/documentation/index.html.erb @@ -0,0 +1,77 @@ +<% content_for :breadcrumb do %> +
rails new my_new_app
gem 'simple_form'
bundle install
rails generate simple_form:install --bootstrap
Example: Lets build the following form.
+The code is straightforward and looks like this:
+ +
+
+ <%= render file: 'documentation/form_example' %>
+
+
+The code is very simple, isn't it?
+ +
+ But it can get better! We can use the wrapper_mapping
option
+ to remove that wrapper duplication. This option receives a Hash containing an input
+ type and the wrapper that will be used for all inputs with specified type.
+
+ Example: +
+ { string: :string_wrapper, boolean: :boolean_wrapper }
+
+
+ All the String inputs will now use the :string_wrapper
, and the
+ same applies to boolean fields, which will use the :boolean_wrapper
+ for all its inputs.
+ You can see more information about wrapper_mappings
+ <%= link_to 'here.', 'https://github.com/plataformatec/simple_form/blob/v3.0.2/lib/simple_form.rb#L107-L111' %>
+
+
+
+ <%= render file: 'documentation/form_example_with_wrapper_mappings' %>
+
+
++ Simple, right? You can see the code for the other examples + <%= link_to 'on GitHub.', 'https://github.com/rafaelfranca/simple_form-bootstrap' %> +
+
+ // with a bunch of 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:
+ // or the HTML5 style:
+ 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(/&(?![\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 '' + code + '';
+ }
+
+ /**
+ * 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)
+ {
+ if (typeof regex === "undefined" || regex === null) {
+ //console.warn("undefined regular expression")
+ return 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
+ //
+ // var foo = true;
+ //
+ //
+ // will become
+ // var foo = true;
+ //
+ // 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 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;
diff --git a/vendor/assets/javascripts/ruby.js b/vendor/assets/javascripts/ruby.js
new file mode 100644
index 00000000..a90fc960
--- /dev/null
+++ b/vendor/assets/javascripts/ruby.js
@@ -0,0 +1,227 @@
+/**
+ * Ruby patterns
+ *
+ * @author Matthew King
+ * @author Jesse Farmer
+ * @author actsasflinn
+ * @version 1.0.6
+ */
+
+Rainbow.extend('ruby', [
+ /**
+ * __END__ DATA
+ */
+ {
+ 'matches': {
+ 1: 'variable.language',
+ 2: {
+ 'language': null
+ }
+ },
+ //find __END__ and consume remaining text
+ 'pattern': /^(__END__)\n((?:.*\n)*)/gm
+ },
+ /**
+ * Strings
+ * 1. No support for multi-line strings
+ */
+ {
+ 'name': 'string',
+ 'matches': {
+ 1: 'string.open',
+ 2: [{
+ 'name': 'string.interpolation',
+ 'matches': {
+ 1: 'string.open',
+ 2: {
+ 'language': 'ruby'
+ },
+ 3: 'string.close'
+ },
+ 'pattern': /(\#\{)(.*?)(\})/g
+ }],
+ 3: 'string.close'
+ },
+ 'pattern': /("|`)(.*?[^\\\1])?(\1)/g
+ },
+ {
+ 'name': 'string',
+ 'pattern': /('|"|`)([^\\\1\n]|\\.)*?\1/g
+ },
+ {
+ 'name': 'string',
+ 'pattern': /%[qQ](?=(\(|\[|\{|<|.)(.*?)(?:'|\)|\]|\}|>|\1))(?:\(\2\)|\[\2\]|\{\2\}|\<\2>|\1\2\1)/g
+ },
+ /**
+ * Heredocs
+ * Heredocs of the form `<<'HTML' ... HTML` are unsupported.
+ */
+ {
+ 'matches': {
+ 1: 'string',
+ 2: 'string',
+ 3: 'string'
+ },
+ 'pattern': /(<<)(\w+).*?$([\s\S]*?^\2)/gm
+ },
+ {
+ 'matches': {
+ 1: 'string',
+ 2: 'string',
+ 3: 'string'
+ },
+ 'pattern': /(<<\-)(\w+).*?$([\s\S]*?\2)/gm
+ },
+ /**
+ * Regular expressions
+ * Escaped delimiter (`/\//`) is unsupported.
+ */
+ {
+ 'name': 'string.regexp',
+ 'matches': {
+ 1: 'string.regexp',
+ 2: {
+ 'name': 'string.regexp',
+ 'pattern': /\\(.){1}/g
+ },
+ 3: 'string.regexp',
+ 4: 'string.regexp'
+ },
+ 'pattern': /(\/)(.*?)(\/)([a-z]*)/g
+ },
+ {
+ 'name': 'string.regexp',
+ 'matches': {
+ 1: 'string.regexp',
+ 2: {
+ 'name': 'string.regexp',
+ 'pattern': /\\(.){1}/g
+ },
+ 3: 'string.regexp',
+ 4: 'string.regexp'
+ },
+ 'pattern': /%r(?=(\(|\[|\{|<|.)(.*?)('|\)|\]|\}|>|\1))(?:\(\2\)|\[\2\]|\{\2\}|\<\2>|\1\2\1)([a-z]*)/g
+ },
+ /**
+ * Comments
+ */
+ {
+ 'name': 'comment',
+ 'pattern': /#.*$/gm
+ },
+ {
+ 'name': 'comment',
+ 'pattern': /^\=begin[\s\S]*?\=end$/gm
+ },
+ /**
+ * Symbols
+ */
+ {
+ 'matches': {
+ 1: 'constant'
+ },
+ 'pattern': /(\w+:)[^:]/g
+ },
+ {
+ 'matches': {
+ 1: 'constant.symbol'
+ },
+ 'pattern': /[^:](:(?:\w+|(?=['"](.*?)['"])(?:"\2"|'\2')))/g
+ },
+ {
+ 'name': 'constant.numeric',
+ 'pattern': /\b(0x[\da-f]+|\d+)\b/g
+ },
+ {
+ 'name': 'support.class',
+ 'pattern': /\b[A-Z]\w*(?=((\.|::)[A-Za-z]|\[))/g
+ },
+ {
+ 'name': 'constant',
+ 'pattern': /\b[A-Z]\w*\b/g
+ },
+ /**
+ * Keywords, variables, constants, and operators
+ * In Ruby some keywords are valid method names, e.g., MyClass#yield
+ * Don't mark those instances as "keywords"
+ */
+ {
+ 'matches': {
+ 1: 'storage.class',
+ 2: 'entity.name.class',
+ 3: 'entity.other.inherited-class'
+ },
+ 'pattern': /\s*(class)\s+((?:(?:::)?[A-Z]\w*)+)(?:\s+<\s+((?:(?:::)?[A-Z]\w*)+))?/g
+ },
+ {
+ 'matches': {
+ 1: 'storage.module',
+ 2: 'entity.name.class'
+ },
+ 'pattern': /\s*(module)\s+((?:(?:::)?[A-Z]\w*)+)/g
+ },
+ {
+ 'name': 'variable.global',
+ 'pattern': /\$([a-zA-Z_]\w*)\b/g
+ },
+ {
+ 'name': 'variable.class',
+ 'pattern': /@@([a-zA-Z_]\w*)\b/g
+ },
+ {
+ 'name': 'variable.instance',
+ 'pattern': /@([a-zA-Z_]\w*)\b/g
+ },
+ {
+ 'matches': {
+ 1: 'keyword.control'
+ },
+ 'pattern': /[^\.]\b(BEGIN|begin|case|class|do|else|elsif|END|end|ensure|for|if|in|module|rescue|then|unless|until|when|while)\b(?![?!])/g
+ },
+ {
+ 'matches': {
+ 1: 'keyword.control.pseudo-method'
+ },
+ 'pattern': /[^\.]\b(alias|alias_method|break|next|redo|retry|return|super|undef|yield)\b(?![?!])|\bdefined\?|\bblock_given\?/g
+ },
+ {
+ 'matches': {
+ 1: 'constant.language'
+ },
+ 'pattern': /\b(nil|true|false)\b(?![?!])/g
+ },
+ {
+ 'matches': {
+ 1: 'variable.language'
+ },
+ 'pattern': /\b(__(FILE|LINE)__|self)\b(?![?!])/g
+ },
+ {
+ 'matches': {
+ 1: 'keyword.special-method'
+ },
+ 'pattern': /\b(require|gem|initialize|new|loop|include|extend|raise|attr_reader|attr_writer|attr_accessor|attr|catch|throw|private|module_function|public|protected)\b(?![?!])/g
+ },
+ {
+ 'name': 'keyword.operator',
+ 'pattern': /\s\?\s|=|<<|<<=|%=|&=|\*=|\*\*=|\+=|\-=|\^=|\|{1,2}=|<<|<=>|<(?!<|=)|>(?!<|=|>)|<=|>=|===|==|=~|!=|!~|%|&|\*\*|\*|\+|\-|\/|\||~|>>/g
+ },
+ {
+ 'matches': {
+ 1: 'keyword.operator.logical'
+ },
+ 'pattern': /[^\.]\b(and|not|or)\b/g
+ },
+
+ /**
+ * Functions
+ * 1. No support for marking function parameters
+ */
+ {
+ 'matches': {
+ 1: 'storage.function',
+ 2: 'entity.name.function'
+ },
+ 'pattern': /(def)\s(.*?)(?=(\s|\())/g
+ }
+], true);
diff --git a/vendor/assets/stylesheets/github.css b/vendor/assets/stylesheets/github.css
new file mode 100644
index 00000000..088f0657
--- /dev/null
+++ b/vendor/assets/stylesheets/github.css
@@ -0,0 +1,88 @@
+/**
+ * GitHub theme
+ *
+ * @author Craig Campbell
+ * @version 1.0.4
+ */
+pre {
+ border: 1px solid #ccc;
+ word-wrap: break-word;
+ padding: 6px 10px;
+ line-height: 19px;
+ margin-bottom: 20px;
+}
+
+code {
+ border: 1px solid #eaeaea;
+ margin: 0px 2px;
+ padding: 0px 5px;
+ font-size: 12px;
+}
+
+pre code {
+ border: 0px;
+ padding: 0px;
+ margin: 0px;
+ -moz-border-radius: 0px;
+ -webkit-border-radius: 0px;
+ border-radius: 0px;
+}
+
+pre, code {
+ font-family: Consolas, 'Liberation Mono', Courier, monospace;
+ color: #333;
+ background: #f8f8f8;
+ -moz-border-radius: 3px;
+ -webkit-border-radius: 3px;
+ border-radius: 3px;
+}
+
+pre, pre code {
+ font-size: 13px;
+}
+
+pre .comment {
+ color: #998;
+}
+
+pre .support {
+ color: #0086B3;
+}
+
+pre .tag, pre .tag-name {
+ color: navy;
+}
+
+pre .keyword, pre .css-property, pre .vendor-prefix, pre .sass, pre .class, pre .id, pre .css-value, pre .entity.function, pre .storage.function {
+ font-weight: bold;
+}
+
+pre .css-property, pre .css-value, pre .vendor-prefix, pre .support.namespace {
+ color: #333;
+}
+
+pre .constant.numeric, pre .keyword.unit, pre .hex-color {
+ font-weight: normal;
+ color: #099;
+}
+
+pre .entity.class {
+ color: #458;
+}
+
+pre .entity.id, pre .entity.function {
+ color: #900;
+}
+
+pre .attribute, pre .variable {
+ color: teal;
+}
+
+pre .string, pre .support.value {
+ font-weight: normal;
+ color: #d14;
+}
+
+pre .regexp {
+ color: #009926;
+}