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