radius 0.5.1 → 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
Binary file
data/CHANGELOG CHANGED
@@ -1,4 +1,12 @@
1
1
  = Change Log
2
+ === 0.6.1
3
+ * Fixed a problem with non-tags that have no prefix or tagname (see test_parse_chirpy_bird)
4
+
5
+ === 0.6.0
6
+ * Split radius.rb into multiple files.
7
+ * Port the really hairy regexes from Radius::Parser to a single Ragel machine.
8
+ * Added and refactored tests.
9
+ * Refactored Rakefile and other administrativia.
2
10
 
3
11
  === 0.5.1
4
12
  * Fixed a problem with parsing quotes where a single tag preceding a double tag would consume the start
@@ -0,0 +1,21 @@
1
+ CHANGELOG
2
+ Manifest.txt
3
+ QUICKSTART.rdoc
4
+ README.rdoc
5
+ Rakefile
6
+ lib/radius.rb
7
+ lib/radius/context.rb
8
+ lib/radius/delegating_open_struct.rb
9
+ lib/radius/error.rb
10
+ lib/radius/parse_tag.rb
11
+ lib/radius/parser.rb
12
+ lib/radius/parser/scan.rb
13
+ lib/radius/parser/scan.rl
14
+ lib/radius/tag_binding.rb
15
+ lib/radius/tag_definitions.rb
16
+ lib/radius/utility.rb
17
+ lib/radius/version.rb
18
+ tasks/scan.rake
19
+ test/context_test.rb
20
+ test/parser_test.rb
21
+ test/test_helper.rb
@@ -8,7 +8,7 @@ the tags that will be used in the template. This is actually quite simple:
8
8
 
9
9
  require 'radius'
10
10
 
11
- context = Context.new
11
+ context = Radius::Context.new
12
12
  context.define_tag "hello" do |tag|
13
13
  "Hello #{tag.attr['name'] || 'World'}!"
14
14
  end
@@ -53,7 +53,7 @@ With the code above your parser can easily handle Textile:
53
53
 
54
54
  This code will output:
55
55
 
56
- <h1>Hello <strong>World</strong>!</h1>
56
+ <h1>Hello <b>World</b>!</h1>
57
57
 
58
58
 
59
59
  == Nested Tags
@@ -61,7 +61,7 @@ This code will output:
61
61
  But wait!--it gets better. Because container tags can manipulate the content they contain
62
62
  you can use them to iterate over collections:
63
63
 
64
- context = Context.new
64
+ context = Radius::Context.new
65
65
 
66
66
  context.define_tag "stooge" do |tag|
67
67
  content = ''
@@ -72,7 +72,7 @@ you can use them to iterate over collections:
72
72
  content
73
73
  end
74
74
 
75
- context.define_tag "stooge:name" do
75
+ context.define_tag "stooge:name" do |tag|
76
76
  tag.locals.name
77
77
  end
78
78
 
@@ -126,7 +126,7 @@ So far this doesn't save you a whole lot of typing, but suppose you want to expo
126
126
  methods that are on that object? You could do this:
127
127
 
128
128
  context.define_tag "user", :for => user, :expose => [ :name, :age, :email ]
129
-
129
+
130
130
  This will add a total of four tags to the context. One for the <tt>user</tt> variable, and
131
131
  one for each of the three methods listed in the +expose+ clause. You could now get the user's
132
132
  name inside your template like this:
@@ -206,6 +206,7 @@ accessible to all tags:
206
206
  context.define_tag "inc" do |tag|
207
207
  tag.globals.count ||= 0
208
208
  tag.globals.count += 1
209
+ ""
209
210
  end
210
211
 
211
212
  context.define_tag "count" do |tag|
@@ -215,20 +216,18 @@ accessible to all tags:
215
216
  TagBinding#locals mirrors the variables that are in TagBinding#globals, but allows child
216
217
  tags to redefine variables. This is valuable when defining context sensitive tags:
217
218
 
218
- require 'radius'
219
-
220
219
  class Person
221
220
  attr_accessor :name, :friend
222
221
  def initialize(name)
