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,35 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'inversion/tilt'
|
5
|
+
require 'sinatra'
|
6
|
+
require 'sinatra/base'
|
7
|
+
rescue LoadError => err
|
8
|
+
warn "Couldn't load Inversion Sinatra support: %s: %s" % [ err.class.name, err.message ]
|
9
|
+
end
|
10
|
+
|
11
|
+
# Add support for Tilt (https://github.com/rtomayko/tilt) if it's already been loaded.
|
12
|
+
if defined?( ::Sinatra ) # :nodoc:
|
13
|
+
|
14
|
+
# A mixin to add Inversion support to Sinatra::Base
|
15
|
+
module Inversion::SinatraTemplateHelpers
|
16
|
+
|
17
|
+
### Add an 'inversion' helper method to Sinatra's template DSL:
|
18
|
+
###
|
19
|
+
### get '/' do
|
20
|
+
### inversion :company_directory, :locals => { :people => People.all }
|
21
|
+
### end
|
22
|
+
def inversion( template, options={}, locals={} )
|
23
|
+
render :inversion, template, options, locals
|
24
|
+
end
|
25
|
+
|
26
|
+
end # Inversion::SinatraTemplateHelpers
|
27
|
+
|
28
|
+
# Inject Inversion helpers as a mixin
|
29
|
+
# :stopdoc:
|
30
|
+
class Sinatra::Base
|
31
|
+
include Inversion::SinatraTemplateHelpers
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
@@ -0,0 +1,250 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# vim: set noet nosta sw=4 ts=4 :
|
3
|
+
|
4
|
+
require 'pathname'
|
5
|
+
require 'inversion' unless defined?( Inversion )
|
6
|
+
|
7
|
+
# Load the Configurability library if it's installed
|
8
|
+
begin
|
9
|
+
require 'configurability'
|
10
|
+
require 'configurability/config'
|
11
|
+
rescue LoadError
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
# The main template class. Instances of this class are created by parsing template
|
16
|
+
# source and combining the resulting node tree with a set of attributes that
|
17
|
+
# can be used to populate it when rendered.
|
18
|
+
class Inversion::Template
|
19
|
+
include Inversion::Loggable
|
20
|
+
|
21
|
+
# Configurability support -- load template configuration from the 'templates' section
|
22
|
+
# of the config.
|
23
|
+
if defined?( Configurability )
|
24
|
+
extend Configurability
|
25
|
+
config_key :templates if respond_to?( :config_key )
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
# Load subordinate classes
|
30
|
+
require 'inversion/template/parser'
|
31
|
+
require 'inversion/template/node'
|
32
|
+
require 'inversion/template/tag'
|
33
|
+
require 'inversion/renderstate'
|
34
|
+
|
35
|
+
|
36
|
+
# Valid actions for 'on_render_error'
|
37
|
+
VALID_ERROR_ACTIONS = [
|
38
|
+
:ignore,
|
39
|
+
:comment,
|
40
|
+
:propagate,
|
41
|
+
]
|
42
|
+
|
43
|
+
### Default config values
|
44
|
+
DEFAULT_CONFIG = {
|
45
|
+
:ignore_unknown_tags => true,
|
46
|
+
:on_render_error => :comment,
|
47
|
+
:debugging_comments => false,
|
48
|
+
:comment_start => '<!-- ',
|
49
|
+
:comment_end => ' -->',
|
50
|
+
:template_paths => [],
|
51
|
+
:escape_format => :html,
|
52
|
+
:strip_tag_lines => true,
|
53
|
+
}
|
54
|
+
|
55
|
+
|
56
|
+
### Global config
|
57
|
+
@config = DEFAULT_CONFIG.dup
|
58
|
+
class << self; attr_accessor :config; end
|
59
|
+
|
60
|
+
|
61
|
+
### Configure the templating system.
|
62
|
+
def self::configure( config )
|
63
|
+
Inversion.log.debug "Merging config %p with current config %p" % [ config, self.config ]
|
64
|
+
self.config = self.config.merge( config )
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
### Read a template object from the specified +path+.
|
69
|
+
def self::load( path, parsestate=nil, opts={} )
|
70
|
+
|
71
|
+
# Shift the options hash over if there isn't a parse state
|
72
|
+
if parsestate.is_a?( Hash )
|
73
|
+
opts = parsestate
|
74
|
+
parsestate = nil
|
75
|
+
end
|
76
|
+
|
77
|
+
tmpl = nil
|
78
|
+
path = Pathname( path )
|
79
|
+
template_paths = self.config[:template_paths] + [ Dir.pwd ]
|
80
|
+
|
81
|
+
# Unrestricted template location.
|
82
|
+
if path.absolute?
|
83
|
+
tmpl = path
|
84
|
+
|
85
|
+
# Template files searched under paths specified in 'template_paths', then
|
86
|
+
# the current working directory. First match wins.
|
87
|
+
else
|
88
|
+
tmpl = template_paths.collect {|dir| Pathname(dir) + path }.find do |fullpath|
|
89
|
+
fullpath.exist?
|
90
|
+
end
|
91
|
+
|
92
|
+
raise RuntimeError, "Unable to find template %p within configured paths %p" %
|
93
|
+
[ path.to_s, template_paths ] if tmpl.nil?
|
94
|
+
end
|
95
|
+
|
96
|
+
# We trust files read from disk
|
97
|
+
source = tmpl.read
|
98
|
+
source.untaint
|
99
|
+
|
100
|
+
# Load the instance and set the path to the source
|
101
|
+
template = self.new( source, parsestate, opts )
|
102
|
+
template.source_file = tmpl.expand_path
|
103
|
+
|
104
|
+
return template
|
105
|
+
end
|
106
|
+
|
107
|
+
|
108
|
+
### Create a new Inversion:Template with the given +source+.
|
109
|
+
def initialize( source, parsestate=nil, opts={} )
|
110
|
+
if parsestate.is_a?( Hash )
|
111
|
+
self.log.debug "Shifting template options: %p" % [ parsestate ]
|
112
|
+
opts = parsestate
|
113
|
+
parsestate = nil
|
114
|
+
else
|
115
|
+
self.log.debug "Parse state is: %p" % [ parsestate ]
|
116
|
+
end
|
117
|
+
|
118
|
+
@source = source
|
119
|
+
@parser = Inversion::Template::Parser.new( self, opts )
|
120
|
+
@node_tree = []
|
121
|
+
@options = self.class.config.merge( opts )
|
122
|
+
|
123
|
+
@attributes = {}
|
124
|
+
@source_file = nil
|
125
|
+
|
126
|
+
# Now parse the template source into a tree of nodes and pre-generate accessors
|
127
|
+
# for any attributes defined by them
|
128
|
+
@node_tree = @parser.parse( source, parsestate )
|
129
|
+
self.define_attribute_accessors
|
130
|
+
end
|
131
|
+
|
132
|
+
|
133
|
+
|
134
|
+
######
|
135
|
+
public
|
136
|
+
######
|
137
|
+
|
138
|
+
attr_reader :source
|
139
|
+
|
140
|
+
attr_accessor :source_file
|
141
|
+
|
142
|
+
attr_reader :attributes
|
143
|
+
|
144
|
+
attr_reader :options
|
145
|
+
|
146
|
+
attr_reader :node_tree
|
147
|
+
|
148
|
+
|
149
|
+
### Render the template, optionally passing a render state (if, for example, the
|
150
|
+
### template is being rendered inside another template).
|
151
|
+
def render( parentstate=nil, &block )
|
152
|
+
self.log.info "rendering template 0x%08x" % [ self.object_id/2 ]
|
153
|
+
state = Inversion::RenderState.new( parentstate, self.attributes, self.options, &block )
|
154
|
+
|
155
|
+
# Pre-render hook
|
156
|
+
self.walk_tree {|node| node.before_rendering(state) }
|
157
|
+
|
158
|
+
self.log.debug " rendering node tree: %p" % [ @node_tree ]
|
159
|
+
self.walk_tree {|node| state << node }
|
160
|
+
|
161
|
+
# Post-render hook
|
162
|
+
self.walk_tree {|node| node.after_rendering(state) }
|
163
|
+
|
164
|
+
self.log.info " done rendering template 0x%08x" % [ self.object_id/2 ]
|
165
|
+
return state.to_s
|
166
|
+
end
|
167
|
+
alias_method :to_s, :render
|
168
|
+
|
169
|
+
|
170
|
+
### Return a human-readable representation of the template object suitable
|
171
|
+
### for debugging.
|
172
|
+
def inspect
|
173
|
+
return "#<%s:%08x (loaded from %s) attributes: %p, node_tree: %p, options: %p>" % [
|
174
|
+
self.class.name,
|
175
|
+
self.object_id / 2,
|
176
|
+
self.source_file ? self.source_file : "memory",
|
177
|
+
self.attributes,
|
178
|
+
self.node_tree.map(&:as_comment_body),
|
179
|
+
self.options,
|
180
|
+
]
|
181
|
+
end
|
182
|
+
|
183
|
+
|
184
|
+
#########
|
185
|
+
protected
|
186
|
+
#########
|
187
|
+
|
188
|
+
### Proxy method: handle attribute readers/writers for attributes that aren't yet
|
189
|
+
### defined.
|
190
|
+
def method_missing( sym, *args, &block )
|
191
|
+
return super unless sym.to_s =~ /^([a-z]\w+)=?$/i
|
192
|
+
attribute = $1
|
193
|
+
self.install_accessors( attribute )
|
194
|
+
|
195
|
+
# Call the new method via #method to avoid a method_missing loop.
|
196
|
+
return self.method( sym ).call( *args, &block )
|
197
|
+
end
|
198
|
+
|
199
|
+
|
200
|
+
### Walk the template's node tree, yielding each node in turn to the given block.
|
201
|
+
def walk_tree( nodes=@node_tree, &block )
|
202
|
+
nodes.each do |node|
|
203
|
+
yield( node )
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
|
208
|
+
### Search for identifiers in the template's node tree and declare an accessor
|
209
|
+
### for each one that's found.
|
210
|
+
def define_attribute_accessors
|
211
|
+
self.walk_tree do |node|
|
212
|
+
self.add_attributes_from_node( node )
|
213
|
+
end
|
214
|
+
|
215
|
+
self.attributes.each do |key, _|
|
216
|
+
self.install_accessors( key )
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
|
221
|
+
### Add attributes for the given +node+'s identifiers.
|
222
|
+
def add_attributes_from_node( node )
|
223
|
+
if node.respond_to?( :identifiers )
|
224
|
+
node.identifiers.each do |id|
|
225
|
+
next if @attributes.key?( id.to_sym )
|
226
|
+
@attributes[ id.to_sym ] = nil
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
|
232
|
+
### Install reader and writer methods for the attribute associated with the specified +key+.
|
233
|
+
def install_accessors( key )
|
234
|
+
reader, writer = self.make_attribute_accessors( key )
|
235
|
+
|
236
|
+
self.singleton_class.send( :define_method, key, &reader )
|
237
|
+
self.singleton_class.send( :define_method, "#{key}=", &writer )
|
238
|
+
end
|
239
|
+
|
240
|
+
|
241
|
+
### Make method bodies
|
242
|
+
def make_attribute_accessors( key )
|
243
|
+
key = key.to_sym
|
244
|
+
reader = lambda { self.attributes[key] }
|
245
|
+
writer = lambda {|newval| self.attributes[key] = newval }
|
246
|
+
|
247
|
+
return reader, writer
|
248
|
+
end
|
249
|
+
end # class Inversion::Template
|
250
|
+
|
@@ -0,0 +1,120 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# vim: set noet nosta sw=4 ts=4 :
|
3
|
+
|
4
|
+
require 'inversion/template/codetag'
|
5
|
+
|
6
|
+
# Inversion attribute tag.
|
7
|
+
#
|
8
|
+
# Attribute tags add an accessor to a template like 'attr_accessor' does for Ruby classes.
|
9
|
+
#
|
10
|
+
# == Syntax
|
11
|
+
#
|
12
|
+
# <?attr foo ?>
|
13
|
+
# <?attr "%0.2f" % foo ?>
|
14
|
+
#
|
15
|
+
class Inversion::Template::AttrTag < Inversion::Template::CodeTag
|
16
|
+
|
17
|
+
# <?attr foo ?>
|
18
|
+
tag_pattern '$(ident)' do |tag, match|
|
19
|
+
tag.send( :log ).debug " Identifier is: %p" % [ match.string(1) ]
|
20
|
+
tag.name = match.string( 1 ).untaint.to_sym
|
21
|
+
end
|
22
|
+
|
23
|
+
# <?attr "%s" % foo ?>
|
24
|
+
tag_pattern 'tstring_beg $(tstring_content) tstring_end sp* $(op) sp* $(ident)' do |tag, match|
|
25
|
+
op = match.string( 2 )
|
26
|
+
raise Inversion::ParseError, "expected '%%', got %p instead" % [ op ] unless op == '%'
|
27
|
+
|
28
|
+
tag.format = match.string( 1 )
|
29
|
+
tag.name = match.string( 3 ).untaint.to_sym
|
30
|
+
end
|
31
|
+
|
32
|
+
# <?attr foo.methodchain ?>
|
33
|
+
tag_pattern '$(ident) $( .+ )' do |tag, match|
|
34
|
+
tag.name = match.string( 1 ).untaint.to_sym
|
35
|
+
tag.methodchain = match.string( 2 )
|
36
|
+
end
|
37
|
+
|
38
|
+
# <?attr "%s" % foo.methodchain ?>
|
39
|
+
tag_pattern 'tstring_beg $(tstring_content) tstring_end sp* $(op) sp* $(ident) $( .+ )' do |tag, match|
|
40
|
+
op = match.string( 2 )
|
41
|
+
raise Inversion::ParseError, "expected '%%', got %p instead" % [ op ] unless op == '%'
|
42
|
+
|
43
|
+
tag.format = match.string( 1 )
|
44
|
+
tag.name = match.string( 3 ).untaint.to_sym
|
45
|
+
tag.methodchain = match.string( 4 )
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
|
50
|
+
### Create a new AttrTag with the given +name+, which should be a valid
|
51
|
+
### Ruby identifier. The +linenum+ and +colnum+ should be the line and column of
|
52
|
+
### the tag in the template source, if available.
|
53
|
+
def initialize( body, linenum=nil, colnum=nil )
|
54
|
+
@name = nil
|
55
|
+
@format = nil
|
56
|
+
@methodchain = nil
|
57
|
+
|
58
|
+
super
|
59
|
+
|
60
|
+
# Add an identifier for the tag name
|
61
|
+
self.identifiers << self.name.untaint.to_sym
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
######
|
66
|
+
public
|
67
|
+
######
|
68
|
+
|
69
|
+
# the name of the attribute
|
70
|
+
attr_accessor :name
|
71
|
+
|
72
|
+
# the format string used to format the attribute in the template (if
|
73
|
+
# one was declared)
|
74
|
+
attr_accessor :format
|
75
|
+
|
76
|
+
# the chain of methods that should be called (if any).
|
77
|
+
attr_accessor :methodchain
|
78
|
+
|
79
|
+
|
80
|
+
### Render the tag attributes of the specified +render_state+ and return them.
|
81
|
+
def render( render_state )
|
82
|
+
self.log.debug "Rendering %p with state: %p" % [ self, render_state ]
|
83
|
+
|
84
|
+
value = nil
|
85
|
+
attribute = render_state.attributes[ self.name.to_sym ]
|
86
|
+
self.log.debug " initial attribute: %p" % [ attribute ]
|
87
|
+
|
88
|
+
# Evaluate the method chain (if there is one) against the attribute
|
89
|
+
if self.methodchain
|
90
|
+
methodchain = "self" + self.methodchain
|
91
|
+
self.log.debug " evaling methodchain: %p on: %p" % [ methodchain, attribute ]
|
92
|
+
value = attribute.instance_eval( methodchain )
|
93
|
+
else
|
94
|
+
value = attribute
|
95
|
+
end
|
96
|
+
self.log.debug " evaluated value: %p" % [ value ]
|
97
|
+
|
98
|
+
return value unless value
|
99
|
+
|
100
|
+
# Apply the format if there is one
|
101
|
+
if self.format
|
102
|
+
return self.format % value
|
103
|
+
else
|
104
|
+
return value
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
|
109
|
+
### Render the tag as the body of a comment, suitable for template debugging.
|
110
|
+
def as_comment_body
|
111
|
+
comment = "%s: { template.%s" % [ self.tagname, self.name ]
|
112
|
+
comment << self.methodchain if self.methodchain
|
113
|
+
comment << " }"
|
114
|
+
comment << " with format: %p" % [ self.format ] if self.format
|
115
|
+
|
116
|
+
return comment
|
117
|
+
end
|
118
|
+
|
119
|
+
end # class Inversion::Template::AttrTag
|
120
|
+
|
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# vim: set noet nosta sw=4 ts=4 :
|
3
|
+
|
4
|
+
require 'inversion/template/attrtag'
|
5
|
+
|
6
|
+
# Inversion call tag.
|
7
|
+
#
|
8
|
+
# This just exists to make 'call' an alias for 'attr'.
|
9
|
+
#
|
10
|
+
# == Syntax
|
11
|
+
#
|
12
|
+
# <?call foo.bar ?>
|
13
|
+
# <?call "%0.2f" % foo.bar ?>
|
14
|
+
|
15
|
+
class Inversion::Template::CallTag < Inversion::Template::AttrTag; end
|
16
|
+
|
@@ -0,0 +1,164 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# vim: set noet nosta sw=4 ts=4 :
|
3
|
+
|
4
|
+
require 'ripper'
|
5
|
+
require 'inversion/template/tag'
|
6
|
+
|
7
|
+
# The base class for Inversion tags that parse the body section of the tag using
|
8
|
+
# a Ruby parser.
|
9
|
+
#
|
10
|
+
# It provides a +tag_pattern+ declarative method that is used to specify a pattern of
|
11
|
+
# tokens to match, and a block for handling tag instances that match the pattern.
|
12
|
+
#
|
13
|
+
# class Inversion::Template::MyTag < Inversion::Template::CodeTag
|
14
|
+
#
|
15
|
+
# # Match a tag that looks like: <?my "string of stuff" ?>
|
16
|
+
# tag_pattern 'tstring_beg $(tstring_content) tstring_end' do |tag, match|
|
17
|
+
# tag.string = match.string( 1 )
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# The tokens in the +tag_pattern+ are Ruby token names used by the parser. If you're creating
|
23
|
+
# your own tag, you can dump the tokens for a particular snippet using the 'inversion'
|
24
|
+
# command-line tool that comes with the gem:
|
25
|
+
#
|
26
|
+
# $ inversion tagtokens 'attr.dump! {|thing| thing.length }'
|
27
|
+
# ident<"attr"> period<"."> ident<"dump!"> sp<" "> lbrace<"{"> op<"|"> \
|
28
|
+
# ident<"thing"> op<"|"> sp<" "> ident<"thing"> period<"."> \
|
29
|
+
# ident<"length"> sp<" "> rbrace<"}">
|
30
|
+
#
|
31
|
+
# :TODO: Finish the tag_pattern docs: placeholders, regex limitations, etc.
|
32
|
+
#
|
33
|
+
class Inversion::Template::CodeTag < Inversion::Template::Tag
|
34
|
+
include Inversion::Loggable,
|
35
|
+
Inversion::AbstractClass
|
36
|
+
|
37
|
+
|
38
|
+
### A subclass of Ripper::TokenPattern that binds matches to the beginning and
|
39
|
+
### end of the matched string.
|
40
|
+
class TokenPattern < Ripper::TokenPattern
|
41
|
+
|
42
|
+
# the token pattern's source string
|
43
|
+
attr_reader :source
|
44
|
+
|
45
|
+
#########
|
46
|
+
protected
|
47
|
+
#########
|
48
|
+
|
49
|
+
### Compile the token pattern into a Regexp
|
50
|
+
def compile( pattern )
|
51
|
+
if m = /[^\w\s$()\[\]{}?*+\.]/.match( pattern )
|
52
|
+
raise Ripper::TokenPattern::CompileError,
|
53
|
+
"invalid char in pattern: #{m[0].inspect}"
|
54
|
+
end
|
55
|
+
|
56
|
+
buf = '^'
|
57
|
+
pattern.scan( /(?:\w+|\$\(|[()\[\]\{\}?*+\.]+)/ ) do |tok|
|
58
|
+
case tok
|
59
|
+
when /\w/
|
60
|
+
buf << map_token( tok )
|
61
|
+
when '$('
|
62
|
+
buf << '('
|
63
|
+
when '('
|
64
|
+
buf << '(?:'
|
65
|
+
when /[?*\[\])\.]/
|
66
|
+
buf << tok
|
67
|
+
else
|
68
|
+
raise ScriptError, "invalid token in pattern: %p" % [ tok ]
|
69
|
+
end
|
70
|
+
end
|
71
|
+
buf << '$'
|
72
|
+
|
73
|
+
Regexp.compile( buf )
|
74
|
+
rescue RegexpError => err
|
75
|
+
raise Ripper::TokenPattern::CompileError, err.message
|
76
|
+
end
|
77
|
+
|
78
|
+
end # class TokenPattern
|
79
|
+
|
80
|
+
|
81
|
+
#################################################################
|
82
|
+
### C L A S S M E T H O D S
|
83
|
+
#################################################################
|
84
|
+
|
85
|
+
### Return the tag patterns for this class, or those of its superclass
|
86
|
+
### if it doesn't override them.
|
87
|
+
def self::tag_patterns
|
88
|
+
return @tag_patterns if defined?( @tag_patterns )
|
89
|
+
return self.superclass.tag_patterns
|
90
|
+
end
|
91
|
+
|
92
|
+
|
93
|
+
### Declare a +token_pattern+ for tag bodies along with a +callback+ that will
|
94
|
+
### be called when a tag matching the pattern is instantiated.
|
95
|
+
def self::tag_pattern( token_pattern, &callback ) #:yield:
|
96
|
+
pattern = TokenPattern.compile( token_pattern )
|
97
|
+
@tag_patterns = [] unless defined?( @tag_patterns )
|
98
|
+
@tag_patterns << [ pattern, callback ]
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
#################################################################
|
103
|
+
### I N S T A N C E M E T H O D S
|
104
|
+
#################################################################
|
105
|
+
|
106
|
+
### Initialize a new tag that expects Ruby code in its +body+. Calls the
|
107
|
+
### tag's #parse_pi_body method with the specified +body+.
|
108
|
+
def initialize( body, linenum=nil, colnum=nil ) # :notnew:
|
109
|
+
super
|
110
|
+
|
111
|
+
@body = body.strip
|
112
|
+
@identifiers = []
|
113
|
+
@matched_pattern = self.match_tag_pattern( body )
|
114
|
+
end
|
115
|
+
|
116
|
+
|
117
|
+
######
|
118
|
+
public
|
119
|
+
######
|
120
|
+
|
121
|
+
# the body of the tag
|
122
|
+
attr_reader :body
|
123
|
+
|
124
|
+
# the identifiers in the code contained in the tag
|
125
|
+
attr_reader :identifiers
|
126
|
+
|
127
|
+
|
128
|
+
### Render the node as text.
|
129
|
+
pure_virtual :render
|
130
|
+
|
131
|
+
|
132
|
+
|
133
|
+
#########
|
134
|
+
protected
|
135
|
+
#########
|
136
|
+
|
137
|
+
### Match the given +body+ against one of the tag's tag patterns, calling the
|
138
|
+
### block associated with the first one that matches and returning the matching
|
139
|
+
### pattern.
|
140
|
+
def match_tag_pattern( body )
|
141
|
+
|
142
|
+
self.class.tag_patterns.each do |tp, callback|
|
143
|
+
if match = tp.match( body.strip )
|
144
|
+
self.log.debug "Matched tag pattern: %p, match is: %p" % [ tp, match ]
|
145
|
+
callback.call( self, match )
|
146
|
+
return tp
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
self.log.error "Failed to match %p with %d patterns." %
|
151
|
+
[ body, self.class.tag_patterns.length ]
|
152
|
+
|
153
|
+
valid_patterns = self.class.tag_patterns.map( &:first ).map( &:source ).join( "\n ")
|
154
|
+
tokenized_src = Ripper.lex( body ).collect do |tok|
|
155
|
+
self.log.debug " lexed token: #{tok.inspect}"
|
156
|
+
"%s<%s>" % [ tok[1].to_s[3..-1], tok[2] ]
|
157
|
+
end.join(' ')
|
158
|
+
|
159
|
+
raise Inversion::ParseError, "malformed %s: expected one of:\n %s\ngot:\n %s" %
|
160
|
+
[ self.tagname, valid_patterns, tokenized_src ]
|
161
|
+
end
|
162
|
+
|
163
|
+
end # class Inversion::Template::CodeTag
|
164
|
+
|