radius 0.5.0 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,12 @@
1
+ = Roadmap
2
+
3
+ This is a prioritized roadmap for future releases:
4
+
5
+ 1. Clean up the current code base. [Done]
6
+
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. [Done]
9
+
10
+ 3. Create a simple DSL for defining contexts. [Done]
11
+
12
+ 4. Optimize for speed. Incorporate strscan?
@@ -0,0 +1,86 @@
1
+ require 'rubygems'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+ require 'rake/gempackagetask'
5
+
6
+ PKG_NAME = 'radius'
7
+ PKG_VERSION = '0.5.1'
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
+
19
+ task :default => :test
20
+
21
+ Rake::TestTask.new do |t|
22
+ t.pattern = 'test/**/*_test.rb'
23
+ end
24
+
25
+ Rake::RDocTask.new do |rd|
26
+ rd.title = 'Radius -- Powerful Tag-Based Templates'
27
+ rd.main = "README"
28
+ rd.rdoc_files.include("lib/**/*.rb")
29
+ rd.rdoc_files.include(RDOC_EXTRAS)
30
+ rd.rdoc_dir = 'doc'
31
+ end
32
+
33
+ spec = Gem::Specification.new do |s|
34
+ s.name = PKG_NAME
35
+ s.version = PKG_VERSION
36
+ s.summary = 'Powerful tag-based template system.'
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)."
38
+ s.homepage = 'http://radius.rubyforge.org'
39
+ s.rubyforge_project = RUBY_FORGE_PROJECT
40
+ s.platform = Gem::Platform::RUBY
41
+ s.requirements << 'none'
42
+ s.require_path = 'lib'
43
+ s.autorequire = 'radius'
44
+ s.has_rdoc = true
45
+ s.rdoc_options << '--title' << RDOC_TITLE << '--line-numbers' << '--main' << 'README'
46
+ s.extra_rdoc_files = RDOC_EXTRAS
47
+ files = FileList['**/*']
48
+ files.exclude 'doc'
49
+ files.exclude '**/._*'
50
+ s.files = files.to_a
51
+ end
52
+
53
+ Rake::GemPackageTask.new(spec) do |pkg|
54
+ pkg.need_zip = true
55
+ pkg.need_tar = true
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
+ end
@@ -0,0 +1,497 @@
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