223
222
  @name = name
224
223
  end
225
224
  end
226
-
225
+
227
226
  jack = Person.new('Jack')
228
227
  jill = Person.new('Jill')
229
228
  jack.friend = jill
230
229
  jill.friend = jack
231
-
230
+
232
231
  context = Radius::Context.new do |c|
233
232
  c.define_tag "jack" do |tag|
234
233
  tag.locals.person = jack
@@ -252,9 +251,9 @@ tags to redefine variables. This is valuable when defining context sensitive tag
252
251
  parser.parse('<r:jack:name />') #=> "Jack"
253
252
  parser.parse('<r:jill:name />') #=> "Jill"
254
253
  parser.parse('<r:jill:friend:name />') #=> "Jack"
255
- parser.parse('<r:jill:friend:friend:name />') #=> "Jack"
254
+ parser.parse('<r:jack:friend:friend:name />') #=> "Jack"
256
255
  parser.parse('<r:jill><r:friend:name /> and <r:name /></r:jill>') #=> "Jack and Jill"
257
- parser.parse('<r:name />') # raises an UndefinedTagError exception
256
+ parser.parse('<r:name />') # raises a Radius::UndefinedTagError exception
258
257
 
259
258
  Notice how TagBinding#locals enables intelligent nesting. "<r:jill:name />" evaluates to
260
259
  "Jill", but "<r:jill:friend:name />" evaluates to "Jack". Locals loose scope as soon as
@@ -264,7 +263,7 @@ The final line in the example above demonstrates that calling "<r:name />" raise
264
263
  TagMissing error. This is because of the way the name tag was defined:
265
264
 
266
265
  tag.locals.person.name rescue tag.missing!
267
-
266
+
268
267
  If person is not defined on locals it will return nil. Calling #name on nil would normally
269
268
  raise a NoMethodError exception, but because of the 'rescue' clause the TagBinding#missing!
270
269
  method is called which fires off Context#tag_missing. By default Context#tag_missing raises
