inversion 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.
Files changed (78) hide show
  1. data.tar.gz.sig +2 -0
  2. data/.gemtest +0 -0
  3. data/ChangeLog +836 -0
  4. data/History.md +4 -0
  5. data/Manifest.txt +74 -0
  6. data/README.rdoc +171 -0
  7. data/Rakefile +55 -0
  8. data/bin/inversion +276 -0
  9. data/lib/inversion.rb +98 -0
  10. data/lib/inversion/exceptions.rb +21 -0
  11. data/lib/inversion/mixins.rb +236 -0
  12. data/lib/inversion/monkeypatches.rb +20 -0
  13. data/lib/inversion/renderstate.rb +337 -0
  14. data/lib/inversion/sinatra.rb +35 -0
  15. data/lib/inversion/template.rb +250 -0
  16. data/lib/inversion/template/attrtag.rb +120 -0
  17. data/lib/inversion/template/calltag.rb +16 -0
  18. data/lib/inversion/template/codetag.rb +164 -0
  19. data/lib/inversion/template/commenttag.rb +54 -0
  20. data/lib/inversion/template/conditionaltag.rb +49 -0
  21. data/lib/inversion/template/configtag.rb +60 -0
  22. data/lib/inversion/template/containertag.rb +45 -0
  23. data/lib/inversion/template/elsetag.rb +62 -0
  24. data/lib/inversion/template/elsiftag.rb +49 -0
  25. data/lib/inversion/template/endtag.rb +55 -0
  26. data/lib/inversion/template/escapetag.rb +26 -0
  27. data/lib/inversion/template/fortag.rb +120 -0
  28. data/lib/inversion/template/iftag.rb +69 -0
  29. data/lib/inversion/template/importtag.rb +70 -0
  30. data/lib/inversion/template/includetag.rb +51 -0
  31. data/lib/inversion/template/node.rb +102 -0
  32. data/lib/inversion/template/parser.rb +297 -0
  33. data/lib/inversion/template/pptag.rb +28 -0
  34. data/lib/inversion/template/publishtag.rb +72 -0
  35. data/lib/inversion/template/subscribetag.rb +88 -0
  36. data/lib/inversion/template/tag.rb +150 -0
  37. data/lib/inversion/template/textnode.rb +43 -0
  38. data/lib/inversion/template/unlesstag.rb +60 -0
  39. data/lib/inversion/template/uriencodetag.rb +30 -0
  40. data/lib/inversion/template/yieldtag.rb +51 -0
  41. data/lib/inversion/tilt.rb +82 -0
  42. data/lib/inversion/utils.rb +235 -0
  43. data/spec/data/sinatra/hello.inversion +1 -0
  44. data/spec/inversion/mixins_spec.rb +177 -0
  45. data/spec/inversion/monkeypatches_spec.rb +35 -0
  46. data/spec/inversion/renderstate_spec.rb +291 -0
  47. data/spec/inversion/sinatra_spec.rb +59 -0
  48. data/spec/inversion/template/attrtag_spec.rb +216 -0
  49. data/spec/inversion/template/calltag_spec.rb +30 -0
  50. data/spec/inversion/template/codetag_spec.rb +51 -0
  51. data/spec/inversion/template/commenttag_spec.rb +84 -0
  52. data/spec/inversion/template/configtag_spec.rb +105 -0
  53. data/spec/inversion/template/containertag_spec.rb +54 -0
  54. data/spec/inversion/template/elsetag_spec.rb +105 -0
  55. data/spec/inversion/template/elsiftag_spec.rb +87 -0
  56. data/spec/inversion/template/endtag_spec.rb +78 -0
  57. data/spec/inversion/template/escapetag_spec.rb +59 -0
  58. data/spec/inversion/template/fortag_spec.rb +98 -0
  59. data/spec/inversion/template/iftag_spec.rb +241 -0
  60. data/spec/inversion/template/importtag_spec.rb +106 -0
  61. data/spec/inversion/template/includetag_spec.rb +108 -0
  62. data/spec/inversion/template/node_spec.rb +81 -0
  63. data/spec/inversion/template/parser_spec.rb +170 -0
  64. data/spec/inversion/template/pptag_spec.rb +51 -0
  65. data/spec/inversion/template/publishtag_spec.rb +69 -0
  66. data/spec/inversion/template/subscribetag_spec.rb +60 -0
  67. data/spec/inversion/template/tag_spec.rb +97 -0
  68. data/spec/inversion/template/textnode_spec.rb +86 -0
  69. data/spec/inversion/template/unlesstag_spec.rb +84 -0
  70. data/spec/inversion/template/uriencodetag_spec.rb +49 -0
  71. data/spec/inversion/template/yieldtag_spec.rb +54 -0
  72. data/spec/inversion/template_spec.rb +269 -0
  73. data/spec/inversion/tilt_spec.rb +47 -0
  74. data/spec/inversion_spec.rb +95 -0
  75. data/spec/lib/constants.rb +9 -0
  76. data/spec/lib/helpers.rb +160 -0
  77. metadata +316 -0
  78. metadata.gz.sig +0 -0
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env ruby
2
+ # vim: set noet nosta sw=4 ts=4 :
3
+
4
+ require 'inversion/mixins'
5
+ require 'inversion/template/attrtag'
6
+ require 'inversion/template/containertag'
7
+ require 'inversion/template/conditionaltag'
8
+ require 'inversion/template/elsiftag'
9
+ require 'inversion/template/elsetag'
10
+
11
+ # Inversion 'if' tag.
12
+ #
13
+ # This tag causes a section of the template to be rendered only if its methodchain or attribute
14
+ # is a true value.
15
+ #
16
+ # == Syntax
17
+ #
18
+ # <?if attr ?>...<?end?>
19
+ # <?if obj.method ?>...<?end?>
20
+ #
21
+ class Inversion::Template::IfTag < Inversion::Template::AttrTag
22
+ include Inversion::Loggable,
23
+ Inversion::Template::ContainerTag,
24
+ Inversion::Template::ConditionalTag
25
+
26
+ # Inherits AttrTag's tag patterns
27
+
28
+ ### Render the tag's contents if the condition is true, or any else or elsif sections
29
+ ### if the condition isn't true.
30
+ def render( state )
31
+ self.enable_rendering if super
32
+ self.render_subnodes( state )
33
+ return nil
34
+ end
35
+
36
+
37
+ ### Render the tag's subnodes according to the tag's logical state.
38
+ def render_subnodes( renderstate )
39
+ self.log.debug "Rendering subnodes. Rendering initially %s" %
40
+ [ self.rendering_enabled? ? "enabled" : "disabled" ]
41
+
42
+ # walk the subtree, modifying the logic flags for else and elsif tags,
43
+ # and rendering nodes if rendering is enabled
44
+ self.subnodes.each do |node|
45
+ case node
46
+ when Inversion::Template::ElsifTag
47
+ self.log.debug " logic switch: %p..." % [ node ]
48
+ if !self.rendering_was_enabled? && node.render( renderstate )
49
+ self.enable_rendering
50
+ else
51
+ self.disable_rendering
52
+ end
53
+
54
+ when Inversion::Template::ElseTag
55
+ self.log.debug " logic switch: %p..." % [ node ]
56
+ if !self.rendering_was_enabled?
57
+ self.enable_rendering
58
+ else
59
+ self.disable_rendering
60
+ end
61
+
62
+ else
63
+ renderstate << node if self.rendering_enabled?
64
+ end
65
+ end
66
+ end
67
+
68
+ end # class Inversion::Template::IfTag
69
+
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env ruby
2
+ # vim: set noet nosta sw=4 ts=4 :
3
+
4
+ require 'inversion/template/tag'
5
+
6
+ # Inversion import tag.
7
+ #
8
+ # The import tag copies one or more attributes from an enclosing template so they
9
+ # only need to be set once.
10
+ #
11
+ # == Syntax
12
+ #
13
+ # <?import txn ?>
14
+ # <?import foo, bar ?>
15
+ #
16
+ class Inversion::Template::ImportTag < Inversion::Template::Tag
17
+
18
+ ### Create a new ImportTag with the given +name+, which should be a valid
19
+ ### Ruby identifier.
20
+ def initialize( body, linenum=nil, colnum=nil )
21
+ super
22
+ @attributes = body.split( /\s*,\s*/ ).collect {|name| name.untaint.strip.to_sym }
23
+ end
24
+
25
+
26
+ ######
27
+ public
28
+ ######
29
+
30
+ # the names of the attributes to import
31
+ attr_reader :attributes
32
+
33
+
34
+ ### Merge the inherited renderstate into the current template's +renderstate+.
35
+ def render( renderstate )
36
+ if (( cstate = renderstate.containerstate ))
37
+ self.log.debug "Importing inherited attributes: %p from %p" % [ @attributes, cstate.attributes ]
38
+
39
+ # Pick out the attributes that are being imported
40
+ inherited_attrs = @attributes.inject( {} ) do |attrs, key|
41
+ attrs[ key ] = cstate.attributes[ key ]
42
+ attrs
43
+ end
44
+
45
+ # Merge, but overwrite unset values with inherited ones
46
+ renderstate.attributes.merge!( inherited_attrs ) do |key, oldval, newval|
47
+ if oldval.nil?
48
+ self.log.debug "Importing attribute %p: %p" % [ key, newval ]
49
+ newval
50
+ else
51
+ self.log.debug "Not importing attribute %p: already set to %p" % [ key, oldval ]
52
+ oldval
53
+ end
54
+ end
55
+
56
+ else
57
+ self.log.debug "No-op import: no parent attributes set."
58
+ end
59
+
60
+ return nil
61
+ end
62
+
63
+
64
+ ### Render the tag as the body of a comment, suitable for template debugging.
65
+ def as_comment_body
66
+ return "Import %s" % [ self.attributes.join(', ') ]
67
+ end
68
+
69
+ end # class Inversion::Template::ImportTag
70
+
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env ruby
2
+ # vim: set noet nosta sw=4 ts=4 :
3
+
4
+ require 'pathname'
5
+ require 'inversion/mixins'
6
+ require 'inversion/template/tag'
7
+
8
+
9
+ # Inversion 'include' tag.
10
+ #
11
+ # A tag that inserts other template files into the current template.
12
+ #
13
+ # == Example
14
+ #
15
+ # <?include /an/absolute/path/to/a/different/template.tmpl ?>
16
+ # <?include a/relative/path/to/a/different/template.tmpl ?>
17
+ #
18
+ #
19
+ class Inversion::Template::IncludeTag < Inversion::Template::Tag
20
+ include Inversion::Loggable
21
+
22
+
23
+ ### Create a new IncludeTag with the specified +path+.
24
+ def initialize( path, linenum=nil, colnum=nil )
25
+ super
26
+ self.log.debug "Body is: %p" % [ @body ]
27
+ @path = @body
28
+ @included_template = nil
29
+ end
30
+
31
+
32
+ ######
33
+ public
34
+ ######
35
+
36
+ # The path of the included template
37
+ attr_reader :path
38
+
39
+
40
+ ### Parser callback -- Load the included template and check for recursive includes.
41
+ def before_appending( parsestate )
42
+ @included_template = parsestate.load_subtemplate( self.path )
43
+ end
44
+
45
+
46
+ def after_appending( parsestate )
47
+ parsestate.append_tree( @included_template.node_tree )
48
+ end
49
+
50
+ end # class Inversion::Template::IncludeTag
51
+
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env ruby
2
+ # vim: set noet nosta sw=4 ts=4 :
3
+
4
+ require 'inversion/mixins'
5
+ require 'inversion/template' unless defined?( Inversion::Template )
6
+
7
+ # Inversion template node base class. Template text is parsed by the
8
+ # Inversion::Template::Parser into nodes, and appended to a tree
9
+ # that is later walked when the template is rendered.
10
+ #
11
+ # This class is abstract; it just defines the API that other nodes
12
+ # are expected to implement.
13
+ class Inversion::Template::Node
14
+ include Inversion::AbstractClass
15
+
16
+
17
+ ### Create a new TextNode with the specified +source+.
18
+ def initialize( body, linenum=nil, colnum=nil )
19
+ @body = body
20
+ @linenum = linenum
21
+ @colnum = colnum
22
+ end
23
+
24
+
25
+ ######
26
+ public
27
+ ######
28
+
29
+ # The line number the node was parsed from in the template source (if known)
30
+ attr_reader :linenum
31
+
32
+ # The column number the node was parsed from in the template source (if known)
33
+ attr_reader :colnum
34
+
35
+
36
+ ### Render the node using the given +render_state+. By default, rendering a node
37
+ ### returns +nil+.
38
+ def render( render_state )
39
+ return nil
40
+ end
41
+
42
+
43
+ ### Render the node as a comment
44
+ def as_comment_body
45
+ return self.inspect
46
+ end
47
+
48
+
49
+ ### Returns +true+ if the node introduces a new parsing/rendering scope.
50
+ def is_container?
51
+ return false
52
+ end
53
+ alias_method :container?, :is_container?
54
+
55
+
56
+ ### Return the location of the tag in the template, if it was parsed from one (i.e.,
57
+ ### if it was created with a StringScanner)
58
+ def location
59
+ return "line %s, column %s" % [
60
+ self.linenum || '??',
61
+ self.colnum || '??',
62
+ ]
63
+ end
64
+
65
+
66
+ ### Default (no-op) implementation of the before_appending callback. This exists so defining
67
+ ### the append callbacks are optional for Node's subclasses.
68
+ def before_appending( state )
69
+ # Nothing to do
70
+ return nil
71
+ end
72
+ alias_method :before_append, :before_appending
73
+
74
+
75
+ ### Default (no-op) implementation of the after_appending callback. This exists so defining
76
+ ### the append callbacks are optional for Node's subclasses.
77
+ def after_appending( state )
78
+ # Nothing to do
79
+ return nil
80
+ end
81
+ alias_method :after_append, :after_appending
82
+
83
+
84
+ ### Default (no-op) implementation of the before_rendering callback. This exists so defining
85
+ ### the rendering callbacks are optional for Node's subclasses.
86
+ def before_rendering( state=nil )
87
+ # Nothing to do
88
+ return nil
89
+ end
90
+ alias_method :before_render, :before_rendering
91
+
92
+
93
+ ### Default (no-op) implementation of the after_rendering callback. This exists so defining
94
+ ### the rendering callbacks are optional for Node's subclasses.
95
+ def after_rendering( state=nil )
96
+ # Nothing to do
97
+ return nil
98
+ end
99
+ alias_method :after_render, :after_rendering
100
+
101
+ end # class Inversion::Template::Node
102
+
@@ -0,0 +1,297 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+ # vim: set noet nosta sw=4 ts=4 :
4
+
5
+ require 'inversion/template' unless defined?( Inversion::Template )
6
+ require 'inversion/mixins'
7
+ require 'inversion/template/textnode'
8
+ require 'inversion/template/tag'
9
+ require 'inversion/template/endtag'
10
+
11
+ # This is the parser for Inversion templates. It takes template source and
12
+ # returns a tree of Inversion::Template::Node objects (if parsing is successful).
13
+ class Inversion::Template::Parser
14
+ include Inversion::Loggable
15
+
16
+ # The pattern for matching a tag opening
17
+ TAG_OPEN = /[\[<]\?/
18
+
19
+ # The pattern for matching a tag.
20
+ TAG_PATTERN = %r{
21
+ (?<tagstart>#{TAG_OPEN}) # Tag opening: either <? or [?
22
+ (?<tagname>[a-z]\w*) # The name of the tag
23
+ (?:\s+ # At least once whitespace character between the tagname and body
24
+ (?<body>.+?) # The body of the tag
25
+ )?
26
+ \s*
27
+ (?<tagend>\?[\]>]) # Tag closing: either ?] or ?>
28
+ }x
29
+
30
+ # Valid tagends by tagstart
31
+ MATCHING_BRACKETS = {
32
+ '<?' => '?>',
33
+ '[?' => '?]',
34
+ }
35
+
36
+ # Default values for parser configuration options.
37
+ DEFAULT_OPTIONS = {
38
+ :ignore_unknown_tags => true,
39
+ }
40
+
41
+
42
+ ### Create a new Inversion::Template::Parser with the specified config +options+.
43
+ def initialize( template, options={} )
44
+ @template = template
45
+ @options = DEFAULT_OPTIONS.merge( options )
46
+ end
47
+
48
+
49
+ ######
50
+ public
51
+ ######
52
+
53
+ # The parser's config options
54
+ attr_reader :options
55
+
56
+
57
+ ### Parse the given +source+ into one or more Inversion::Template::Nodes and return
58
+ ### it as an Array.
59
+ def parse( source, inherited_state=nil )
60
+ state = nil
61
+
62
+ if inherited_state
63
+ inherited_state.template = @template
64
+ state = inherited_state
65
+ else
66
+ state = Inversion::Template::Parser::State.new( @template, self.options )
67
+ end
68
+
69
+ self.log.debug "Starting parse of template source (%0.2fK, %s)" %
70
+ [ source.bytesize/1024.0, source.encoding ]
71
+
72
+ last_pos = last_linenum = last_colnum = 0
73
+ source.scan( TAG_PATTERN ) do |*|
74
+ match = Regexp.last_match
75
+ start_pos, end_pos = match.offset( 0 )
76
+ linenum = match.pre_match.count( "\n" ) + 1
77
+ colnum = match.pre_match.length - (match.pre_match.rindex("\n") || -1)
78
+
79
+ # Error on <?...?] and vice-versa.
80
+ unless match[:tagend] == MATCHING_BRACKETS[match[:tagstart]]
81
+ raise Inversion::ParseError,
82
+ "malformed tag %p: mismatched start and end brackets at line %d, column %d" %
83
+ [ match[0], linenum, colnum ]
84
+ end
85
+
86
+ # Check for nested tags
87
+ if match[0].index( TAG_OPEN, 2 )
88
+ raise Inversion::ParseError, "unclosed or nested tag %p at line %d, column %d" %
89
+ [ match[0], linenum, colnum ]
90
+ end
91
+
92
+ self.log.debug " found a tag at offset: %d (%p) (line %d, col %d)" %
93
+ [ start_pos, abbrevstring(match[0]), linenum, colnum ]
94
+
95
+ # If there were characters between the end of the last match and
96
+ # the beginning of the tag, create a text node with them
97
+ unless last_pos == start_pos
98
+ text = match.pre_match[ last_pos..-1 ]
99
+ self.log.debug " adding literal text node: %p" % [ abbrevstring(text) ]
100
+ state << Inversion::Template::TextNode.new( text, last_linenum, last_colnum )
101
+ end
102
+
103
+ self.log.debug " creating tag with tagname: %p, body: %p" %
104
+ [ match[:tagname], match[:body] ]
105
+
106
+ tag = Inversion::Template::Tag.create( match[:tagname], match[:body], linenum, colnum )
107
+ if tag.nil?
108
+ unless state.options[ :ignore_unknown_tags ]
109
+ raise Inversion::ParseError, "Unknown tag %p at line %d, column %d" %
110
+ [ match[:tagname], linenum, colnum ]
111
+ end
112
+
113
+ tag = Inversion::Template::TextNode.new( match[0], linenum, colnum )
114
+ end
115
+
116
+ self.log.debug " created tag: %p" % [ tag ]
117
+ state << tag
118
+
119
+ # Keep offsets for the next match
120
+ last_pos = end_pos
121
+ last_linenum = linenum + match[0].count( "\n" )
122
+ last_colnum = match[0].length - ( match[0].rindex("\n") || -1 )
123
+ end
124
+
125
+ # If there are any characters left over after the last tag
126
+ remainder = source[ last_pos..-1 ]
127
+ if remainder && !remainder.empty?
128
+ self.log.debug "Remainder after last tag: %p" % [ abbrevstring(remainder) ]
129
+
130
+ # Detect unclosed tags
131
+ if remainder.index( "<?" ) || remainder.index( "[?" )
132
+ raise Inversion::ParseError,
133
+ "unclosed tag after line %d, column %d" % [ last_linenum, last_colnum ]
134
+ end
135
+
136
+ # Add any remaining text as a text node
137
+ state << Inversion::Template::TextNode.new( remainder, last_linenum, last_colnum )
138
+ end
139
+
140
+ return state.tree
141
+ end
142
+
143
+
144
+ #######
145
+ private
146
+ #######
147
+
148
+ ### Return at most +length+ characters long from the given +string+, appending +ellipsis+
149
+ ### at the end if it was truncated.
150
+ def abbrevstring( string, length=30, ellipsis='…' )
151
+ return string if string.length < length
152
+ length -= ellipsis.length
153
+ return string[ 0, length ] + ellipsis
154
+ end
155
+
156
+
157
+ # Parse state object class. State objects keep track of where in the parse tree
158
+ # new nodes should be appended, and manages inclusion.
159
+ class State
160
+ include Inversion::Loggable
161
+
162
+ ### Create a new State object
163
+ def initialize( template, options={} )
164
+ @template = template
165
+ @options = options.dup
166
+ @tree = []
167
+ @node_stack = [ @tree ]
168
+ @include_stack = []
169
+ end
170
+
171
+
172
+ ### Copy constructor -- duplicate inner structures.
173
+ def initialize_copy( original )
174
+ @template = original.template
175
+ @options = original.options.dup
176
+ @tree = @tree.map( &:dup )
177
+ @node_stack = [ @tree ]
178
+ @include_stack = original.include_stack.dup
179
+ end
180
+
181
+
182
+ ######
183
+ public
184
+ ######
185
+
186
+ # The parse options in effect for this parse state
187
+ attr_reader :options
188
+
189
+ # The template object for this parser state
190
+ attr_accessor :template
191
+
192
+ # The stack of templates that have been loaded for this state; for loop detection.
193
+ attr_reader :include_stack
194
+
195
+ # The stack of containers
196
+ attr_reader :node_stack
197
+
198
+
199
+ ### Append operator: add nodes to the correct part of the parse tree.
200
+ def <<( node )
201
+ self.log.debug "Appending %p" % [ node ]
202
+
203
+ node.before_appending( self )
204
+ self.node_stack.last << node
205
+
206
+ if node.is_container?
207
+ # Containers get pushed onto the stack so they get appended to
208
+ self.node_stack.push( node )
209
+ else
210
+ # Container nodes' #after_appending gets called in #pop
211
+ node.after_appending( self )
212
+ end
213
+
214
+ return self
215
+ rescue Inversion::ParseError => err
216
+ raise err, "%s at %s" % [ err.message, node.location ]
217
+ end
218
+
219
+
220
+ ### Append another Array of nodes onto this state's node tree.
221
+ def append_tree( newtree )
222
+ newtree.each do |node|
223
+ self.node_stack.last << node
224
+ end
225
+ end
226
+
227
+
228
+ ### Returns the tree if it's well formed.
229
+ def tree
230
+ unless self.is_well_formed?
231
+ raise Inversion::ParseError, "Unclosed container tag: %s, from %s" %
232
+ [ self.node_stack.last.tagname, self.node_stack.last.location ]
233
+ end
234
+ return @tree
235
+ end
236
+
237
+
238
+ ### Check to see if all open tags have been closed.
239
+ def is_well_formed?
240
+ return self.node_stack.length == 1
241
+ end
242
+ alias_method :well_formed?, :is_well_formed?
243
+
244
+
245
+ ### Pop one level off of the node stack and return it.
246
+ def pop
247
+ closed_node = self.node_stack.pop
248
+
249
+ # If there's nothing on the node stack, we've popped the top-level
250
+ # Array, which means there wasn't an opening container.
251
+ raise Inversion::ParseError, "unbalanced end: no open tag" if
252
+ self.node_stack.empty?
253
+
254
+ closed_node.after_appending( self )
255
+
256
+ return closed_node
257
+ end
258
+
259
+
260
+ ### Return the node that is currently being appended to, or +nil+ if there aren't any
261
+ ### opened container nodes.
262
+ def current_node
263
+ return self.node_stack.last
264
+ end
265
+
266
+
267
+ ### Clear any parsed nodes from the state, leaving the options and include_stack intact.
268
+ def clear_nodes
269
+ @tree = []
270
+ @node_stack = [ @tree ]
271
+ end
272
+
273
+
274
+ ### Load a subtemplate from the specified +path+, checking for recursive-dependency.
275
+ def load_subtemplate( path )
276
+ if self.include_stack.include?( path )
277
+ stack_desc = ( self.include_stack + [path] ).join( ' --> ' )
278
+ msg = "Recursive load of %p detected: from %s" % [ path, stack_desc ]
279
+
280
+ self.log.error( msg )
281
+ raise Inversion::StackError, msg
282
+ end
283
+
284
+ self.log.debug "Include stack is: %p" % [ self.include_stack ]
285
+
286
+ substate = self.dup
287
+ substate.clear_nodes
288
+ substate.include_stack.push( path )
289
+
290
+ return Inversion::Template.load( path, substate, self.options )
291
+ end
292
+
293
+ end # class Inversion::Template::Parser::State
294
+
295
+ end # class Inversion::Template::Parser
296
+
297
+