cerubis 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +10 -0
- data/.travis.yml +14 -0
- data/Gemfile +15 -0
- data/README.md +111 -0
- data/Rakefile +15 -0
- data/TODO.md +31 -0
- data/cerubis.gemspec +22 -0
- data/examples/blocks.rb +20 -0
- data/examples/full/blocks/script_block.rb +8 -0
- data/examples/full/config.rb +14 -0
- data/examples/full/example.rb +25 -0
- data/examples/full/helpers/html_helpers.rb +13 -0
- data/examples/full/helpers/include_helper.rb +15 -0
- data/examples/full/models/page.rb +17 -0
- data/examples/full/models/site.rb +13 -0
- data/examples/full/templates/_footer.cerubis +1 -0
- data/examples/full/templates/_header.cerubis +1 -0
- data/examples/full/templates/main.cerubis +31 -0
- data/examples/helpers.rb +22 -0
- data/examples/html.rb +18 -0
- data/examples/variables.rb +10 -0
- data/lib/cerubis.rb +55 -0
- data/lib/cerubis/block.rb +27 -0
- data/lib/cerubis/block_node.rb +37 -0
- data/lib/cerubis/blocks/if.rb +8 -0
- data/lib/cerubis/blocks/loop.rb +20 -0
- data/lib/cerubis/blocks/unless.rb +9 -0
- data/lib/cerubis/condition.rb +59 -0
- data/lib/cerubis/context.rb +30 -0
- data/lib/cerubis/helper.rb +12 -0
- data/lib/cerubis/matcher.rb +19 -0
- data/lib/cerubis/method.rb +19 -0
- data/lib/cerubis/node.rb +27 -0
- data/lib/cerubis/objects/array.rb +4 -0
- data/lib/cerubis/objects/fixnum.rb +4 -0
- data/lib/cerubis/objects/float.rb +4 -0
- data/lib/cerubis/objects/hash.rb +4 -0
- data/lib/cerubis/objects/string.rb +4 -0
- data/lib/cerubis/parser.rb +125 -0
- data/lib/cerubis/syntax_error.rb +4 -0
- data/lib/cerubis/template.rb +21 -0
- data/lib/cerubis/text_node.rb +10 -0
- data/lib/cerubis/variable_replacement.rb +34 -0
- data/lib/cerubis/version.rb +3 -0
- data/test/all.rb +3 -0
- data/test/cerubis/block_node_test.rb +36 -0
- data/test/cerubis/blocks/if_test.rb +24 -0
- data/test/cerubis/blocks/loop_test.rb +25 -0
- data/test/cerubis/blocks/unless_test.rb +25 -0
- data/test/cerubis/condition_test.rb +142 -0
- data/test/cerubis/context_test.rb +33 -0
- data/test/cerubis/helper_test.rb +17 -0
- data/test/cerubis/matcher_test.rb +20 -0
- data/test/cerubis/method_test.rb +60 -0
- data/test/cerubis/parser_test.rb +48 -0
- data/test/cerubis/template_test.rb +38 -0
- data/test/cerubis/text_node_test.rb +16 -0
- data/test/cerubis_test.rb +31 -0
- data/test/matchers/test_block_name.rb +25 -0
- data/test/matchers/test_close_block.rb +25 -0
- data/test/matchers/test_conditions.rb +21 -0
- data/test/matchers/test_helpers.rb +21 -0
- data/test/matchers/test_object_method.rb +37 -0
- data/test/matchers/test_open_block.rb +57 -0
- data/test/matchers/test_operators.rb +29 -0
- data/test/matchers/test_variable.rb +37 -0
- data/test/methods/test_array_methods.rb +21 -0
- data/test/methods/test_fixnum_methods.rb +6 -0
- data/test/methods/test_float_methods.rb +6 -0
- data/test/methods/test_hash_methods.rb +11 -0
- data/test/methods/test_string_methods.rb +11 -0
- data/test/nodes/test_node_defaults.rb +30 -0
- data/test/rendered_test.rb +159 -0
- data/test/test_helper.rb +34 -0
- metadata +149 -0
    
        data/lib/cerubis.rb
    ADDED
    
    | @@ -0,0 +1,55 @@ | |
| 1 | 
            +
            require 'cerubis/variable_replacement'
         | 
| 2 | 
            +
            require 'cerubis/block'
         | 
| 3 | 
            +
            require 'cerubis/blocks/if'
         | 
| 4 | 
            +
            require 'cerubis/blocks/unless'
         | 
