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/CHANGELOG CHANGED
@@ -1,5 +1,17 @@
1
1
  = Change Log
2
2
 
3
+ === 0.5.0
4
+ * Created a DSL for tag definitions (introducing a DSL makes this version of Radiant incompatible with
5
+ the last). The DSL has the following features:
6
+ - full support for nested tags
7
+ - global and local tag variables
8
+ - Contexts can now be defined dynamically (instead of being subclassed)
9
+ - see the QUICKSTART for more info
10
+ * Many refactorings of the library and unit tests.
11
+ * Changed the license to the MIT-LICENSE.
12
+ * Updated documentation to reflect the changes.
13
+ * Updated the version number to reflect the maturity of the code base.
14
+
3
15
  === 0.0.2
4
16
  * Refactored Parser to use Context#render_tag instead of #send when rendering tags defined on a Context.
5
17
  * UndefinedTagError is now thrown when Parser tries to render a tag which doesn't exist on a Context.
data/QUICKSTART CHANGED
@@ -1,21 +1,21 @@
1
1
  = Radius Quick Start
2
2
 
3
+
3
4
  == Defining Tags
4
5
 
5
6
  Before you can parse a template with Radius you need to create a Context object which defines
6
- the tags that will be used in the template. This is pretty simple:
7
+ the tags that will be used in the template. This is actually quite simple:
7
8
 
8
9
  require 'radius'
9
10
 
10
- class MyContext < Radius::Context
11
- def hello(attr)
12
- "Hello #{attr['name'] || 'World'}!"
13
- end
11
+ context = Context.new
12
+ context.define_tag "hello" do |tag|
13
+ "Hello #{tag.attr['name'] || 'World'}!"
14
14
  end
15
15
 
16
- Once you have defined a context you can create a Parser and parse to your heart's content:
16
+ Once you have defined a context you can easily create a Parser:
17
17
 
18
- parser = Radius::Parser.new(MyContext.new)
18
+ parser = Radius::Parser.new(context)
19
19
  puts parser.parse('<p><radius:hello /></p>')
20
20
  puts parser.parse('<p><radius:hello name="John" /></p>')
21
21
 
@@ -24,12 +24,12 @@ This code will output:
24
24
  <p>Hello World!</p>
25
25
  <p>Hello John!</p>
26
26
 
27
- Note how you can pass attributes from the template to the context using the attributes hash
28
- (which is passed in as the first parameter). Above, the first tag that was parsed didn't have
29
- a name attribute so the code in the +hello+ method uses "World" instead. The second time the
30
- tag is parsed the name attribute is set to "John" which is used to create the string "Hello
31
- John!". <b>All tag definitions must accept only one parameter--the attributes hash.</b> Tags
32
- that do not follow this rule will be treated as if they were undefined (like normal methods).
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
33
 
34
34
  == Container Tags
35
35
 
@@ -39,13 +39,14 @@ you could define another tag to parse and create Textile output:
39
39
 
40
40
  require 'redcloth'
41
41
 
42
- class MyContext < Radius::Context
43
- def textile(attr)
44
- contents = yield
45
- RedCloth.new(contents).to_html
46
- end
42
+ context.define_tag "textile" do |tag|
43
+ contents = tag.expand
44
+ RedCloth.new(contents).to_html
47
45
  end
48
46
 
47
+ (The code <tt>tag.expand</tt> above returns the contents of the template between the start and end
48
+ tags.)
49
+
49
50
  With the code above your parser can easily handle Textile:
50
51
 
51
52
  parser.parse('<radius:textile>h1. Hello **World**!</radius:textile>')
@@ -54,24 +55,28 @@ This code will output:
54
55
 
55
56
  <h1>Hello <strong>World</strong>!</h1>
56
57
 
58
+
59
+ == Nested Tags
60
+
57
61
  But wait!--it gets better. Because container tags can manipulate the content they contain
58
62
  you can use them to iterate over collections:
59
63
 
60
- class ThreeStoogesContext < Radius::Context
61
- def stooge(attr)
62
- content = ''
63
- ["Larry", "Moe", "Curly"].each do |name|
64
- @name = name
65
- content << yield
66
- end
67
- content
68
- end
69
- def name(attr)
70
- @name
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
71
  end
