radius 0.0.2 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +12 -0
- data/QUICKSTART +232 -45
- data/README +61 -15
- data/ROADMAP +4 -10
- data/Rakefile +50 -8
- data/lib/radius.rb +415 -75
- data/test/radius_test.rb +263 -113
- metadata +3 -4
- 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
|
7
|
+
the tags that will be used in the template. This is actually quite simple:
|
7
8
|
|
8
9
|
require 'radius'
|
9
10
|
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
16
|
+
Once you have defined a context you can easily create a Parser:
|
17
17
|
|
18
|
-
parser = Radius::Parser.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
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
-
|
43
|
-
|
44
|
-
|
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
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
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(
|
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
|
-
|
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
|
-
|
157
|
+
tag_prefix attribute on a Parser. For example:
|
103
158
|
|
104
|
-
|
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
|
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
|
-
==
|
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
|
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
|
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
|
4
|
-
used in MovableType
|
5
|
-
It uses tags similar to
|
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
|
-
|
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
|
-
|
67
|
+
Copyright (c) 2006, John W. Long
|
31
68
|
|
32
|
-
|
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
|
-
|
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
|
-
|
40
|
-
we want to go:
|
86
|
+
== The Future
|
41
87
|
|
42
|
-
|
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
|
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
|
|