radius 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/CHANGELOG CHANGED
@@ -1,5 +1,9 @@
1
1
  = Change Log
2
2
 
3
+ === 0.5.1
4
+ * Fixed a problem with parsing quotes where a single tag preceding a double tag would consume the start
5
+ tag of the double tag if both contained attributes.
6
+
3
7
  === 0.5.0
4
8
  * Created a DSL for tag definitions (introducing a DSL makes this version of Radiant incompatible with
5
9
  the last). The DSL has the following features:
data/Rakefile CHANGED
@@ -4,7 +4,7 @@ require 'rake/rdoctask'
4
4
  require 'rake/gempackagetask'
5
5
 
6
6
  PKG_NAME = 'radius'
7
- PKG_VERSION = '0.5.0'
7
+ PKG_VERSION = '0.5.1'
8
8
  PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
9
9
  RUBY_FORGE_PROJECT = PKG_NAME
10
10
  RUBY_FORGE_USER = 'jlong'
@@ -83,6 +83,4 @@ task :release => [:gem, :package] do
83
83
  files.each do |file|
84
84
  system("rubyforge add_release #{RUBY_FORGE_GROUPID} #{RUBY_FORGE_PACKAGEID} \"#{RELEASE_NAME}\" #{file}")
85
85
  end
86
-
87
- puts ">>>> done <<<<"
88
86
  end
@@ -406,10 +406,10 @@ module Radius
406
406
 
407
407
  protected
408
408
 
