radius 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (8) hide show
  1. data/DSL-SPEC +151 -0
  2. data/QUICKSTART +98 -0
  3. data/README +49 -0
  4. data/ROADMAP +18 -0
  5. data/Rakefile +41 -0
  6. data/lib/radius.rb +126 -0
  7. data/test/radius_test.rb +122 -0
  8. metadata +55 -0
@@ -0,0 +1,151 @@
1
+ =DSL Specification
2
+
3
+ I haven't implemented a domain specific languages for Contexts yet, but I am hoping to
4
+ do so in a future release. Below are some thoughts (in code) for how I would like it to
5
+ work. Note that with a robust DSL you will be able to define that certain tags are
6
+ only valid within certain containing tags.
7
+
8
+ class StoreContext < Radius::Context
9
+ def initialize(options)
10
+ @prefix = "r" # all tags must be prefixed with "r"
11
+ @user = options[:user]
12
+ @cart = options[:cart]
13
+ @session = options[:session]
14
+ end
15
+
16
+ # expose the @user object variable
17
+ container(:user, :exposes => :user) do
18
+ # Use protect() on an object that has been exposed to prevent access to
19
+ # an attribute in a template. Conversely you could use the expose() method
20
+ # to expose specific attributes to the template and protect all others.
21
+ protect :password
22
+
23
+ # add a single tag that returns the session_id
24
+ tag :session_id do |attributes|
25
+ @session.id
26
+ end
27
+ end
28
+
29
+ # expose the @cart object as the basket tag
30
+ container(:basket, :exposes => :cart) do
31
+ expand do |attributes|
32
+ #
33
+ # some initialization code with attributes before handling
34
+ # content block
35
+ #
36
+ yeild
37
+ end
38
+
39
+ container(:items, :exposes => :item) do
40
+ expand do |attributes|
41
+ @cart.items.each do |@item|
42
+ yield
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+ container :cart do
49
+ expand do |attributes|
50
+ yield
51
+ end
52
+ end
53
+ end
54
+
55
+ class User
56
+ attr_accessor :name, :login, :password, :email
57
+ def initialize(name, login, password, email)
58
+ @name, @login, @password, @email = name, login, password, email
59
+ end
60
+ end
61
+
62
+ class Session
63
+ attr_accessor :id
64
+ def initialize(id)
65
+ @id = id
66
+ end
67
+ end
68
+
69
+ class Cart
70
+ attr_accessor :items
71
+
72
+ def initialize(*items)
73
+ @items = [items].flatten
74
+ end
75
+
76
+ def total
77
+ @items.map { |line_item| line_item.total }
78
+ end
79
+ end
80
+
81
+ class LineItem
82
+ attr_accessor :name, :description, :quantity, :item_price
83
+ def intialize(name, description, price, quantity)
84
+ @name, @description, @price, @quantity = name, description, price, quantity
85
+ end
86
+ def full_price
87
+ @price * @quantity
88
+ end
89
+ end
90
+
91
+ receipt = <<-RECEIPT
92
+ <p><r:user:name />, thank you for shopping with us! An order summary
93
+ is printed below for your convinience. Please print a copy for your records.</p>
94
+ <r:cart>
95
+ <table>
96
+ <thead>
97
+ <tr>
98
+ <td>Product</td>
99
+ <td>Price</td>
100
+ <td>Quantity</td>
101
+ <td>Totals</td>
102
+ </tr>
103
+ </thead>
104
+ <tbody>
105
+ <r:items>
106
+ <tr>
107
+ <td>
108
+ <strong><r:name /></strong><br >
109
+ <r:description />
110
+ </td>
111
+ <td><r:price /></td>
112
+ <td><r:quanity /></td>
113
+ <td><r:full_price /></td>
114
+ </tr>
115
+ </r:items>
116
+ </tbody>
117
+ <tr>
118
+ <td colspan="3">Total</td>
119
+ <td><r:total /></td>
120
+ </tr>
121
+ </table>
122
+ </r:cart>
123
+ RECEIPT
124
+
125
+ user = User.new('John', 'johnboy', 'm@x!mu5', 'johnboy@maximus.com')
126
+ cart = Cart.new(
127
+ LineItem.new('15in PowerBook', "Apple's premium notebook computer.", 1995.98, 1),
128
+ LineItem.new('Mac Notebook Case', "A beautiful black notebook case designed for Apple Powerbooks.", 54.05, 1)
129
+ )
130
+ session = Session.new('a4bd386e512bacd581')
131
+
132
+ context = StoreContext.new(
133
+ :user => user,
134
+ :cart => cart,
135
+ :session => session
136
+ )
137
+
138
+ template = Radius::Template.new(
139
+ :text => receipt,
140
+ :context => context
141
+ )
142
+
143
+ template.compile! # based on context parses text into abstract syntax tree
144
+ puts template.expand # outputs expanded template
145
+
146
+ # alternate usage
147
+ parser = Radius::Parser.new(context)
148
+ puts parser.parse(receipt) # compiles and outputs expanded template
149
+
150
+ # another alternate
151
+ puts Radius.parse(receipt, context)
@@ -0,0 +1,98 @@
1
+ =Quick Start
2
+
3
+ Before you can parse a template with Radius you need to create a Context object which defines
4
+ the tags that will be used in the template. This is pretty simple:
5
+
6
+ require 'radius'
7
+
8
+ class MyContext < Radius::Context
9
+ def hello(attr)
10
+ "Hello #{attr['name'] || 'World'}!"
11
+ end
12
+ end
13
+
14
+ Once you have defined a context you can create a Parser and parse to your heart's content:
15
+
16
+ parser = Radius::Parser.new(MyContext.new)
17
+ puts parser.parse('<p><radius:hello /></p>')
18
+ puts parser.parse('<p><radius:hello name="John" /></p>')
19
+
20
+ This will output:
21
+
22
+ <p>Hello World!</p>
23
+ <p>Hello John!</p>
24
+
25
+ Note how you can pass attributes from the template to the context using the attributes hash
26
+ (which is passed in as the first parameter.). Above the first tag that was parsed didn't have
27
+ a name attribute so the code in the +hello+ method uses "World" instead. The second time the
28
+ tag is parsed the name attribute is set to "John" which is used to create the string "Hello
29
+ John!".
30
+
31
+ Radius also allows you to define "container" tags. That is, tags that contain content and
32
+ that may optionally manipulate it in some way. For example, if you have RedCloth installed
33
+ you could define another tag to parse and create Textile output:
34
+
35
+ require 'redcloth'
36
+
37
+ class MyContext < Radius::Context
38
+ def textile(attr)
39
+ contents = yield
40
+ RedCloth.new(contents).to_html
41
+ end
42
+ end
43
+
44
+ With the code above your parser can easily handle Textile:
45
+
46
+ parser.parse('<radius:textile>h1. Hello **World**!</radius:textile>')
47
+
48
+ This will output:
49
+
50
+ <h1>Hello <strong>World</strong>!</h1>
51
+
52
+ But wait!--it gets better. Because container tags can manipulate what they contain you can use
53
+ them to iterate over collections:
54
+
55
+ class ThreeStoogesContext < Radius::Context
56
+ def initialize
57
+ @prefix = 'ts'
58
+ end
59
+ def stooge(attr)
60
+ content = ''
61
+ ["Larry", "Moe", "Curly"].each do |name|
62
+ @name = name
63
+ content << yield
64
+ end
65
+ content
66
+ end
67
+ def name(attr)
68
+ @name
69
+ end
70
+ end
71
+
72
+ parser = Radius::Parser.new(ThreeStoogesContext.new)
73
+
74
+ template = <<-TEMPLATE
75
+ <ul>
76
+ <ts:stooge>
77
+ <li><ts:name /></li>
78
+ </ts:stooge>
79
+ </ul>
80
+ TEMPLATE
81
+
82
+ puts parser.parse(template)
83
+
84
+ This will output:
85
+
86
+ <ul>
87
+
88
+ <li>Larry</li>
89
+
90
+ <li>Moe</li>
91
+
92
+ <li>Curly</li>
93
+
94
+ </ul>
95
+
96
+ The above code also illustrates how you can set the prefix instance variable to control the
97
+ string that prefixes Radius tags. By setting the prefix to "ts" our tags must begin with "ts"
98
+ instead of "radius" like they did in the other examples.
data/README ADDED
@@ -0,0 +1,49 @@
1
+ = Radius -- Powerful Tag-Based Templates
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,
6
+ etc...).
7
+
8
+
9
+ == Download
10
+
11
+ The latest version of Radius can be found on RubyForge:
12
+
13
+ http://rubyforge.org/projects/radius/
14
+
15
+
16
+ == Installation
17
+
18
+ It is recommended that you install Radius using the RubyGems packaging system:
19
+
20
+ % gem install --remote radius
21
+
22
+ You can also install Radius by copying radius.rb into the Ruby load path.
23
+
24
+ == License
25
+
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.
28
+
29
+
30
+ == Quick Start
31
+
32
+ To get up and running fast with Radius read:
33
+
34
+ link:files/QUICKSTART.html
35
+
36
+
37
+ == A Call to Action
38
+
39
+ Radius is still very much in the development stages. Take a look at the roadmap to see where
40
+ we want to go:
41
+
42
+ link:files/ROADMAP.html
43
+
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. :)
46
+
47
+ --
48
+
49
+ John Long :: http://wiseheartdesign.com
data/ROADMAP ADDED
@@ -0,0 +1,18 @@
1
+ = Roadmap
2
+
3
+ This is a prioritized roadmap for future releases:
4
+
5
+ 1. Clean up the current code base.
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.
9
+
10
+ 3. Create a simple DSL for defining contexts. I had done some thinking
11
+ about this in the past. This thread on Ruby-Talk defined part of it:
12
+
13
+ http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/127640/
14
+
15
+ Update: See link:files/DSL-SPEC.html for a fuller explanation of how
16
+ the DSL should behave.
17
+
18
+ 4. Optimize for speed. Incorporate strscan?
@@ -0,0 +1,41 @@
1
+ require 'rubygems'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+ require 'rake/gempackagetask'
5
+
6
+ task :default => :test
7
+
8
+ Rake::TestTask.new do |t|
9
+ t.pattern = 'test/**/*_test.rb'
10
+ end
11
+
12
+ Rake::RDocTask.new do |rd|
13
+ rd.title = 'Radius -- Powerful Tag-Based Templates'
14
+ rd.main = "README"
15
+ rd.rdoc_files.include("README", "QUICKSTART", "ROADMAP", "DSL-SPEC", "lib/**/*.rb")
16
+ rd.rdoc_dir = 'doc'
17
+ end
18
+
19
+ spec = Gem::Specification.new do |s|
20
+ s.platform = Gem::Platform::RUBY
21
+ s.summary = "Powerful tag-based template system."
22
+ s.name = 'radius'
23
+ s.version = '0.0.1'
24
+ s.requirements << 'none'
25
+ s.require_path = 'lib'
26
+ s.autorequire = 'radius'
27
+ s.has_rdoc = true
28
+ s.rdoc_options << '--title' << 'Radius -- Powerful Tag-Based Templates' <<
29
+ '--main' << 'README' <<
30
+ '--line-numbers' << 'ROADMAP' <<
31
+ 'QUICKSTART' << 'DSL-SPEC' << 'README'
32
+ files = FileList['**/*']
33
+ files.exclude 'doc'
34
+ s.files = files.to_a
35
+ s.description = "Radius is a small, but powerful tag-based template language inspired\nby the template languages used in MovableType and TextPattern."
36
+ end
37
+
38
+ Rake::GemPackageTask.new(spec) do |pkg|
39
+ pkg.need_zip = true
40
+ pkg.need_tar = true
41
+ end
@@ -0,0 +1,126 @@
1
+ module Radius
2
+ class ParseError < StandardError # :nodoc:
3
+ end
4
+
5
+ class MissingEndTagError < ParseError # :nodoc:
6
+ def initialize(tag_name)
7
+ super("end tag not found for start tag `#{tag_name}'")
8
+ end
9
+ end
10
+
11
+ #
12
+ # An abstract class for creating a Context. A context defines the tags that
13
+ # are available for use in a template.
14
+ #
15
+ class Context
16
+ # The prefix attribute controls the string of text that is helps the parser
17
+ # identify template tags. By default this attribute is set to "radius", but
18
+ # you may want to override this for your own contexts.
19
+ attr_accessor :prefix
20
+
21
+ # Creates a new Context object.
22
+ def initialize
23
+ @prefix = 'radius'
24
+ end
25
+ end
26
+
27
+ class Tag # :nodoc:
28
+ def initialize(&b)
29
+ @block = b
30
+ end
31
+ def on_parse(&b)
32
+ @block = b
33
+ end
34
+ def to_s
35
+ @block.call(self)
36
+ end
37
+ end
38
+
39
+ class ContainerTag < Tag # :nodoc:
40
+ attr_accessor :name, :attributes, :contents
41
+
42
+ def initialize(name="", attributes={}, contents=[], &b)
43
+ @name, @attributes, @contents = name, attributes, contents
44
+ super(&b)
45
+ end
46
+ end
47
+
48
+ #
49
+ # The Radius parser. Initialize a parser with the Context object that defines
50
+ # how tags should be expanded.
51
+ #
52
+ class Parser
53
+ # The Context object used to expand template tags.
54
+ attr_accessor :context
55
+
56
+ # Creates a new parser object initialized with a context.
57
+ def initialize(context = Context.new)
58
+ @context = context
59
+ end
60
+
61
+ # Parse string for tags, expand them, and return the result.
62
+ def parse(string)
63
+ @stack = [ContainerTag.new { |t| t.contents.to_s }]
64
+ pre_parse(string)
65
+ @stack.last.to_s
66
+ end
67
+
68
+ def pre_parse(text) # :nodoc:
69
+ re = %r{<#{@context.prefix}:(\w+?)(?:\s+?([^/>]*?)|)>|</#{@context.prefix}:(\w+?)\s*?>}
70
+ if md = re.match(text)
71
+ start_tag, attr, end_tag = $1, $2, $3
72
+ @stack.last.contents << Tag.new { parse_individual(md.pre_match) }
73
+ remaining = md.post_match
74
+ if start_tag
75
+ parse_start_tag(start_tag, attr, remaining)
76
+ else
77
+ parse_end_tag(end_tag, remaining)
78
+ end
79
+ else
80
+ if @stack.length == 1
81
+ @stack.last.contents << Tag.new { parse_individual(text) }
82
+ else
83
+ raise MissingEndTagError.new(@stack.last.name)
84
+ end
85
+ end
86
+ end
87
+
88
+ def parse_start_tag(start_tag, attr, remaining) # :nodoc:
89
+ @stack.push(ContainerTag.new(start_tag, parse_attributes(attr)))
90
+ pre_parse(remaining)
91
+ end
92
+
93
+ def parse_end_tag(end_tag, remaining) # :nodoc:
94
+ popped = @stack.pop
95
+ if popped.name == end_tag
96
+ popped.on_parse { |t| @context.send(popped.name, popped.attributes) { t.contents.to_s } }
97
+ tag = @stack.last
98
+ tag.contents << popped
99
+ pre_parse(remaining)
100
+ else
101
+ raise MissingEndTagError.new(popped.name)
102
+ end
103
+ end
104
+
105
+ def parse_individual(text) # :nodoc:
106
+ re = /<#{@context.prefix}:(\w+?)\s+?(.*?)\s*?\/>/
107
+ if md = re.match(text)
108
+ attr = parse_attributes($2)
109
+ replace = @context.send($1, attr)
110
+ md.pre_match + replace + parse_individual(md.post_match)
111
+ else
112
+ text || ''
113
+ end
114
+ end
115
+
116
+ def parse_attributes(text) # :nodoc:
117
+ attr = {}
118
+ re = /(\w+?)\s*=\s*('|")(.*?)\2/
119
+ while md = re.match(text)
120
+ attr[$1] = $3
121
+ text = md.post_match
122
+ end
123
+ attr
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,122 @@
1
+ require 'test/unit'
2
+ require 'radius'
3
+
4
+ class ContextTest < Test::Unit::TestCase
5
+ def test_initialize
6
+ c = Radius::Context.new
7
+ assert_equal 'radius', c.prefix
8
+ end
9
+ end
10
+
11
+ class RadiusTest < Test::Unit::TestCase
12
+ class TestContext < Radius::Context
13
+ def initialize
14
+ @prefix = "test"
15
+ @items = ["Larry", "Moe", "Curly"]
16
+ end
17
+
18
+ def echo(attr)
19
+ attr["text"]
20
+ end
21
+
22
+ def add(attr)
23
+ (attr["param1"].to_i + attr["param2"].to_i).to_s
24
+ end
25
+
26
+ def reverse(attr)
27
+ yield.reverse
28
+ end
29
+
30
+ def capitalize(attr)
31
+ yield.upcase
32
+ end
33
+
34
+ def count(attr)
35
+ case
36
+ when attr["set"]
37
+ @count = attr["set"].to_i
38
+ ""
39
+ when attr["inc"] == "true"
40
+ @count = (@count || 0) + 1
41
+ ""
42
+ else
43
+ @count.to_s
44
+ end
45
+ end
46
+
47
+ def loop(attr)
48
+ t = attr["times"].to_i
49
+ result = ""
50
+ t.times { result += yield }
51
+ result
52
+ end
53
+
54
+ def each_item(attr)
55
+ result = []
56
+ @items.each { |@item| result << yield }
57
+ @item = nil
58
+ result.join(attr["between"] || "")
59
+ end
60
+
61
+ def item(attr)
62
+ @item
63
+ end
64
+ end
65
+
66
+ def setup
67
+ @t = Radius::Parser.new(TestContext.new )
68
+ end
69
+
70
+ def test_parse_individual
71
+ r = @t.parse_individual(%{<<test:echo text="hello world!" />>})
72
+ assert_equal("<hello world!>", r)
73
+
74
+ r = @t.parse_individual(%{<test:add param1="1" param2='2'/>})
75
+ assert_equal("3", r)
76
+
77
+ r = @t.parse_individual(%{a <test:echo text="3 + 1 =" /> <test:add param1="3" param2="1"/> b})
78
+ assert_equal("a 3 + 1 = 4 b", r)
79
+ end
80
+
81
+ def test_parse_attributes
82
+ r = @t.parse_attributes(%{ a="1" b='2'c="3"d="'" })
83
+ assert_equal({"a" => "1", "b" => "2", "c" => "3", "d" => "'"}, r)
84
+ end
85
+
86
+ def test_parse
87
+ r = @t.parse(%{<<test:echo text="hello world!" />>})
88
+ assert_equal("<hello world!>", r)
89
+
90
+ r = @t.parse("<test:reverse>test</test:reverse>")
91
+ assert_equal("test".reverse, r)
92
+
93
+ r = @t.parse("<test:reverse>test</test:reverse> <test:capitalize>test</test:capitalize>")
94
+ assert_equal("tset TEST", r)
95
+
96
+ r = @t.parse("<test:echo text='hello world!' /> cool: <test:reverse>a <test:capitalize>test</test:capitalize> b</test:reverse> !")
97
+ assert_equal("hello world! cool: b TSET a !", r)
98
+
99
+ r = @t.parse("<test:reverse><test:echo text='hello world!' /></test:reverse>")
100
+ assert_equal("!dlrow olleh", r)
101
+
102
+ r = @t.parse("<test:reverse><test:capitalize>test</test:capitalize> <test:echo text='hello world!' /></test:reverse>")
103
+ assert_equal("!dlrow olleh TSET", r)
104
+
105
+ r = @t.parse("<test:reverse>12<test:capitalize>at</test:capitalize>34</test:reverse>")
106
+ assert_equal("43TA21", r)
107
+ end
108
+ def test_parse__loop
109
+ r = @t.parse(%{<test:count set="0" /><test:loop times="5"><test:count inc="true" /><test:count /></test:loop>})
110
+ assert_equal("12345", r)
111
+
112
+ r = @t.parse(%{Three Stooges: <test:each_item between=", ">"<test:item />"</test:each_item>})
113
+ assert_equal(%{Three Stooges: "Larry", "Moe", "Curly"}, r)
114
+ end
115
+
116
+ def test_parse__fail_on_no_end_tag
117
+ assert_raises(Radius::MissingEndTagError) { @t.parse("<test:reverse>") }
118
+ end
119
+ def test_parse__fail_on_no_end_tag_2
120
+ assert_raises(Radius::MissingEndTagError) { @t.parse("<test:reverse><test:capitalize></test:reverse>") }
121
+ end
122
+ end
metadata ADDED
@@ -0,0 +1,55 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.8.10
3
+ specification_version: 1
4
+ name: radius
5
+ version: !ruby/object:Gem::Version
6
+ version: 0.0.1
7
+ date: 2006-01-21
8
+ summary: Powerful tag-based template system.
9
+ require_paths:
10
+ - lib
11
+ email:
12
+ homepage:
13
+ rubyforge_project:
14
+ description: "Radius is a small, but powerful tag-based template language inspired by the
15
+ template languages used in MovableType and TextPattern."
16
+ autorequire: radius
17
+ default_executable:
18
+ bindir: bin
19
+ has_rdoc: true
20
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
21
+ requirements:
22
+ -
23
+ - ">"
24
+ - !ruby/object:Gem::Version
25
+ version: 0.0.0
26
+ version:
27
+ platform: ruby
28
+ authors: []
29
+ files:
30
+ - DSL-SPEC
31
+ - lib
32
+ - QUICKSTART
33
+ - Rakefile
34
+ - README
35
+ - ROADMAP
36
+ - test
37
+ - lib/radius.rb
38
+ - test/radius_test.rb
39
+ test_files: []
40
+ rdoc_options:
41
+ - "--title"
42
+ - "Radius -- Powerful Tag-Based Templates"
43
+ - "--main"
44
+ - README
45
+ - "--line-numbers"
46
+ - ROADMAP
47
+ - QUICKSTART
48
+ - DSL-SPEC
49
+ - README
50
+ extra_rdoc_files: []
51
+ executables: []
52
+ extensions: []
53
+ requirements:
54
+ - none
55
+ dependencies: []