72
+ content
73
+ end
74
+
75
+ context.define_tag "stooge:name" do
76
+ tag.locals.name
72
77
  end
73
78
 
74
- parser = Radius::Parser.new(ThreeStoogesContext.new)
79
+ parser = Radius::Parser.new(context)
75
80
 
76
81
  template = <<-TEMPLATE
77
82
  <ul>
@@ -95,42 +100,224 @@ This code will output:
95
100
 
96
101
  </ul>
97
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>
98
135
 
99
- == Altering the Tag Prefix
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
100
155
 
101
156
  By default, all Radius tags must begin with "radius". You can change this by altering the
102
- prefix attribute on a Context. For example:
157
+ tag_prefix attribute on a Parser. For example:
103
158
 
104
- class MyContext < Radius::Context
105
- def initialize
106
- @prefix = 'r'
107
- end
108
- end
159
+ parser = Radius::Parser.new(context, :tag_prefix => 'r')
109
160
 
110
- Now, when parsing templates with MyContext, Radius will require that tags begin with "r".
161
+ Now, when parsing templates with +parser+, Radius will require that every tag begin with "r"
162
+ instead of "radius".
111
163
 
112
164
 
113
- == Using Context#tag_missing to Define Behavior for Missing Tags
165
+ == Custom Behavior for Undefined Tags
114
166
 
115
167
  Context#tag_missing behaves much like Object#method_missing only it allows you to define
116
168
  specific behavior for when a tag is not defined on a Context. For example:
117
169
 
118
170
  class LazyContext < Radius::Context
119
- def initialize
120
- @prefix = 'lazy'
121
- end
122
171
  def tag_missing(tag, attr, &block)
123
172
  "<strong>ERROR: Undefined tag `#{tag}' with attributes #{attr.inspect}</strong>"
124
173
  end
125
174
  end
126
175
 
127
- parser = Radius::Parser.new(LazyContext.new)
176
+ parser = Radius::Parser.new(LazyContext.new, :tag_prefix => 'lazy')
128
177
  puts parser.parse('<lazy:weird value="true" />')
129
178
 
130
- This code will output:
179
+ This will output:
131
180
 
132
181
  <strong>ERROR: Undefined tag `weird' with attributes {"value"=>"true"}</strong>
133
182
 
134
183
  Normally, when the Radius Parser encounters an undefined tag for a Context it raises an
135
184
  UndefinedTagError, but since we have defined #tag_missing on LazyContext the Parser now
136
- outputs our custom message.
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.
data/README CHANGED
@@ -1,10 +1,48 @@
1
1
  = Radius -- Powerful Tag-Based Templates
2
2
 
3
- Radius is a small, but powerful 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 HTML, but can be used to generate any form of plain text (XML, e-mail,
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
6
  etc...).
7
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
+
8
46
 
9
47
  == Download
10
48
 
@@ -21,28 +59,36 @@ It is recommended that you install Radius using the RubyGems packaging system:
21
59
 
22
60
  You can also install Radius by copying lib/radius.rb into the Ruby load path.
23
61
 
24
- == License
25
62
 
26
- Radius is free software and may be redistributed under the same terms and Ruby itself. For
27
- more details, see the readme file in the Ruby distribution.
63
+ == License
28
64
 
65
+ Radius is free software and may be redistributed under the terms of the MIT-LICENSE:
29
66
 
30
- == Quick Start
67
+ Copyright (c) 2006, John W. Long
31
68
 
32
- To get up and running fast with Radius read:
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:
33
74
 
34
- link:files/QUICKSTART.html
75
+ The above copyright notice and this permission notice shall be included in all copies or
76
+ substantial portions of the Software.
35
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.
36
84
 
37
- == A Call to Action
38
85
 
39
- Radius is still very much in the development stages. Take a look at the roadmap to see where
40
- we want to go:
86
+ == The Future
41
87
 
42
- link:files/ROADMAP.html
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.
43
90
 
44
- If you are a smart developer with a passion for excellence, now is the time to jump on board.
45
- Contact me and we'll talk. :)
91
+ If you are interested in helping with the development of Radiant, contact me and we'll talk.
46
92
 
47
93
  Enjoy!
48
94