radius 0.0.2 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +12 -0
- data/QUICKSTART +232 -45
- data/README +61 -15
- data/ROADMAP +4 -10
- data/Rakefile +50 -8
- data/lib/radius.rb +415 -75
- data/test/radius_test.rb +263 -113
- metadata +3 -4
- data/DSL-SPEC +0 -151
data/ROADMAP
CHANGED
@@ -2,17 +2,11 @@
|
|
2
2
|
|
3
3
|
This is a prioritized roadmap for future releases:
|
4
4
|
|
5
|
-
1. Clean up the current code base.
|
5
|
+
1. Clean up the current code base. [Done]
|
6
6
|
|
7
7
|
2. Add support for multi-level contexts: tags should be able to be
|
8
|
-
defined to only be valid within other sets of tags.
|
8
|
+
defined to only be valid within other sets of tags. [Done]
|
9
9
|
|
10
|
-
3. Create a simple DSL for defining contexts.
|
11
|
-
about this in the past. This thread on Ruby-Talk defined part of it:
|
10
|
+
3. Create a simple DSL for defining contexts. [Done]
|
12
11
|
|
13
|
-
|
14
|
-
|
15
|
-
Update: See link:files/DSL-SPEC.html for a fuller explanation of how
|
16
|
-
the DSL might behave.
|
17
|
-
|
18
|
-
4. Optimize for speed. Incorporate strscan?
|
12
|
+
4. Optimize for speed. Incorporate strscan?
|
data/Rakefile
CHANGED
@@ -3,30 +3,40 @@ require 'rake/testtask'
|
|
3
3
|
require 'rake/rdoctask'
|
4
4
|
require 'rake/gempackagetask'
|
5
5
|
|
6
|
+
PKG_NAME = 'radius'
|
7
|
+
PKG_VERSION = '0.5.0'
|
8
|
+
PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
|
9
|
+
RUBY_FORGE_PROJECT = PKG_NAME
|
10
|
+
RUBY_FORGE_USER = 'jlong'
|
11
|
+
|
12
|
+
RELEASE_NAME = PKG_VERSION
|
13
|
+
RUBY_FORGE_GROUPID = '1262'
|
14
|
+
RUBY_FORGE_PACKAGEID = '1538'
|
15
|
+
|
16
|
+
RDOC_TITLE = "Radius -- Powerful Tag-Based Templates"
|
17
|
+
RDOC_EXTRAS = ["README", "QUICKSTART", "ROADMAP", "CHANGELOG"]
|
18
|
+
|
6
19
|
task :default => :test
|
7
20
|
|
8
21
|
Rake::TestTask.new do |t|
|
9
22
|
t.pattern = 'test/**/*_test.rb'
|
10
23
|
end
|
11
24
|
|
12
|
-
RDOC_TITLE = "Radius -- Powerful Tag-Based Templates"
|
13
|
-
RDOC_EXTRAS = ["README", "QUICKSTART", "ROADMAP", "DSL-SPEC", "CHANGELOG"]
|
14
|
-
|
15
25
|
Rake::RDocTask.new do |rd|
|
16
26
|
rd.title = 'Radius -- Powerful Tag-Based Templates'
|
17
|
-
rd.main = "
|
27
|
+
rd.main = "README"
|
18
28
|
rd.rdoc_files.include("lib/**/*.rb")
|
19
29
|
rd.rdoc_files.include(RDOC_EXTRAS)
|
20
30
|
rd.rdoc_dir = 'doc'
|
21
31
|
end
|
22
32
|
|
23
33
|
spec = Gem::Specification.new do |s|
|
24
|
-
s.name =
|
25
|
-
s.
|
26
|
-
s.version = '0.0.2'
|
34
|
+
s.name = PKG_NAME
|
35
|
+
s.version = PKG_VERSION
|
27
36
|
s.summary = 'Powerful tag-based template system.'
|
28
37
|
s.description = "Radius is a small, but powerful tag-based template language for Ruby\nsimilar to the ones used in MovableType and TextPattern. It has tags\nsimilar to HTML or XML, but can be used to generate any form of plain\ntext (not just HTML)."
|
29
38
|
s.homepage = 'http://radius.rubyforge.org'
|
39
|
+
s.rubyforge_project = RUBY_FORGE_PROJECT
|
30
40
|
s.platform = Gem::Platform::RUBY
|
31
41
|
s.requirements << 'none'
|
32
42
|
s.require_path = 'lib'
|
@@ -43,4 +53,36 @@ end
|
|
43
53
|
Rake::GemPackageTask.new(spec) do |pkg|
|
44
54
|
pkg.need_zip = true
|
45
55
|
pkg.need_tar = true
|
46
|
-
end
|
56
|
+
end
|
57
|
+
|
58
|
+
desc "Uninstall Gem"
|
59
|
+
task :uninstall_gem do
|
60
|
+
sh "gem uninstall radius" rescue nil
|
61
|
+
end
|
62
|
+
|
63
|
+
desc "Build and install Gem from source"
|
64
|
+
task :install_gem => [:package, :uninstall_gem] do
|
65
|
+
dir = File.join(File.dirname(__FILE__), 'pkg')
|
66
|
+
chdir(dir) do
|
67
|
+
latest = Dir['radius-*.gem'].last
|
68
|
+
sh "gem install #{latest}"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# --- Ruby forge release manager by florian gross -------------------------------------------------
|
73
|
+
#
|
74
|
+
# task found in Tobias Luetke's library 'liquid'
|
75
|
+
#
|
76
|
+
|
77
|
+
desc "Publish the release files to RubyForge."
|
78
|
+
task :release => [:gem, :package] do
|
79
|
+
files = ["gem", "tgz", "zip"].map { |ext| "pkg/#{PKG_FILE_NAME}.#{ext}" }
|
80
|
+
|
81
|
+
system("rubyforge login --username #{RUBY_FORGE_USER}")
|
82
|
+
|
83
|
+
files.each do |file|
|
84
|
+
system("rubyforge add_release #{RUBY_FORGE_GROUPID} #{RUBY_FORGE_PACKAGEID} \"#{RELEASE_NAME}\" #{file}")
|
85
|
+
end
|
86
|
+
|
87
|
+
puts ">>>> done <<<<"
|
88
|
+
end
|
data/lib/radius.rb
CHANGED
@@ -1,3 +1,23 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2006, John W. Long
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
5
|
+
# software and associated documentation files (the "Software"), to deal in the Software
|
6
|
+
# without restriction, including without limitation the rights to use, copy, modify,
|
7
|
+
# merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
# permit persons to whom the Software is furnished to do so, subject to the following
|
9
|
+
# conditions:
|
10
|
+
#
|
11
|
+
# The above copyright notice and this permission notice shall be included in all copies
|
12
|
+
# or substantial portions of the Software.
|
13
|
+
#
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
15
|
+
# INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
16
|
+
# PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
17
|
+
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
18
|
+
# CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
19
|
+
# OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
20
|
+
#++
|
1
21
|
module Radius
|
2
22
|
# Abstract base class for all parsing errors.
|
3
23
|
class ParseError < StandardError
|
@@ -14,144 +34,464 @@ module Radius
|
|
14
34
|
|
15
35
|
# Occurs when Context#render_tag cannot find the specified tag on a Context.
|
16
36
|
class UndefinedTagError < ParseError
|
17
|
-
# Create a new
|
37
|
+
# Create a new UndefinedTagError object for +tag_name+.
|
18
38
|
def initialize(tag_name)
|
19
39
|
super("undefined tag `#{tag_name}'")
|
20
40
|
end
|
21
41
|
end
|
22
42
|
|
43
|
+
module TagDefinitions # :nodoc:
|
44
|
+
class TagFactory # :nodoc:
|
45
|
+
def initialize(context)
|
46
|
+
@context = context
|
47
|
+
end
|
48
|
+
|
49
|
+
def define_tag(name, options, &block)
|
50
|
+
options = prepare_options(name, options)
|
51
|
+
validate_params(name, options, &block)
|
52
|
+
construct_tag_set(name, options, &block)
|
53
|
+
expose_methods_as_tags(name, options)
|
54
|
+
end
|
55
|
+
|
56
|
+
protected
|
57
|
+
|
58
|
+
# Adds the tag definition to the context. Override in subclasses to add additional tags
|
59
|
+
# (child tags) when the tag is created.
|
60
|
+
def construct_tag_set(name, options, &block)
|
61
|
+
if block
|
62
|
+
@context.definitions[name.to_s] = block
|
63
|
+
else
|
64
|
+
lp = last_part(name)
|
65
|
+
@context.define_tag(name) do |tag|
|
66
|
+
if tag.single?
|
67
|
+
options[:for]
|
68
|
+
else
|
69
|
+
tag.locals.send("#{ lp }=", options[:for]) unless options[:for].nil?
|
70
|
+
tag.expand
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Normalizes options pased to tag definition. Override in decendants to preform
|
77
|
+
# additional normalization.
|
78
|
+
def prepare_options(name, options)
|
79
|
+
options = Util.symbolize_keys(options)
|
80
|
+
options[:expose] = expand_array_option(options[:expose])
|
81
|
+
object = options[:for]
|
82
|
+
options[:attributes] = object.respond_to?(:attributes) unless options.has_key? :attributes
|
83
|
+
options[:expose] += object.attributes.keys if options[:attributes]
|
84
|
+
options
|
85
|
+
end
|
86
|
+
|
87
|
+
# Validates parameters passed to tag definition. Override in decendants to add custom
|
88
|
+
# validations.
|
89
|
+
def validate_params(name, options, &block)
|
90
|
+
unless options.has_key? :for
|
91
|
+
raise ArgumentError.new("tag definition must contain a :for option or a block") unless block
|
92
|
+
raise ArgumentError.new("tag definition must contain a :for option when used with the :expose option") unless options[:expose].empty?
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Exposes the methods of an object as child tags.
|
97
|
+
def expose_methods_as_tags(name, options)
|
98
|
+
options[:expose].each do |method|
|
99
|
+
tag_name = "#{name}:#{method}"
|
100
|
+
lp = last_part(name)
|
101
|
+
@context.define_tag(tag_name) do |tag|
|
102
|
+
object = tag.locals.send(lp)
|
103
|
+
object.send(method)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
protected
|
109
|
+
|
110
|
+
def expand_array_option(value)
|
111
|
+
[*value].compact.map { |m| m.to_s.intern }
|
112
|
+
end
|
113
|
+
|
114
|
+
def last_part(name)
|
115
|
+
name.split(':').last
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
class DelegatingOpenStruct # :nodoc:
|
121
|
+
attr_accessor :object
|
122
|
+
|
123
|
+
def initialize(object = nil)
|
124
|
+
@object = object
|
125
|
+
@hash = {}
|
126
|
+
end
|
127
|
+
|
128
|
+
def method_missing(method, *args, &block)
|
129
|
+
symbol = (method.to_s =~ /^(.*?)=$/) ? $1.intern : method
|
130
|
+
if (0..1).include?(args.size)
|
131
|
+
if args.size == 1
|
132
|
+
@hash[symbol] = args.first
|
133
|
+
else
|
134
|
+
if @hash.has_key?(symbol)
|
135
|
+
@hash[symbol]
|
136
|
+
else
|
137
|
+
unless object.nil?
|
138
|
+
@object.send(method, *args, &block)
|
139
|
+
else
|
140
|
+
nil
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
else
|
145
|
+
super
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
23
150
|
#
|
24
|
-
#
|
25
|
-
#
|
151
|
+
# A tag binding is passed into each tag definition and contains helper methods for working
|
152
|
+
# with tags. Use it to gain access to the attributes that were passed to the tag, to
|
153
|
+
# render the tag contents, and to do other tasks.
|
154
|
+
#
|
155
|
+
class TagBinding
|
156
|
+
# The Context that the TagBinding is associated with. Used internally. Try not to use
|
157
|
+
# this object directly.
|
158
|
+
attr_reader :context
|
159
|
+
|
160
|
+
# The locals object for the current tag.
|
161
|
+
attr_reader :locals
|
162
|
+
|
163
|
+
# The name of the tag (as used in a template string).
|
164
|
+
attr_reader :name
|
165
|
+
|
166
|
+
# The attributes of the tag. Also aliased as TagBinding#attr.
|
167
|
+
attr_reader :attributes
|
168
|
+
alias :attr :attributes
|
169
|
+
|
170
|
+
# The render block. When called expands the contents of the tag. Use TagBinding#expand
|
171
|
+
# instead.
|
172
|
+
attr_reader :block
|
173
|
+
|
174
|
+
# Creates a new TagBinding object.
|
175
|
+
def initialize(context, locals, name, attributes, block)
|
176
|
+
@context, @locals, @name, @attributes, @block = context, locals, name, attributes, block
|
177
|
+
end
|
178
|
+
|
179
|
+
# Evaluates the current tag and returns the rendered contents.
|
180
|
+
def expand
|
181
|
+
double? ? block.call : ''
|
182
|
+
end
|
183
|
+
|
184
|
+
# Returns true if the current tag is a single tag.
|
185
|
+
def single?
|
186
|
+
block.nil?
|
187
|
+
end
|
188
|
+
|
189
|
+
# Returns true if the current tag is a container tag.
|
190
|
+
def double?
|
191
|
+
not single?
|
192
|
+
end
|
193
|
+
|
194
|
+
# The globals object from which all locals objects ultimately inherit their values.
|
195
|
+
def globals
|
196
|
+
@context.globals
|
197
|
+
end
|
198
|
+
|
199
|
+
# Returns a list of the way tags are nested around the current tag as a string.
|
200
|
+
def nesting
|
201
|
+
@context.current_nesting
|
202
|
+
end
|
203
|
+
|
204
|
+
# Fires off Context#tag_missing for the current tag.
|
205
|
+
def missing!
|
206
|
+
@context.tag_missing(name, attributes, &block)
|
207
|
+
end
|
208
|
+
|
209
|
+
# Renders the tag using the current context .
|
210
|
+
def render(tag, attributes = {}, &block)
|
211
|
+
@context.render_tag(tag, attributes, &block)
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
#
|
216
|
+
# A context contains the tag definitions which are available for use in a template.
|
217
|
+
# See the QUICKSTART[link:files/QUICKSTART.html] for a detailed explaination its
|
218
|
+
# usage.
|
26
219
|
#
|
27
220
|
class Context
|
28
|
-
#
|
29
|
-
|
30
|
-
|
31
|
-
attr_accessor :prefix
|
221
|
+
# A hash of tag definition blocks that define tags accessible on a Context.
|
222
|
+
attr_accessor :definitions # :nodoc:
|
223
|
+
attr_accessor :globals # :nodoc:
|
32
224
|
|
33
225
|
# Creates a new Context object.
|
34
|
-
def initialize
|
35
|
-
@
|
226
|
+
def initialize(&block)
|
227
|
+
@definitions = {}
|
228
|
+
@tag_binding_stack = []
|
229
|
+
@globals = DelegatingOpenStruct.new
|
230
|
+
with(&block) if block_given?
|
231
|
+
end
|
232
|
+
|
233
|
+
# Yeild an instance of self for tag definitions:
|
234
|
+
#
|
235
|
+
# context.with do |c|
|
236
|
+
# c.define_tag 'test' do
|
237
|
+
# 'test'
|
238
|
+
# end
|
239
|
+
# end
|
240
|
+
#
|
241
|
+
def with
|
242
|
+
yield self
|
243
|
+
self
|
36
244
|
end
|
37
245
|
|
246
|
+
# Creates a tag definition on a context. Several options are available to you
|
247
|
+
# when creating a tag:
|
248
|
+
#
|
249
|
+
# +for+:: Specifies an object that the tag is in reference to. This is
|
250
|
+
# applicable when a block is not passed to the tag, or when the
|
251
|
+
# +expose+ option is also used.
|
252
|
+
#
|
253
|
+
# +expose+:: Specifies that child tags should be set for each of the methods
|
254
|
+
# contained in this option. May be either a single symbol/string or
|
255
|
+
# an array of symbols/strings.
|
256
|
+
#
|
257
|
+
# +attributes+:: Specifies whether or not attributes should be exposed
|
258
|
+
# automatically. Useful for ActiveRecord objects. Boolean. Defaults
|
259
|
+
# to +true+.
|
260
|
+
#
|
261
|
+
def define_tag(name, options = {}, &block)
|
262
|
+
type = Util.impartial_hash_delete(options, :type).to_s
|
263
|
+
klass = Util.constantize('Radius::TagDefinitions::' + Util.camelize(type) + 'TagFactory') rescue raise(ArgumentError.new("Undefined type `#{type}' in options hash"))
|
264
|
+
klass.new(self).define_tag(name, options, &block)
|
265
|
+
end
|
266
|
+
|
38
267
|
# Returns the value of a rendered tag. Used internally by Parser#parse.
|
39
|
-
def render_tag(
|
40
|
-
|
41
|
-
|
42
|
-
send(symbol, attributes, &block)
|
268
|
+
def render_tag(name, attributes = {}, &block)
|
269
|
+
if name =~ /^(.+?):(.+)$/
|
270
|
+
render_tag($1) { render_tag($2, attributes, &block) }
|
43
271
|
else
|
44
|
-
|
272
|
+
tag_definition_block = @definitions[qualified_tag_name(name.to_s)]
|
273
|
+
if tag_definition_block
|
274
|
+
stack(name, attributes, block) do |tag|
|
275
|
+
tag_definition_block.call(tag).to_s
|
276
|
+
end
|
277
|
+
else
|
278
|
+
tag_missing(name, attributes, &block)
|
279
|
+
end
|
45
280
|
end
|
46
281
|
end
|
47
|
-
|
282
|
+
|
48
283
|
# Like method_missing for objects, but fired when a tag is undefined.
|
49
284
|
# Override in your own Context to change what happens when a tag is
|
50
285
|
# undefined. By default this method raises an UndefinedTagError.
|
51
|
-
def tag_missing(
|
52
|
-
raise UndefinedTagError.new(
|
286
|
+
def tag_missing(name, attributes, &block)
|
287
|
+
raise UndefinedTagError.new(name)
|
53
288
|
end
|
289
|
+
|
290
|
+
# Returns the state of the current render stack. Useful from inside
|
291
|
+
# a tag definition. Normally just use TagBinding#nesting.
|
292
|
+
def current_nesting
|
293
|
+
@tag_binding_stack.collect { |tag| tag.name }.join(':')
|
294
|
+
end
|
295
|
+
|
296
|
+
private
|
297
|
+
|
298
|
+
# A convienence method for managing the various parts of the
|
299
|
+
# tag binding stack.
|
300
|
+
def stack(name, attributes, block)
|
301
|
+
previous = @tag_binding_stack.last
|
302
|
+
previous_locals = previous.nil? ? @globals : previous.locals
|
303
|
+
locals = DelegatingOpenStruct.new(previous_locals)
|
304
|
+
binding = TagBinding.new(self, locals, name, attributes, block)
|
305
|
+
@tag_binding_stack.push(binding)
|
306
|
+
result = yield(binding)
|
307
|
+
@tag_binding_stack.pop
|
308
|
+
result
|
309
|
+
end
|
310
|
+
|
311
|
+
# Returns a fully qualified tag name based on state of the
|
312
|
+
# tag binding stack.
|
313
|
+
def qualified_tag_name(name)
|
314
|
+
nesting_parts = @tag_binding_stack.collect { |tag| tag.name }
|
315
|
+
nesting_parts << name unless nesting_parts.last == name
|
316
|
+
specific_name = nesting_parts.join(':') # specific_name always has the highest specificity
|
317
|
+
unless @definitions.has_key? specific_name
|
318
|
+
possible_matches = @definitions.keys.grep(/(^|:)#{name}$/)
|
319
|
+
specificity = possible_matches.inject({}) { |hash, tag| hash[numeric_specificity(tag, nesting_parts)] = tag; hash }
|
320
|
+
max = specificity.keys.max
|
321
|
+
if max != 0
|
322
|
+
specificity[max]
|
323
|
+
else
|
324
|
+
name
|
325
|
+
end
|
326
|
+
else
|
327
|
+
specific_name
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
# Returns the specificity for +tag_name+ at nesting defined
|
332
|
+
# by +nesting_parts+ as a number.
|
333
|
+
def numeric_specificity(tag_name, nesting_parts)
|
334
|
+
nesting_parts = nesting_parts.dup
|
335
|
+
name_parts = tag_name.split(':')
|
336
|
+
specificity = 0
|
337
|
+
value = 1
|
338
|
+
if nesting_parts.last == name_parts.last
|
339
|
+
while nesting_parts.size > 0
|
340
|
+
if nesting_parts.last == name_parts.last
|
341
|
+
specificity += value
|
342
|
+
name_parts.pop
|
343
|
+
end
|
344
|
+
nesting_parts.pop
|
345
|
+
value *= 0.1
|
346
|
+
end
|
347
|
+
specificity = 0 if (name_parts.size > 0)
|
348
|
+
end
|
349
|
+
specificity
|
350
|
+
end
|
54
351
|
end
|
55
|
-
|
56
|
-
class
|
352
|
+
|
353
|
+
class ParseTag # :nodoc:
|
57
354
|
def initialize(&b)
|
58
355
|
@block = b
|
59
356
|
end
|
60
|
-
|
357
|
+
|
61
358
|
def on_parse(&b)
|
62
359
|
@block = b
|
63
360
|
end
|
64
|
-
|
361
|
+
|
65
362
|
def to_s
|
66
363
|
@block.call(self)
|
67
364
|
end
|
68
365
|
end
|
69
366
|
|
70
|
-
class
|
367
|
+
class ParseContainerTag < ParseTag # :nodoc:
|
71
368
|
attr_accessor :name, :attributes, :contents
|
72
369
|
|
73
|
-
def initialize(name="", attributes={}, contents=[], &b)
|
370
|
+
def initialize(name = "", attributes = {}, contents = [], &b)
|
74
371
|
@name, @attributes, @contents = name, attributes, contents
|
75
372
|
super(&b)
|
76
373
|
end
|
77
374
|
end
|
78
|
-
|
375
|
+
|
79
376
|
#
|
80
|
-
# The Radius parser. Initialize a parser with
|
81
|
-
# how tags should be expanded.
|
377
|
+
# The Radius parser. Initialize a parser with a Context object that
|
378
|
+
# defines how tags should be expanded. See the QUICKSTART[link:files/QUICKSTART.html]
|
379
|
+
# for a detailed explaination of its usage.
|
82
380
|
#
|
83
381
|
class Parser
|
84
382
|
# The Context object used to expand template tags.
|
85
383
|
attr_accessor :context
|
86
384
|
|
385
|
+
# The string that prefixes all tags that are expanded by a parser
|
386
|
+
# (the part in the tag name before the first colon).
|
387
|
+
attr_accessor :tag_prefix
|
388
|
+
|
87
389
|
# Creates a new parser object initialized with a Context.
|
88
|
-
def initialize(context = Context.new)
|
390
|
+
def initialize(context = Context.new, options = {})
|
391
|
+
if context.kind_of?(Hash) and options.empty?
|
392
|
+
options = context
|
393
|
+
context = options[:context] || options['context'] || Context.new
|
394
|
+
end
|
395
|
+
options = Util.symbolize_keys(options)
|
89
396
|
@context = context
|
397
|
+
@tag_prefix = options[:tag_prefix]
|
90
398
|
end
|
91
399
|
|
92
|
-
#
|
400
|
+
# Parses string for tags, expands them, and returns the result.
|
93
401
|
def parse(string)
|
94
|
-
@stack = [
|
402
|
+
@stack = [ParseContainerTag.new { |t| t.contents.to_s }]
|
95
403
|
pre_parse(string)
|
96
404
|
@stack.last.to_s
|
97
405
|
end
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
406
|
+
|
407
|
+
protected
|
408
|
+
|
409
|
+
def pre_parse(text)
|
410
|
+
re = %r{<#{@tag_prefix}:([\w:]+?)(\s+(?:\w+\s*=\s*(["']).*?\3\s*)*|)>|</#{@tag_prefix}:([\w:]+?)\s*>}
|
411
|
+
if md = re.match(text)
|
412
|
+
start_tag, attr, end_tag = $1, $2, $4
|
413
|
+
@stack.last.contents << ParseTag.new { parse_individual(md.pre_match) }
|
414
|
+
remaining = md.post_match
|
415
|
+
if start_tag
|
416
|
+
parse_start_tag(start_tag, attr, remaining)
|
417
|
+
else
|
418
|
+
parse_end_tag(end_tag, remaining)
|
419
|
+
end
|
107
420
|
else
|
108
|
-
|
421
|
+
if @stack.length == 1
|
422
|
+
@stack.last.contents << ParseTag.new { parse_individual(text) }
|
423
|
+
else
|
424
|
+
raise MissingEndTagError.new(@stack.last.name)
|
425
|
+
end
|
109
426
|
end
|
110
|
-
|
111
|
-
|
112
|
-
|
427
|
+
end
|
428
|
+
|
429
|
+
def parse_start_tag(start_tag, attr, remaining) # :nodoc:
|
430
|
+
@stack.push(ParseContainerTag.new(start_tag, parse_attributes(attr)))
|
431
|
+
pre_parse(remaining)
|
432
|
+
end
|
433
|
+
|
434
|
+
def parse_end_tag(end_tag, remaining) # :nodoc:
|
435
|
+
popped = @stack.pop
|
436
|
+
if popped.name == end_tag
|
437
|
+
popped.on_parse { |t| @context.render_tag(popped.name, popped.attributes) { t.contents.to_s } }
|
438
|
+
tag = @stack.last
|
439
|
+
tag.contents << popped
|
440
|
+
pre_parse(remaining)
|
113
441
|
else
|
114
|
-
raise MissingEndTagError.new(
|
442
|
+
raise MissingEndTagError.new(popped.name)
|
115
443
|
end
|
116
444
|
end
|
117
|
-
end
|
118
|
-
|
119
|
-
def parse_start_tag(start_tag, attr, remaining) # :nodoc:
|
120
|
-
@stack.push(ContainerTag.new(start_tag, parse_attributes(attr)))
|
121
|
-
pre_parse(remaining)
|
122
|
-
end
|
123
445
|
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
446
|
+
def parse_individual(text) # :nodoc:
|
447
|
+
re = %r{<#{@tag_prefix}:([\w:]+?)(\s+(?:\w+\s*=\s*(["']).*?\3\s*)*|)/>}
|
448
|
+
if md = re.match(text)
|
449
|
+
attr = parse_attributes($2)
|
450
|
+
replace = @context.render_tag($1, attr)
|
451
|
+
md.pre_match + replace + parse_individual(md.post_match)
|
452
|
+
else
|
453
|
+
text || ''
|
454
|
+
end
|
455
|
+
end
|
456
|
+
|
457
|
+
def parse_attributes(text) # :nodoc:
|
458
|
+
attr = {}
|
459
|
+
re = /(\w+?)\s*=\s*('|")(.*?)\2/
|
460
|
+
while md = re.match(text)
|
461
|
+
attr[$1] = $3
|
462
|
+
text = md.post_match
|
463
|
+
end
|
464
|
+
attr
|
465
|
+
end
|
466
|
+
end
|
467
|
+
|
468
|
+
module Util # :nodoc:
|
469
|
+
def self.symbolize_keys(hash)
|
470
|
+
new_hash = {}
|
471
|
+
hash.keys.each do |k|
|
472
|
+
new_hash[k.to_s.intern] = hash[k]
|
133
473
|
end
|
474
|
+
new_hash
|
134
475
|
end
|
135
476
|
|
136
|
-
def
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
else
|
143
|
-
text || ''
|
144
|
-
end
|
477
|
+
def self.impartial_hash_delete(hash, key)
|
478
|
+
string = key.to_s
|
479
|
+
symbol = string.intern
|
480
|
+
value1 = hash.delete(symbol)
|
481
|
+
value2 = hash.delete(string)
|
482
|
+
value1 || value2
|
145
483
|
end
|
146
484
|
|
147
|
-
def
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
485
|
+
def self.constantize(camelized_string)
|
486
|
+
raise "invalid constant name `#{camelized_string}'" unless camelized_string.split('::').all? { |part| part =~ /^[A-Za-z]+$/ }
|
487
|
+
Object.module_eval(camelized_string)
|
488
|
+
end
|
489
|
+
|
490
|
+
def self.camelize(underscored_string)
|
491
|
+
string = ''
|
492
|
+
underscored_string.split('_').each { |part| string << part.capitalize }
|
493
|
+
string
|
155
494
|
end
|
156
495
|
end
|
496
|
+
|
157
497
|
end
|