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.
- data.tar.gz.sig +2 -0
- data/.gemtest +0 -0
- data/ChangeLog +836 -0
- data/History.md +4 -0
- data/Manifest.txt +74 -0
- data/README.rdoc +171 -0
- data/Rakefile +55 -0
- data/bin/inversion +276 -0
- data/lib/inversion.rb +98 -0
- data/lib/inversion/exceptions.rb +21 -0
- data/lib/inversion/mixins.rb +236 -0
- data/lib/inversion/monkeypatches.rb +20 -0
- data/lib/inversion/renderstate.rb +337 -0
- data/lib/inversion/sinatra.rb +35 -0
- data/lib/inversion/template.rb +250 -0
- data/lib/inversion/template/attrtag.rb +120 -0
- data/lib/inversion/template/calltag.rb +16 -0
- data/lib/inversion/template/codetag.rb +164 -0
- data/lib/inversion/template/commenttag.rb +54 -0
- data/lib/inversion/template/conditionaltag.rb +49 -0
- data/lib/inversion/template/configtag.rb +60 -0
- data/lib/inversion/template/containertag.rb +45 -0
- data/lib/inversion/template/elsetag.rb +62 -0
- data/lib/inversion/template/elsiftag.rb +49 -0
- data/lib/inversion/template/endtag.rb +55 -0
- data/lib/inversion/template/escapetag.rb +26 -0
- data/lib/inversion/template/fortag.rb +120 -0
- data/lib/inversion/template/iftag.rb +69 -0
- data/lib/inversion/template/importtag.rb +70 -0
- data/lib/inversion/template/includetag.rb +51 -0
- data/lib/inversion/template/node.rb +102 -0
- data/lib/inversion/template/parser.rb +297 -0
- data/lib/inversion/template/pptag.rb +28 -0
- data/lib/inversion/template/publishtag.rb +72 -0
- data/lib/inversion/template/subscribetag.rb +88 -0
- data/lib/inversion/template/tag.rb +150 -0
- data/lib/inversion/template/textnode.rb +43 -0
- data/lib/inversion/template/unlesstag.rb +60 -0
- data/lib/inversion/template/uriencodetag.rb +30 -0
- data/lib/inversion/template/yieldtag.rb +51 -0
- data/lib/inversion/tilt.rb +82 -0
- data/lib/inversion/utils.rb +235 -0
- data/spec/data/sinatra/hello.inversion +1 -0
- data/spec/inversion/mixins_spec.rb +177 -0
- data/spec/inversion/monkeypatches_spec.rb +35 -0
- data/spec/inversion/renderstate_spec.rb +291 -0
- data/spec/inversion/sinatra_spec.rb +59 -0
- data/spec/inversion/template/attrtag_spec.rb +216 -0
- data/spec/inversion/template/calltag_spec.rb +30 -0
- data/spec/inversion/template/codetag_spec.rb +51 -0
- data/spec/inversion/template/commenttag_spec.rb +84 -0
- data/spec/inversion/template/configtag_spec.rb +105 -0
- data/spec/inversion/template/containertag_spec.rb +54 -0
- data/spec/inversion/template/elsetag_spec.rb +105 -0
- data/spec/inversion/template/elsiftag_spec.rb +87 -0
- data/spec/inversion/template/endtag_spec.rb +78 -0
- data/spec/inversion/template/escapetag_spec.rb +59 -0
- data/spec/inversion/template/fortag_spec.rb +98 -0
- data/spec/inversion/template/iftag_spec.rb +241 -0
- data/spec/inversion/template/importtag_spec.rb +106 -0
- data/spec/inversion/template/includetag_spec.rb +108 -0
- data/spec/inversion/template/node_spec.rb +81 -0
- data/spec/inversion/template/parser_spec.rb +170 -0
- data/spec/inversion/template/pptag_spec.rb +51 -0
- data/spec/inversion/template/publishtag_spec.rb +69 -0
- data/spec/inversion/template/subscribetag_spec.rb +60 -0
- data/spec/inversion/template/tag_spec.rb +97 -0
- data/spec/inversion/template/textnode_spec.rb +86 -0
- data/spec/inversion/template/unlesstag_spec.rb +84 -0
- data/spec/inversion/template/uriencodetag_spec.rb +49 -0
- data/spec/inversion/template/yieldtag_spec.rb +54 -0
- data/spec/inversion/template_spec.rb +269 -0
- data/spec/inversion/tilt_spec.rb +47 -0
- data/spec/inversion_spec.rb +95 -0
- data/spec/lib/constants.rb +9 -0
- data/spec/lib/helpers.rb +160 -0
- metadata +316 -0
- 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
|
+
|