Class: Inversion::Parser

Inherits:
Object
  • Object
show all
Extended by:
Loggability
Defined in:
lib/inversion/parser.rb

Overview

This is the parser for Inversion templates. It takes template source and returns a tree of Inversion::Template::Node objects (if parsing is successful).

Defined Under Namespace

Classes: State

Constant Summary collapse

TAG_OPEN =

The pattern for matching a tag opening

/[\[<]\?/
TAG_PATTERN =

The pattern for matching a tag.

%r{
  (?<tagstart>#{TAG_OPEN})    # Tag opening: either <? or [?
  (?<tagname>[a-z]\w*)        # The name of the tag
  (?:\s+                      # At least once whitespace character between the tagname and body
      (?<body>.+?)            # The body of the tag
  )?
  \s*
  (?<tagend>\?[\]>])          # Tag closing: either ?] or ?>
}x
MATCHING_BRACKETS =

Valid tagends by tagstart

{
  '<?' => '?>',
  '[?' => '?]',
}
DEFAULT_OPTIONS =

Default values for parser configuration options.

{
  :ignore_unknown_tags => true,
}

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(template, options = {}) ⇒ Parser

Create a new Inversion::Parser with the specified config ‘options`.



48
49
50
51
# File 'lib/inversion/parser.rb', line 48

def initialize( template, options={} )
  @template = template
  @options  = DEFAULT_OPTIONS.merge( options )
end

Instance Attribute Details

#optionsObject (readonly)

The parser’s config options



59
60
61
# File 'lib/inversion/parser.rb', line 59

def options
  @options
end

Instance Method Details

#parse(source, inherited_state = nil) ⇒ Object

Parse the given ‘source` into one or more Inversion::Template::Nodes and return it as an Array.



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/inversion/parser.rb', line 64

def parse( source, inherited_state=nil )
  state = nil

  if inherited_state
    inherited_state.template = @template
    state = inherited_state
  else
    state = Inversion::Parser::State.new( @template, self.options )
  end

  self.log.debug "Starting parse of template source (%0.2fK, %s)" %
    [ source.bytesize/1024.0, source.encoding ]

  t0 = Time.now
  last_pos = last_linenum = last_colnum = 0
  source.scan( TAG_PATTERN ) do |*|
    match = Regexp.last_match
    start_pos, end_pos = match.offset( 0 )
    linenum            = match.pre_match.count( "\n" ) + 1
    colnum             = match.pre_match.length - (match.pre_match.rindex("\n") || -1)

    # Error on <?...?] and vice-versa.
    unless match[:tagend] == MATCHING_BRACKETS[match[:tagstart]]
      raise Inversion::ParseError,
        "malformed tag %p: mismatched start and end brackets at line %d, column %d" %
        [ match[0], linenum, colnum ]
    end

    # Check for nested tags
    if match[0].index( TAG_OPEN, 2 )
      raise Inversion::ParseError, "unclosed or nested tag %p at line %d, column %d" %
        [ match[0], linenum, colnum ]
    end

    # self.log.debug "  found a tag at offset: %d (%p) (line %d, col %d)" %
    #     [ start_pos, abbrevstring(match[0]), linenum, colnum ]

    # If there were characters between the end of the last match and
    # the beginning of the tag, create a text node with them
    unless last_pos == start_pos
      text = match.pre_match[ last_pos..-1 ]
      # self.log.debug "  adding literal text node: %p" % [ abbrevstring(text) ]
      state << Inversion::Template::TextNode.new( text, last_linenum, last_colnum )
    end

    # self.log.debug "  creating tag with tagname: %p, body: %p" %
    #    [ match[:tagname], match[:body] ]

    tag = Inversion::Template::Tag.create( match[:tagname], match[:body], linenum, colnum )
    if tag.nil?
      unless state.options[ :ignore_unknown_tags ]
        raise Inversion::ParseError, "Unknown tag %p at line %d, column %d" %
          [ match[:tagname], linenum, colnum ]
      end

        tag = Inversion::Template::TextNode.new( match[0], linenum, colnum )
    end

    # self.log.debug "  created tag: %p" % [ tag ]
    state << tag

    # Keep offsets for the next match
    last_pos     = end_pos
    last_linenum = linenum + match[0].count( "\n" )
    last_colnum  = match[0].length - ( match[0].rindex("\n") || -1 )
  end

  # If there are any characters left over after the last tag
  remainder = source[ last_pos..-1 ]
  if remainder && !remainder.empty?
    # self.log.debug "Remainder after last tag: %p" % [ abbrevstring(remainder) ]

    # Detect unclosed tags
    if remainder.index( "<?" ) || remainder.index( "[?" )
      raise Inversion::ParseError,
        "unclosed tag after line %d, column %d" % [ last_linenum, last_colnum ]
    end

    # Add any remaining text as a text node
    state << Inversion::Template::TextNode.new( remainder, last_linenum, last_colnum )
  end
  self.log.debug "  done parsing: %0.5fs" % [ Time.now - t0 ]

  return state.tree
end