@@ -5,13 +5,13 @@ used in MovableType[http://www.movabletype.org] and TextPattern[http://www.textp
5
5
  It uses tags similar to XML, but can be used to generate any form of plain text (HTML, e-mail,
6
6
  etc...).
7
7
 
8
- == Example
8
+ == Usage
9
9
 
10
10
  With Radius, it is extremely easy to create custom tags and parse them. Here's a small
11
11
  example:
12
12
 
13
13
  require 'radius'
14
-
14
+
15
15
  # Define tags on a context that will be available to a template:
16
16
  context = Radius::Context.new do |c|
17
17
  c.define_tag 'hello' do
@@ -30,7 +30,7 @@ example:
30
30
 
31
31
  # Parse tags and output the result
32
32
  puts parser.parse(%{A small example:\n<r:repeat times="3">* <r:hello />!\n</r:repeat>})
33
-
33
+
34
34
  Output:
35
35
 
36
36
  A small example:
@@ -39,16 +39,19 @@ Output:
39
39
  * Hello world!
40
40
 
41
41
 
42
- = Quick Start
42
+ == Quick Start
43
43
 
44
- Read the QUICKSTART[link:files/QUICKSTART.html] to get up and running fast with Radius.
44
+ Read the QUICKSTART file to get up and running with Radius.
45
45
 
46
46
 
47
- == Download
48
-
49
- The latest version of Radius can be found on RubyForge:
50
-
51
- http://rubyforge.org/projects/radius/
47
+ = Requirements
48
+
49
+ Radius does not have any external requirements for using the library in your
50
+ own programs.
51
+
52
+ Ragel is required to create the ruby parser from the Ragel specification,
53
+ and both Ragel and Graphviz are required to draw the state graph for the
54
+ parser.
52
55
 
53
56
 
54
57
  == Installation
@@ -64,7 +67,7 @@ You can also install Radius by copying lib/radius.rb into the Ruby load path.
64
67
 
65
68
  Radius is free software and may be redistributed under the terms of the MIT-LICENSE:
66
69
 
67
- Copyright (c) 2006, John W. Long
70
+ Copyright (c) 2006-2009, John W. Long
68
71
 
69
72
  Permission is hereby granted, free of charge, to any person obtaining a copy of this
70
73
  software and associated documentation files (the "Software"), to deal in the Software
@@ -83,15 +86,33 @@ OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
83
86
  DEALINGS IN THE SOFTWARE.
84
87
 
85
88
 
86
- == The Future
89
+ == Roadmap
90
+
91
+ This is a prioritized roadmap for future releases:
92
+
93
+ 1. Clean up the current code base. [Done]
94
+
95
+ 2. Add support for multi-level contexts: tags should be able to be
96
+ defined to only be valid within other sets of tags. [Done]
97
+
98
+ 3. Create a simple DSL for defining contexts. [Done]
99
+
100
+ 4. Optimize for speed, modify scan.rl to emit C.
101
+
102
+
103
+ == Development
104
+
105
+ The latest version of Radius can be found on RubyForge:
106
+
107
+ http://rubyforge.org/projects/radius
87
108
 
88
- Radius is nearing completion, but is still very much in the development stages. Take a look
89
- at the ROADMAP[link:files/ROADMAP.html] to see where we want to go.
109
+ Experimental and development versions of Radius can be found on Github:
110
+
111
+ http://github.com/jlong/radius
90
112
 
91
- If you are interested in helping with the development of Radiant, contact me and we'll talk.
113
+ If you are interested in helping with the development of Radiant, feel free to
114
+ fork the project on GitHub and send me a pull request.
92
115
 
93
- Enjoy!
94
116
 
95
- --
96
117
  John Long ::
97
118
  http://wiseheartdesign.com
data/Rakefile CHANGED
@@ -1,86 +1,31 @@
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
1
+ %w(rubygems rake rake/clean fileutils newgem rubigen hoe).each { |f| require f }
2
+
3
+ require 'lib/radius/version'
4
+
5
+ # Generate all the Rake tasks
6
+ # Run 'rake -T' to see list of generated tasks (from gem root directory)
7
+ Hoe.spec 'radius' do |p|
8
+ p.version = Radius::Version.to_s
9
+ p.url = "http://radius.rubyforge.org"
10
+ p.developer('John W. Long', 'me@johnwlong.com')
11
+ p.author = [
12
+ "John W. Long (me@johnwlong.com)",
13
+ "David Chelimsky (dchelimsky@gmail.com)",
14
+ "Bryce Kerley (bkerley@brycekerley.net)"
15
+ ]
16
+ p.changes = p.paragraphs_of("CHANGELOG", 1..2).join("\n\n")
17
+ p.rubyforge_name = p.name
18
+ p.extra_dev_deps = [
19
+ ['newgem', ">= #{::Newgem::VERSION}"]
20
+ ]
21
+ p.readme_file = 'README.rdoc'
22
+ p.extra_rdoc_files |= %w(README.rdoc QUICKSTART.rdoc)
23
+ p.clean_globs |= %w(**/.DS_Store tmp *.log) # Remove these files on "rake clean"
24
+ path = (p.rubyforge_name == p.name) ? p.rubyforge_name : "\#{p.rubyforge_name}/\#{p.name}"
25
+ p.remote_rdoc_dir = File.join(path.gsub(/^#{p.rubyforge_name}\/?/,''), 'rdoc')
26
+ p.rsync_args = '-av --delete --ignore-errors'
27
+ p.test_globs = "test/**/*_test.rb"
28
+ end
29
+
30
+ require 'newgem/tasks' # load /tasks/*.rake
31
+ Dir['tasks/**/*.rake'].each { |t| load t }
@@ -1,497 +1,10 @@
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
1
+ require 'radius/version'
2
+ require 'radius/error'
3
+ require 'radius/tag_definitions'
4
+ require 'radius/delegating_open_struct'
5
+ require 'radius/tag_binding'
6
+ require 'radius/context'
7
+ require 'radius/parse_tag'
8
+ require 'radius/parser/scan'
9
+ require 'radius/parser'
10
+ require 'radius/utility'