| 5 | 
            +
            require 'cerubis/blocks/loop'
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            class Cerubis
         | 
| 8 | 
            +
              autoload :Node,        'cerubis/node'
         | 
| 9 | 
            +
              autoload :TextNode,    'cerubis/text_node'
         | 
| 10 | 
            +
              autoload :BlockNode,   'cerubis/block_node'
         | 
| 11 | 
            +
              autoload :Matcher,     'cerubis/matcher'
         | 
| 12 | 
            +
              autoload :Template,    'cerubis/template'
         | 
| 13 | 
            +
              autoload :Parser,      'cerubis/parser'
         | 
| 14 | 
            +
              autoload :Condition,   'cerubis/condition'
         | 
| 15 | 
            +
              autoload :Context,     'cerubis/context'
         | 
| 16 | 
            +
              autoload :Method,      'cerubis/method'
         | 
| 17 | 
            +
              autoload :Helper,      'cerubis/helper'
         | 
| 18 | 
            +
              autoload :SyntaxError, 'cerubis/syntax_error'
         | 
| 19 | 
            +
             | 
| 20 | 
            +
              def self.register_block(name, klass)
         | 
| 21 | 
            +
                blocks[name] = klass
         | 
| 22 | 
            +
              end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
              def self.register_helper(*mods)
         | 
| 25 | 
            +
                mod = mods.pop
         | 
| 26 | 
            +
                mods.each { |key| helpers[key] = mod }
         | 
| 27 | 
            +
              end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
              def self.blocks
         | 
| 30 | 
            +
                @blocks ||= {}
         | 
| 31 | 
            +
              end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
              def self.helpers
         | 
| 34 | 
            +
                @helpers ||= {}
         | 
| 35 | 
            +
              end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
              def self.render(template, context={})
         | 
| 38 | 
            +
                new.render(template, context)
         | 
| 39 | 
            +
              end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
              def render(template, context={})
         | 
| 42 | 
            +
                Template.new(template, context)
         | 
| 43 | 
            +
              end
         | 
| 44 | 
            +
            end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
            # Patching Ruby core objects
         | 
| 47 | 
            +
            require 'cerubis/objects/array'
         | 
| 48 | 
            +
            require 'cerubis/objects/float'
         | 
| 49 | 
            +
            require 'cerubis/objects/fixnum'
         | 
| 50 | 
            +
            require 'cerubis/objects/hash'
         | 
| 51 | 
            +
            require 'cerubis/objects/string'
         | 
| 52 | 
            +
             | 
| 53 | 
            +
            Cerubis.register_block :if, Cerubis::Blocks::If
         | 
| 54 | 
            +
            Cerubis.register_block :unless, Cerubis::Blocks::Unless
         | 
| 55 | 
            +
            Cerubis.register_block :loop, Cerubis::Blocks::Loop
         | 
| @@ -0,0 +1,27 @@ | |
| 1 | 
            +
            class Cerubis
         | 
| 2 | 
            +
              module Block
         | 
| 3 | 
            +
                def self.included(base)
         | 
| 4 | 
            +
                  base.send(:attr, :condition)
         | 
| 5 | 
            +
                  base.send(:attr, :node)
         | 
| 6 | 
            +
                end
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                def initialize(options)
         | 
| 9 | 
            +
                  @node = options[:node]
         | 
| 10 | 
            +
                  @condition = Condition.new(options[:condition], :context => node.context, :type => options[:type])
         | 
| 11 | 
            +
                end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                def true?
         | 
| 14 | 
            +
                  condition.true?
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                def render
         | 
| 18 | 
            +
                  return unless true?
         | 
| 19 | 
            +
                  replace_variables(yield)
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                private
         | 
| 23 | 
            +
                  def context
         | 
| 24 | 
            +
                    node.context
         | 
| 25 | 
            +
                  end
         | 
| 26 | 
            +
              end
         | 
| 27 | 
            +
            end
         | 
| @@ -0,0 +1,37 @@ | |
| 1 | 
            +
            class Cerubis
         | 
| 2 | 
            +
              class BlockNode
         | 
| 3 | 
            +
                include Node
         | 
| 4 | 
            +
                attr_accessor :block
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                def render
         | 
| 7 | 
            +
                  @render ||= begin
         | 
| 8 | 
            +
                    define_node!
         | 
| 9 | 
            +
                    parse!
         | 
| 10 | 
            +
                    block.render { pre_render }
         | 
