radius 0.0.2 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (9) hide show
  1. data/CHANGELOG +12 -0
  2. data/QUICKSTART +232 -45
  3. data/README +61 -15
  4. data/ROADMAP +4 -10
  5. data/Rakefile +50 -8
  6. data/lib/radius.rb +415 -75
  7. data/test/radius_test.rb +263 -113
  8. metadata +3 -4
  9. 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. I had done some thinking
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
- http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/127640/
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 = "Radius"
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 = 'radius'
25
- s.rubyforge_project = 'radius'
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
@@ -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 MissingEndTagError object for +tag_name+.
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
- # An abstract class for creating a Context. A context defines the tags that
25
- # are available for use in a template.
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
- # The prefix attribute controls the string of text that is helps the parser
29
- # identify template tags. By default this attribute is set to "radius", but
30
- # you may want to override this when creating your own contexts.
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
- @prefix = 'radius'
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(tag, attributes = {}, &block)
40
- symbol = tag.to_s.intern
41
- if respond_to?(symbol) and method(symbol).arity == 1
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
- tag_missing(tag, attributes, &block)
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(tag, attributes, &block)
52
- raise UndefinedTagError.new(tag)
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 Tag # :nodoc:
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 ContainerTag < Tag # :nodoc:
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 the Context object that defines
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
- # Parse string for tags, expand them, and return the result.
400
+ # Parses string for tags, expands them, and returns the result.
93
401
  def parse(string)
94
- @stack = [ContainerTag.new { |t| t.contents.to_s }]
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
- def pre_parse(text) # :nodoc:
100
- re = %r{<#{@context.prefix}:(\w+?)(?:\s+?([^/>]*?)|)>|</#{@context.prefix}:(\w+?)\s*?>}
101
- if md = re.match(text)
102
- start_tag, attr, end_tag = $1, $2, $3
103
- @stack.last.contents << Tag.new { parse_individual(md.pre_match) }
104
- remaining = md.post_match
105
- if start_tag
106
- parse_start_tag(start_tag, attr, remaining)
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
- parse_end_tag(end_tag, remaining)
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
- else
111
- if @stack.length == 1
112
- @stack.last.contents << Tag.new { parse_individual(text) }
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(@stack.last.name)
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
- def parse_end_tag(end_tag, remaining) # :nodoc:
125
- popped = @stack.pop
126
- if popped.name == end_tag
127
- popped.on_parse { |t| @context.render_tag(popped.name, popped.attributes) { t.contents.to_s } }
128
- tag = @stack.last
129
- tag.contents << popped
130
- pre_parse(remaining)
131
- else
132
- raise MissingEndTagError.new(popped.name)
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 parse_individual(text) # :nodoc:
137
- re = /<#{@context.prefix}:(\w+?)\s+?(.*?)\s*?\/>/
138
- if md = re.match(text)
139
- attr = parse_attributes($2)
140
- replace = @context.render_tag($1, attr)
141
- md.pre_match + replace + parse_individual(md.post_match)
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 parse_attributes(text) # :nodoc:
148
- attr = {}
149
- re = /(\w+?)\s*=\s*('|")(.*?)\2/
150
- while md = re.match(text)
151
- attr[$1] = $3
152
- text = md.post_match
153
- end
154
- attr
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