radius 0.5.1 → 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,497 +0,0 @@
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
- #++
21
- module Radius
22
- # Abstract base class for all parsing errors.
23
- class ParseError < StandardError
24
- end
25
-
26
- # Occurs when Parser cannot find an end tag for a given tag in a template or when
27
- # tags are miss-matched in a template.
28
- class MissingEndTagError < ParseError
29
- # Create a new MissingEndTagError object for +tag_name+.
30
- def initialize(tag_name)
31
- super("end tag not found for start tag `#{tag_name}'")
32
- end
33
- end
34
-
35
- # Occurs when Context#render_tag cannot find the specified tag on a Context.
36
- class UndefinedTagError < ParseError
37
- # Create a new UndefinedTagError object for +tag_name+.
38
- def initialize(tag_name)
39
- super("undefined tag `#{tag_name}'")
40
- end
41
- end
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
-
150
- #
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.
219
- #
220
- class Context
221
- # A hash of tag definition blocks that define tags accessible on a Context.
222
- attr_accessor :definitions # :nodoc:
223
- attr_accessor :globals # :nodoc:
224
-
225
- # Creates a new Context object.
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
244
- end
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
-
267
- # Returns the value of a rendered tag. Used internally by Parser#parse.
268
- def render_tag(name, attributes = {}, &block)
269
- if name =~ /^(.+?):(.+)$/
270
- render_tag($1) { render_tag($2, attributes, &block) }
271
- else
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
280
- end
281
- end
282
-
283
- # Like method_missing for objects, but fired when a tag is undefined.
284
- # Override in your own Context to change what happens when a tag is
285
- # undefined. By default this method raises an UndefinedTagError.
286
- def tag_missing(name, attributes, &block)
287
- raise UndefinedTagError.new(name)
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
351
- end
352
-
353
- class ParseTag # :nodoc:
354
- def initialize(&b)
355
- @block = b
356
- end
357
-
358
- def on_parse(&b)
359
- @block = b
360
- end
361
-
362
- def to_s
363
- @block.call(self)
364
- end
365
- end
366
-
367
- class ParseContainerTag < ParseTag # :nodoc:
368
- attr_accessor :name, :attributes, :contents
369
-
370
- def initialize(name = "", attributes = {}, contents = [], &b)
371
- @name, @attributes, @contents = name, attributes, contents
372
- super(&b)
373
- end
374
- end
375
-
376
- #
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.
380
- #
381
- class Parser
382
- # The Context object used to expand template tags.
383
- attr_accessor :context
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
-
389
- # Creates a new parser object initialized with a Context.
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)
396
- @context = context
397
- @tag_prefix = options[:tag_prefix]
398
- end
399
-
400
- # Parses string for tags, expands them, and returns the result.
401
- def parse(string)
402
- @stack = [ParseContainerTag.new { |t| t.contents.to_s }]
403
- pre_parse(string)
404
- @stack.last.to_s
405
- end
406
-
407
- protected
408
-
409
- def pre_parse(text) # :nodoc:
410
- re = %r{<#{@tag_prefix}:([\w:]+?)(\s+(?:\w+\s*=\s*(?:"[^"]*?"|'[^']*?')\s*)*|)>|</#{@tag_prefix}:([\w:]+?)\s*>}
411
- if md = re.match(text)
412
- start_tag, attr, end_tag = $1, $2, $3
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
420
- else
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
426
- end
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)
441
- else
442
- raise MissingEndTagError.new(popped.name)
443
- end
444
- end
445
-
446
- def parse_individual(text) # :nodoc:
447
- re = %r{<#{@tag_prefix}:([\w:]+?)(\s+(?:\w+\s*=\s*(?:"[^"]*?"|'[^']*?')\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]
473
- end
474
- new_hash
475
- end
476
-
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
483
- end
484
-
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
494
- end
495
- end
496
-
497
- end