| 11 | 
            +
                  end
         | 
| 12 | 
            +
                end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                def pre_render
         | 
| 15 | 
            +
                  return children.map(&:render).join if children?
         | 
| 16 | 
            +
                  content
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                private
         | 
| 20 | 
            +
                  def parse!
         | 
| 21 | 
            +
                    self.children = Parser.new(content, :parent => self).nodes
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                  def define_node!
         | 
| 25 | 
            +
                    open_block    = content.match(/^([\s\t]*)(#{Matcher::OpenBlock})/m)
         | 
| 26 | 
            +
                    close_block   = Matcher::CloseBlockPlaceholder.sub('block_name', open_block[3])
         | 
| 27 | 
            +
                    block_name    = open_block[3].to_sym
         | 
| 28 | 
            +
                    condition_str = open_block[4]
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                    content.sub!(/^#{open_block[0]}/, open_block[1])
         | 
| 31 | 
            +
                    content.sub!(/#{close_block}+\Z/,'')
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                    options = { :context => context, :node => self, :condition => condition_str, :type => block_name }
         | 
| 34 | 
            +
                    self.block = Cerubis.blocks[block_name].new(options)
         | 
| 35 | 
            +
                  end
         | 
| 36 | 
            +
              end
         | 
| 37 | 
            +
            end
         | 
| @@ -0,0 +1,20 @@ | |
| 1 | 
            +
            class Cerubis
         | 
| 2 | 
            +
              module Blocks
         | 
| 3 | 
            +
                class Loop
         | 
| 4 | 
            +
                  include VariableReplacement
         | 
| 5 | 
            +
                  include Block
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                  def render
         | 
| 8 | 
            +
                    return unless true?
         | 
| 9 | 
            +
                    collection   = condition.context_objects[1]
         | 
| 10 | 
            +
                    item_key     = condition.parsed_content[0].to_sym
         | 
| 11 | 
            +
                    loop_context = context.dup
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                    collection.map do |item|
         | 
| 14 | 
            +
                      loop_context[item_key] = item
         | 
| 15 | 
            +
                      replace_variables(yield, loop_context)
         | 
| 16 | 
            +
                    end.join
         | 
| 17 | 
            +
                  end
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
              end
         | 
| 20 | 
            +
            end
         | 
| @@ -0,0 +1,59 @@ | |
| 1 | 
            +
            class Cerubis
         | 
| 2 | 
            +
              class Condition
         | 
| 3 | 
            +
                attr :content
         | 
| 4 | 
            +
                attr :type
         | 
| 5 | 
            +
                attr :context
         | 
| 6 | 
            +
                attr :context_objects
         | 
| 7 | 
            +
                attr :parsed_content
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                def initialize(content, options={})
         | 
| 10 | 
            +
                  @content = content.to_s.strip
         | 
| 11 | 
            +
                  @context = options[:context]
         | 
| 12 | 
            +
                  @type    = options[:type]
         | 
| 13 | 
            +
                  @context_objects = []
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  define_condition!
         | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                def true?
         | 
| 19 | 
            +
                  if @parsed_content.size == 3
         | 
| 20 | 
            +
                    validate_object_and_operator
         | 
| 21 | 
            +
                  else
         | 
| 22 | 
            +
                    validate_object
         | 
| 23 | 
            +
                  end
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                private
         | 
| 27 | 
            +
                  def define_condition!
         | 
| 28 | 
            +
                    match = content.match Matcher::Conditions
         | 
| 29 | 
            +
                    @parsed_content = match ? [match[1], match[6], match[7]].compact : []
         | 
| 30 | 
            +
                  end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                  def validate_object
         | 
| 33 | 
            +
                    @context_objects << context.get(@parsed_content[0])
         | 
| 34 | 
            +
                    @context_objects[0]
         | 
| 35 | 
            +
                  end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                  def validate_object_and_operator
         | 
| 38 | 
            +
                    operator     = @parsed_content[1].to_sym
         | 
| 39 | 
            +
                    obj          = context.get(@parsed_content[0])
         | 
| 40 | 
            +
                    obj_compared = context.get(@parsed_content[2])
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                    @context_objects << obj
         | 
| 43 | 
            +
                    @context_objects << obj_compared
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                    case operator
         | 
| 46 | 
            +
                      when :==    then (obj ==  obj_compared)
         | 
| 47 | 
            +
                      when :===   then (obj === obj_compared)
         | 
| 48 | 
            +
                      when :'!='  then (obj !=  obj_compared)
         | 
| 49 | 
            +
                      when :<     then (obj <   obj_compared)
         | 
| 50 | 
            +
                      when :>     then (obj >   obj_compared)
         | 
| 51 | 
            +
                      when :<=    then (obj <=  obj_compared)
         | 
| 52 | 
            +
                      when :>=    then (obj >=  obj_compared)
         | 
| 53 | 
            +
                      when :in
         | 
| 54 | 
            +
                        raise SyntaxError, 'Invalid block conditions' unless type == :loop
         | 
| 55 | 
            +
                        obj_compared && !obj_compared.empty?
         | 
| 56 | 
            +
                    end
         | 
| 57 | 
            +
                  end
         | 
| 58 | 
            +
              end
         | 
| 59 | 
            +
            end
         | 
| @@ -0,0 +1,30 @@ | |
| 1 | 
            +
            class Cerubis
         | 
| 2 | 
            +
              class Context < Hash
         | 
| 3 | 
            +
                def initialize(hash={})
         | 
| 4 | 
            +
                  super(nil)
         | 
| 5 | 
            +
                  hash.each { |key, value| self[key] = value }
         | 
| 6 | 
            +
                end
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            		def get(key)
         | 
| 9 | 
            +
            			case key
         | 
| 10 | 
            +
            				when /^true$/   then true
         | 
| 11 | 
            +
            				when /^false$/  then false
         | 
| 12 | 
            +
            				when /^[0-9]+$/ then Integer(key)
         | 
| 13 | 
            +
            				when /^[0-9]+\.[0-9]+$/ then Float(key)
         | 
| 14 | 
            +
                    when /^'.*'$/ then key[1..-2]
         | 
| 15 | 
            +
            				else
         | 
| 16 | 
            +
            					object_methods = key.split('.')
         | 
| 17 | 
            +
            					object = self[object_methods.shift.to_sym]
         | 
| 18 | 
            +
            					while meth = object_methods.shift do
         | 
| 19 | 
            +
                        if object.respond_to?(:cerubis_respond_to?) && object.cerubis_respond_to?(meth.to_sym)
         | 
| 20 | 
            +
                          object = object.send(meth)
         | 
| 21 | 
            +
                        else
         | 
| 22 | 
            +
                          object = nil
         | 
| 23 | 
            +
                        end
         | 
| 24 | 
            +
            					end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
            					object
         | 
| 27 | 
            +
            			end
         | 
| 28 | 
            +
            		end
         | 
| 29 | 
            +
              end
         | 
| 30 | 
            +
            end
         | 
| @@ -0,0 +1,19 @@ | |
| 1 | 
            +
            class Cerubis
         | 
| 2 | 
            +
              module Matcher
         | 
| 3 | 
            +
                OpenTag       = '{{'
         | 
| 4 | 
            +
                CloseTag      = '}}'
         | 
| 5 | 
            +
                BlockName     = '[a-z_]+'
         | 
| 6 | 
            +
                Method        = '[a-z_0-9]+'
         | 
| 7 | 
            +
                ObjectMethod  = /\'([^\']*)\'|[a-z0-9_]+((\.#{Method}\??)+)?/
         | 
| 8 | 
            +
                Operators     = [:==, :===, :'!=', :'!==', :<, :>, :<=, :>=, :in]
         | 
| 9 | 
            +
                Conditions    = /(#{ObjectMethod})(\s*(#{Operators.join('|')})\s*(#{ObjectMethod}))?/
         | 
| 10 | 
            +
                OpenBlockStr  = "#{OpenTag}\#(#{BlockName})(\s+#{Conditions})?#{CloseTag}"
         | 
| 11 | 
            +
                CloseBlockPlaceholder = "#{OpenTag}\/(block_name)#{CloseTag}"
         | 
| 12 | 
            +
                CloseBlockStr = CloseBlockPlaceholder.sub('block_name', BlockName)
         | 
| 13 | 
            +
                OpenBlock     = /#{OpenBlockStr}/
         | 
| 14 | 
            +
                CloseBlock    = /#{CloseBlockStr}/
         | 
| 15 | 
            +
                Helpers       = /(#{Method})\s+(#{ObjectMethod}+(,\s+#{ObjectMethod})*)/
         | 
| 16 | 
            +
                Variable      = /#{OpenTag}\s*(#{Helpers}|#{ObjectMethod})\s*#{CloseTag}/
         | 
| 17 | 
            +
                CommaOutsideQuote = /,(?=(?:[^']|'[^']*')*$)/
         | 
| 18 | 
            +
              end
         | 
| 19 | 
            +
            end
         | 
| @@ -0,0 +1,19 @@ | |
| 1 | 
            +
            class Cerubis
         | 
| 2 | 
            +
              module Method
         | 
| 3 | 
            +
                def self.included(base)
         | 
| 4 | 
            +
                  base.extend ClassMethods
         | 
| 5 | 
            +
                end
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                def cerubis_respond_to?(meth)
         | 
| 8 | 
            +
                  availabe_methods = self.class.instance_variable_get(:@cerubis_method) || []
         | 
| 9 | 
            +
                  availabe_methods.include? meth
         | 
| 10 | 
            +
                end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                module ClassMethods
         | 
| 13 | 
            +
                  def cerubis_method(*args)
         | 
| 14 | 
            +
                    @cerubis_method ||= []
         | 
| 15 | 
            +
                    @cerubis_method  += args
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
              end
         | 
| 19 | 
            +
            end
         | 
    
        data/lib/cerubis/node.rb
    ADDED
    
    | @@ -0,0 +1,27 @@ | |
| 1 | 
            +
            class Cerubis
         | 
| 2 | 
            +
              module Node
         | 
| 3 | 
            +
                def self.included(base)
         | 
| 4 | 
            +
                  base.send(:attr, :content)
         | 
| 5 | 
            +
                  base.send(:attr, :parent)
         | 
| 6 | 
            +
                  base.send(:attr_accessor, :children)
         | 
| 7 | 
            +
                end
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                def initialize(content, options)
         | 
| 10 | 
            +
                  @content  = content
         | 
| 11 | 
            +
                  @parent   = options[:parent]
         | 
| 12 | 
            +
                  @children = []
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                def context
         | 
| 16 | 
            +
                  parent.context
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                def children?
         | 
| 20 | 
            +
                  !@children.empty?
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
            		def pre_render
         | 
| 24 | 
            +
            			return content
         | 
| 25 | 
            +
            		end
         | 
| 26 | 
            +
              end
         | 
| 27 | 
            +
            end
         | 
| @@ -0,0 +1,125 @@ | |
| 1 | 
            +
            require 'strscan'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            class Cerubis
         | 
| 4 | 
            +
              class Parser
         | 
| 5 | 
            +
                attr :nodes
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                def initialize(content, options)
         | 
| 8 | 
            +
                  @content = content || ''
         | 
| 9 | 
            +
                  @options = options
         | 
| 10 | 
            +
                  @scanner = StringScanner.new(@content)
         | 
| 11 | 
            +
                  @nodes   = []
         | 
| 12 | 
            +
                  parse!
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                private
         | 
| 16 | 
            +
                  def parse!
         | 
| 17 | 
            +
                    @default_regex = Regexp.new("(#{Matcher::OpenBlock})|(#{Matcher::CloseBlock})")
         | 
| 18 | 
            +
                    @current_regex = @default_regex
         | 
| 19 | 
            +
                    @blocks = []
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                    record_positions until @scanner.eos?
         | 
| 22 | 
            +
                    raise SyntaxError, "An open '#{last_block_name}' block is not closed" if blocks_not_closed?
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                    build_nodes
         | 
| 25 | 
            +
                  end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                  def build_nodes
         | 
| 28 | 
            +
                    if !@blocks.empty?
         | 
| 29 | 
            +
                      str_position = @blocks.first[1] # start of first block
         | 
| 30 | 
            +
                      @blocks.unshift([:text, 0, str_position.pred]) unless str_position.zero?
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                      str_position = @blocks.last[2] # end of last block
         | 
| 33 | 
            +
                      if str_position != @content.size
         | 
| 34 | 
            +
                        @blocks << [:text, str_position.next, @content.size]
         | 
| 35 | 
            +
                      end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                      # loop and fill in missing ranges with text nodes
         | 
| 38 | 
            +
                      @blocks.each_with_index do |block, index|
         | 
| 39 | 
            +
                        next_block = @blocks[index.next]
         | 
| 40 | 
            +
                        position   = block[2]
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                        if next_block && next_block[1] != position.next
         | 
| 43 | 
            +
                          next_index        = index.next
         | 
| 44 | 
            +
                          previous_position = next_block[1].pred
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                          @blocks.insert(next_index, [:text, position.next, previous_position])
         | 
| 47 | 
            +
                        end
         | 
| 48 | 
            +
                      end
         | 
| 49 | 
            +
                    elsif !@content.empty? && @options[:parent].is_a?(Template)
         | 
| 50 | 
            +
                      @blocks << [:text, 0, @content.size]
         | 
| 51 | 
            +
                    end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                    create_node(@blocks.shift) until @blocks.empty?
         | 
| 54 | 
            +
                  end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                  def create_node(block)
         | 
| 57 | 
            +
                    start_of_str = block[1]
         | 
| 58 | 
            +
                    end_of_str   = block[2]
         | 
| 59 | 
            +
                    content      = @content[start_of_str..end_of_str]
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                    if block[0] == :text
         | 
| 62 | 
            +
                      @nodes << TextNode.new(content, @options)
         | 
| 63 | 
            +
                    else
         | 
| 64 | 
            +
                      @nodes << BlockNode.new(content, @options)
         | 
| 65 | 
            +
                    end
         | 
| 66 | 
            +
                  end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                  def record_positions
         | 
| 69 | 
            +
                    @scanner.scan_until @current_regex
         | 
| 70 | 
            +
                    current_match = @scanner[0]
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                    if current_match
         | 
| 73 | 
            +
                      if current_match =~ Matcher::OpenBlock
         | 
| 74 | 
            +
                        parse_open_block
         | 
| 75 | 
            +
                      elsif current_match =~ Matcher::CloseBlock
         | 
| 76 | 
            +
                        parse_close_block
         | 
| 77 | 
            +
                      end
         | 
| 78 | 
            +
                    else
         | 
| 79 | 
            +
                      @scanner.terminate
         | 
| 80 | 
            +
                    end
         | 
| 81 | 
            +
                  end
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                  def nested_block?
         | 
| 84 | 
            +
                    return false if @blocks.empty?
         | 
| 85 | 
            +
                    return false if @blocks.last.is_a?(Array) && @blocks.last.size === 3
         | 
| 86 | 
            +
                    true
         | 
| 87 | 
            +
                  end
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                  def blocks_not_closed?
         | 
| 90 | 
            +
                    # a proper closed block is prepresented as an array of 3 elements
         | 
| 91 | 
            +
                    # [block_name, start_of_block, end_of_block]
         | 
| 92 | 
            +
                    # any less than 3 and its not closed
         | 
| 93 | 
            +
                    !@blocks.empty? && (!@blocks.last.is_a?(Array) || @blocks.last.size < 3)
         | 
| 94 | 
            +
                  end
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                  def parse_open_block
         | 
| 97 | 
            +
                    block_name = @scanner[2].to_sym
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                    if nested_block?
         | 
| 100 | 
            +
                      @blocks << block_name
         | 
| 101 | 
            +
                    else
         | 
| 102 | 
            +
                      @blocks << [block_name, (@scanner.pos - @scanner.matched_size)]
         | 
| 103 | 
            +
                    end
         | 
| 104 | 
            +
                  end
         | 
| 105 | 
            +
             | 
| 106 | 
            +
                  def parse_close_block
         | 
| 107 | 
            +
                    raise SyntaxError, "Closing '#{@scanner[15]}' block was found without an opening" if @blocks.empty?
         | 
| 108 | 
            +
                    block_name = @scanner[15].to_sym
         | 
| 109 | 
            +
             | 
| 110 | 
            +
                    if @blocks.last == block_name # found the nested closing block
         | 
| 111 | 
            +
                      @blocks.pop
         | 
| 112 | 
            +
             | 
| 113 | 
            +
                      close_regex    = Matcher::CloseBlockPlaceholder.sub('block_name', last_block_name.to_s)
         | 
| 114 | 
            +
                      @current_regex = Regexp.new("(#{Matcher::OpenBlock})|(#{close_regex})")
         | 
| 115 | 
            +
                    elsif @blocks && @blocks.last.is_a?(Array) && @blocks.last[0] == block_name
         | 
| 116 | 
            +
                      @blocks.last << @scanner.pos.pred
         | 
| 117 | 
            +
                      @current_regex = @default_regex
         | 
| 118 | 
            +
                    end
         | 
| 119 | 
            +
                  end
         | 
| 120 | 
            +
             | 
| 121 | 
            +
                  def last_block_name
         | 
| 122 | 
            +
                    @blocks.last.is_a?(Array) ? @blocks.last[0] : @blocks.last
         | 
| 123 | 
            +
                  end
         | 
| 124 | 
            +
              end
         | 
| 125 | 
            +
            end
         |