409
- def pre_parse(text)
410
- re = %r{<#{@tag_prefix}:([\w:]+?)(\s+(?:\w+\s*=\s*(["']).*?\3\s*)*|)>|</#{@tag_prefix}:([\w:]+?)\s*>}
409
+ def pre_parse(text) # :nodoc:
410
+ re = %r{<#{@tag_prefix}:([\w:]+?)(\s+(?:\w+\s*=\s*(?:"[^"]*?"|'[^']*?')\s*)*|)>|</#{@tag_prefix}:([\w:]+?)\s*>}
411
411
  if md = re.match(text)
412
- start_tag, attr, end_tag = $1, $2, $4
412
+ start_tag, attr, end_tag = $1, $2, $3
413
413
  @stack.last.contents << ParseTag.new { parse_individual(md.pre_match) }
414
414
  remaining = md.post_match
415
415
  if start_tag
@@ -444,7 +444,7 @@ module Radius
444
444
  end
445
445
 
446
446
  def parse_individual(text) # :nodoc:
447
- re = %r{<#{@tag_prefix}:([\w:]+?)(\s+(?:\w+\s*=\s*(["']).*?\3\s*)*|)/>}
447
+ re = %r{<#{@tag_prefix}:([\w:]+?)(\s+(?:\w+\s*=\s*(?:"[^"]*?"|'[^']*?')\s*)*|)/>}
448
448
  if md = re.match(text)
449
449
  attr = parse_attributes($2)
450
450
  replace = @context.render_tag($1, attr)
Binary file
Binary file
Binary file
@@ -0,0 +1,25 @@
1
+ = Change Log
2
+
3
+ === 0.5.1
4
+ * Fixed a problem with parsing quotes where a single tag preceding a double tag would consume the start
5
+ tag of the double tag if both contained attributes.
6
+
7
+ === 0.5.0
8
+ * Created a DSL for tag definitions (introducing a DSL makes this version of Radiant incompatible with
9
+ the last). The DSL has the following features:
10
+ - full support for nested tags
11
+ - global and local tag variables
12
+ - Contexts can now be defined dynamically (instead of being subclassed)
13
+ - see the QUICKSTART for more info
14
+ * Many refactorings of the library and unit tests.
15
+ * Changed the license to the MIT-LICENSE.
16
+ * Updated documentation to reflect the changes.
17
+ * Updated the version number to reflect the maturity of the code base.
18
+
19
+ === 0.0.2
20
+ * Refactored Parser to use Context#render_tag instead of #send when rendering tags defined on a Context.
21
+ * UndefinedTagError is now thrown when Parser tries to render a tag which doesn't exist on a Context.
22
+ * Added Context#tag_missing which works like method_method missing on Object, but is tag specific.
23
+
24
+ === 0.0.1
25
+ * First release.
@@ -0,0 +1,323 @@
1
+ = Radius Quick Start
2
+
3
+
4
+ == Defining Tags
5
+
6
+ Before you can parse a template with Radius you need to create a Context object which defines
7
+ the tags that will be used in the template. This is actually quite simple:
8
+
9
+ require 'radius'
10
+
11
+ context = Context.new
12
+ context.define_tag "hello" do |tag|
13
+ "Hello #{tag.attr['name'] || 'World'}!"
14
+ end
15
+
16
+ Once you have defined a context you can easily create a Parser:
17
+
18
+ parser = Radius::Parser.new(context)
19
+ puts parser.parse('<p><radius:hello /></p>')
20
+ puts parser.parse('<p><radius:hello name="John" /></p>')
21
+
22
+ This code will output:
23
+
24
+ <p>Hello World!</p>
25
+ <p>Hello John!</p>
26
+
27
+ Note how you can pass attributes from the template to the context using the attributes hash.
28
+ Above, the first tag that was parsed didn't have a name attribute so the code in the +hello+
29
+ tag definition uses "World" instead. The second time the tag is parsed the name attribute is
30
+ set to "John" which is used to create the string "Hello John!". Tags that do not follow this
31
+ rule will be treated as if they were undefined (like normal methods).
32
+
33
+
34
+ == Container Tags
35
+
36
+ Radius also allows you to define "container" tags. That is, tags that contain content and
37
+ that may optionally manipulate it in some way. For example, if you have RedCloth installed
38
+ you could define another tag to parse and create Textile output:
39
+
40
+ require 'redcloth'
41
+
42
+ context.define_tag "textile" do |tag|
43
+ contents = tag.expand
44
+ RedCloth.new(contents).to_html
45
+ end
46
+
47
+ (The code <tt>tag.expand</tt> above returns the contents of the template between the start and end
48
+ tags.)
49
+
50
+ With the code above your parser can easily handle Textile:
51
+
52
+ parser.parse('<radius:textile>h1. Hello **World**!</radius:textile>')
53
+
54
+ This code will output:
55
+
56
+ <h1>Hello <strong>World</strong>!</h1>
57
+
58
+
59
+ == Nested Tags
60
+
61
+ But wait!--it gets better. Because container tags can manipulate the content they contain
62
+ you can use them to iterate over collections:
63
+
64
+ context = Context.new
65
+
66
+ context.define_tag "stooge" do |tag|
67
+ content = ''
68
+ ["Larry", "Moe", "Curly"].each do |name|
69
+ tag.locals.name = name
70
+ content << tag.expand
71
+ end
72
+ content
73
+ end
74
+
75
+ context.define_tag "stooge:name" do
76
+ tag.locals.name
77
+ end
78
+
79
+ parser = Radius::Parser.new(context)
80
+
81
+ template = <<-TEMPLATE
82
+ <ul>
83
+ <radius:stooge>
84
+ <li><radius:name /></li>
85
+ </radius:stooge>
86
+ </ul>
87
+ TEMPLATE
88
+
89
+ puts parser.parse(template)
90
+
91
+ This code will output:
92
+
93
+ <ul>
94
+
95
+ <li>Larry</li>
96
+
97
+ <li>Moe</li>
98
+
99
+ <li>Curly</li>
100
+
101
+ </ul>
102
+
103
+ Note how the definition for the +name+ tag is defined. Because "name" is prefixed
104
+ with "stooge:" the +name+ tag cannot appear outside the +stooge+ tag. Had it been defined
105
+ simply as "name" it would be valid anywhere, even outside the +stooge+ tag (which was
106
+ not what we wanted). Using the colon operator you can define tags with any amount of
107
+ nesting.
108
+
109
+
110
+ == Exposing Objects to Templates
111
+
112
+ During normal operation, you will often want to expose certain objects to your templates.
113
+ Writing the tags to do this all by hand would be cumbersome of Radius did not provide
114
+ several mechanisms to make this easier. The first is a way of exposing objects as tags
115
+ on the context object. To expose an object simply call the +define_tag+
116
+ method with the +for+ option:
117
+
118
+ context.define_tag "count", :for => 1
119
+
120
+ This would expose the object <tt>1</tt> to the template as the +count+ tag. It's basically the
121
+ equivalent of writing:
122
+
123
+ context.define_tag("count") { 1 }
124
+
125
+ So far this doesn't save you a whole lot of typing, but suppose you want to expose certain
126
+ methods that are on that object? You could do this:
127
+
128
+ context.define_tag "user", :for => user, :expose => [ :name, :age, :email ]
129
+
130
+ This will add a total of four tags to the context. One for the <tt>user</tt> variable, and
131
+ one for each of the three methods listed in the +expose+ clause. You could now get the user's
132
+ name inside your template like this:
133
+
134
+ <radius:user><radius:name /></radius:user>
135
+
136
+ If "John" was the value stored in <tt>user.name</tt> the template would render as "John".
137
+
138
+
139
+ == Tag Shorthand
140
+
141
+ In the example above we made reference to <tt>user.name</tt> in our template by using the
142
+ following code:
143
+
144
+ <radius:user><radius:name /></radius:user>
145
+
146
+ There is a much easer way to refer to the <tt>user.name</tt> variable. Use the colon operator
147
+ to "scope" the reference to <tt>name</tt>:
148
+
149
+ <radius:user:name />
150
+
151
+ Radius allows you to use this shortcut for all tags.
152
+
153
+
154
+ == Changing the Tag Prefix
155
+
156
+ By default, all Radius tags must begin with "radius". You can change this by altering the
157
+ tag_prefix attribute on a Parser. For example:
158
+
159
+ parser = Radius::Parser.new(context, :tag_prefix => 'r')
160
+
161
+ Now, when parsing templates with +parser+, Radius will require that every tag begin with "r"
162
+ instead of "radius".
163
+
164
+
165
+ == Custom Behavior for Undefined Tags
166
+
167
+ Context#tag_missing behaves much like Object#method_missing only it allows you to define
168
+ specific behavior for when a tag is not defined on a Context. For example:
169
+
170
+ class LazyContext < Radius::Context
171
+ def tag_missing(tag, attr, &block)
172
+ "<strong>ERROR: Undefined tag `#{tag}' with attributes #{attr.inspect}</strong>"
173
+ end
174
+ end
175
+
176
+ parser = Radius::Parser.new(LazyContext.new, :tag_prefix => 'lazy')
177
+ puts parser.parse('<lazy:weird value="true" />')
178
+
179
+ This will output:
180
+
181
+ <strong>ERROR: Undefined tag `weird' with attributes {"value"=>"true"}</strong>
182
+
183
+ Normally, when the Radius Parser encounters an undefined tag for a Context it raises an
184
+ UndefinedTagError, but since we have defined #tag_missing on LazyContext the Parser now
185
+ outputs a nicely formated error message when we parse a string that does not contain a
186
+ valid tag.
187
+
188
+
189
+ == Tag Bindings
190
+
191
+ Radius passes a TagBinding into the block of the Context#define_tag method. The tag
192
+ binding is useful for a number of tasks. A tag binding has an #expand instance method
193
+ which processes a tag's contents and returns the result. It also has a #attr method
194
+ which returns a hash of the attributes that were passed into the tag. TagBinding also
195
+ contains the TagBinding#single? and TagBinding#double? methods which return true or false
196
+ based on wether the tag is a container tag or not. More about the methods which are
197
+ available on tag bindings can be found on the Radius::TagBinding documentation page.
198
+
199
+
200
+ == Tag Binding Locals, Globals, and Context Sensitive Tags
201
+
202
+ A TagBinding also contains two OpenStruct-like objects which are useful when developing
203
+ tags. TagBinding#globals is useful for storing variables which you would like to be
204
+ accessible to all tags:
205
+
206
+ context.define_tag "inc" do |tag|
207
+ tag.globals.count ||= 0
208
+ tag.globals.count += 1
209
+ end
210
+
211
+ context.define_tag "count" do |tag|
212
+ tag.globals.count || 0
213
+ end
214
+
215
+ TagBinding#locals mirrors the variables that are in TagBinding#globals, but allows child
216
+ tags to redefine variables. This is valuable when defining context sensitive tags:
217
+
218
+ require 'radius'
219
+
220
+ class Person
221
+ attr_accessor :name, :friend
222
+ def initialize(name)
223
+ @name = name
224
+ end
225
+ end
226
+
227
+ jack = Person.new('Jack')
228
+ jill = Person.new('Jill')
229
+ jack.friend = jill
230
+ jill.friend = jack
231
+
232
+ context = Radius::Context.new do |c|
233
+ c.define_tag "jack" do |tag|
234
+ tag.locals.person = jack
235
+ tag.expand
236
+ end
237
+ c.define_tag "jill" do |tag|
238
+ tag.locals.person = jill
239
+ tag.expand
240
+ end
241
+ c.define_tag "name" do |tag|
242
+ tag.locals.person.name rescue tag.missing!
243
+ end
244
+ c.define_tag "friend" do |tag|
245
+ tag.locals.person = tag.locals.person.friend rescue tag.missing!
246
+ tag.expand
247
+ end
248
+ end
249
+
250
+ parser = Radius::Parser.new(context, :tag_prefix => 'r')
251
+
252
+ parser.parse('<r:jack:name />') #=> "Jack"
253
+ parser.parse('<r:jill:name />') #=> "Jill"
254
+ parser.parse('<r:jill:friend:name />') #=> "Jack"
255
+ parser.parse('<r:jill:friend:friend:name />') #=> "Jack"
256
+ parser.parse('<r:jill><r:friend:name /> and <r:name /></r:jill>') #=> "Jack and Jill"
257
+ parser.parse('<r:name />') # raises an UndefinedTagError exception
258
+
259
+ Notice how TagBinding#locals enables intelligent nesting. "<r:jill:name />" evaluates to
260
+ "Jill", but "<r:jill:friend:name />" evaluates to "Jack". Locals loose scope as soon as
261
+ the tag they were defined in closes. Globals on the other hand, never loose scope.
262
+
263
+ The final line in the example above demonstrates that calling "<r:name />" raises a
264
+ TagMissing error. This is because of the way the name tag was defined:
265
+
266
+ tag.locals.person.name rescue tag.missing!
267
+
268
+ If person is not defined on locals it will return nil. Calling #name on nil would normally
269
+ raise a NoMethodError exception, but because of the 'rescue' clause the TagBinding#missing!
270
+ method is called which fires off Context#tag_missing. By default Context#tag_missing raises
271
+ a UndefinedTagError exception. The 'rescue tag.missing!' idiom is extremly useful for adding
272
+ simple error checking to context sensitive tags.
273
+
274
+
275
+ == Tag Specificity
276
+
277
+ When Radius is presented with two tags that have the same name, but different nesting
278
+ Radius uses an algorithm similar to the way winning rules are calculated in Cascading Style
279
+ Sheets (CSS) to determine which definition should be used. Each time a tag is encountered
280
+ in a template potential tags are assigned specificity values and the tag with the highest
281
+ specificity wins.
282
+
283
+ For example, given the following tag definitions:
284
+
285
+ nesting
286
+ extra:nesting
287
+ parent:child:nesting
288
+
289
+ And template:
290
+
291
+ <r:parent:extra:child:nesting />
292
+
293
+ Radius will calculate specificity values like this:
294
+
295
+ nesting => 1.0.0.0
296
+ extra:nesting => 1.0.1.0
297
+ parent:child:nesting => 1.1.0.1
298
+
299
+ Meaning that parent:child:nesting will win. If a template contained:
300
+
301
+ <r:parent:child:extra:nesting />
302
+
303
+ The following specificity values would be assigned to each of the tag definitions:
304
+
305
+ nesting => 1.0.0.0
306
+ extra:nesting => 1.1.0.0
307
+ parent:child:nesting => 1.0.1.1
308
+
309
+ Meaning that extra:nesting would win because it is more "specific".
310
+
311
+ Values are assigned by assigning points to each of the tags from right to left.
312
+ Given a tag found in a template with nesting four levels deep, the maximum
313
+ specificity a tag could be assigned would be:
314
+
315
+ 1.1.1.1
316
+
317
+ One point for each of the levels.
318
+
319
+ A deep understanding of tag specificity is not necessary to be effective with
320
+ Radius. For the most part you will find that Radius resolves tags precisely the
321
+ way that you would expect. If you find this section confusing forget about it and
322
+ refer back to it if you find that tags are resolving differently from the way that
323
+ you expected.
@@ -0,0 +1,97 @@
1
+ = Radius -- Powerful Tag-Based Templates
2
+
3
+ Radius is a powerful tag-based template language for Ruby inspired by the template languages
4
+ used in MovableType[http://www.movabletype.org] and TextPattern[http://www.textpattern.com].
5
+ It uses tags similar to XML, but can be used to generate any form of plain text (HTML, e-mail,
6
+ etc...).
7
+
8
+ == Example
9
+
10
+ With Radius, it is extremely easy to create custom tags and parse them. Here's a small
11
+ example:
12
+
13
+ require 'radius'
14
+
15
+ # Define tags on a context that will be available to a template:
16
+ context = Radius::Context.new do |c|
17
+ c.define_tag 'hello' do
18
+ 'Hello world'
19
+ end
20
+ c.define_tag 'repeat' do |tag|
21
+ number = (tag.attr['times'] || '1').to_i
22
+ result = ''
23
+ number.times { result << tag.expand }
24
+ result
25
+ end
26
+ end
27
+
28
+ # Create a parser to parse tags that begin with 'r:'
29
+ parser = Radius::Parser.new(context, :tag_prefix => 'r')
30
+
31
+ # Parse tags and output the result
32
+ puts parser.parse(%{A small example:\n<r:repeat times="3">* <r:hello />!\n</r:repeat>})
33
+
34
+ Output:
35
+
36
+ A small example:
37
+ * Hello world!
38
+ * Hello world!
39
+ * Hello world!
40
+
41
+
42
+ = Quick Start
43
+
44
+ Read the QUICKSTART[link:files/QUICKSTART.html] to get up and running fast with Radius.
45
+
46
+
47
+ == Download
48
+
49
+ The latest version of Radius can be found on RubyForge:
50
+
51
+ http://rubyforge.org/projects/radius/
52
+
53
+
54
+ == Installation
55
+
56
+ It is recommended that you install Radius using the RubyGems packaging system:
57
+
58
+ % gem install --remote radius
59
+
60
+ You can also install Radius by copying lib/radius.rb into the Ruby load path.
61
+
62
+
63
+ == License
64
+
65
+ Radius is free software and may be redistributed under the terms of the MIT-LICENSE:
66
+
67
+ Copyright (c) 2006, John W. Long
68
+
69
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this
70
+ software and associated documentation files (the "Software"), to deal in the Software
71
+ without restriction, including without limitation the rights to use, copy, modify, merge,
72
+ publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
73
+ to whom the Software is furnished to do so, subject to the following conditions:
74
+
75
+ The above copyright notice and this permission notice shall be included in all copies or
76
+ substantial portions of the Software.
77
+
78
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
79
+ INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
80
+ PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
81
+ FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
82
+ OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
83
+ DEALINGS IN THE SOFTWARE.
84
+
85
+
86
+ == The Future
87
+
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.
90
+
91
+ If you are interested in helping with the development of Radiant, contact me and we'll talk.
92
+
93
+ Enjoy!
94
+
95
+ --
96
+ John Long ::
97
+ http://wiseheartdesign.com