radius19-radiant 1.0.0
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 +7 -0
- data/QUICKSTART +323 -0
- data/README +5 -0
- data/ROADMAP +12 -0
- data/Rakefile +56 -0
- data/VERSION +1 -0
- data/lib/radius/context.rb +139 -0
- data/lib/radius/dostruct.rb +31 -0
- data/lib/radius/errors.rb +24 -0
- data/lib/radius/parser.rb +95 -0
- data/lib/radius/parsetag.rb +23 -0
- data/lib/radius/tagbinding.rb +66 -0
- data/lib/radius/tagdefs.rb +78 -0
- data/lib/radius/util.rb +34 -0
- data/lib/radius19.rb +24 -0
- data/test/context_test.rb +61 -0
- data/test/dostruct_test.rb +53 -0
- data/test/parser_test.rb +242 -0
- data/test/tagbinding_test.rb +50 -0
- data/test/tagdefs_test.rb +61 -0
- data/test/test_helper.rb +25 -0
- data/test/util_test.rb +25 -0
- metadata +91 -0
@@ -0,0 +1,31 @@
|
|
1
|
+
module Radius
|
2
|
+
class DelegatingOpenStruct # :nodoc:
|
3
|
+
attr_accessor :object
|
4
|
+
|
5
|
+
def initialize(object = nil)
|
6
|
+
@object = object
|
7
|
+
@hash = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def method_missing(method, *args, &block)
|
11
|
+
symbol = (method.to_s =~ /^(.*?)=$/) ? $1.intern : method
|
12
|
+
if (0..1).include?(args.size)
|
13
|
+
if args.size == 1
|
14
|
+
@hash[symbol] = args.first
|
15
|
+
else
|
16
|
+
if @hash.has_key?(symbol)
|
17
|
+
@hash[symbol]
|
18
|
+
else
|
19
|
+
unless object.nil?
|
20
|
+
@object.send(method, *args, &block)
|
21
|
+
else
|
22
|
+
nil
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
else
|
27
|
+
super
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Radius
|
2
|
+
|
3
|
+
# Abstract base class for all parsing errors.
|
4
|
+
class ParseError < StandardError
|
5
|
+
end
|
6
|
+
|
7
|
+
# Occurs when Parser cannot find an end tag for a given tag in a template or when
|
8
|
+
# tags are miss-matched in a template.
|
9
|
+
class MissingEndTagError < ParseError
|
10
|
+
# Create a new MissingEndTagError object for +tag_name+.
|
11
|
+
def initialize(tag_name)
|
12
|
+
super("end tag not found for start tag `#{tag_name}'")
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Occurs when Context#render_tag cannot find the specified tag on a Context.
|
17
|
+
class UndefinedTagError < ParseError
|
18
|
+
# Create a new UndefinedTagError object for +tag_name+.
|
19
|
+
def initialize(tag_name)
|
20
|
+
super("undefined tag `#{tag_name}'")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module Radius
|
2
|
+
#
|
3
|
+
# The Radius parser. Initialize a parser with a Context object that
|
4
|
+
# defines how tags should be expanded. See the QUICKSTART[link:files/QUICKSTART.html]
|
5
|
+
# for a detailed explaination of its usage.
|
6
|
+
#
|
7
|
+
class Parser
|
8
|
+
# The Context object used to expand template tags.
|
9
|
+
attr_accessor :context
|
10
|
+
|
11
|
+
# The string that prefixes all tags that are expanded by a parser
|
12
|
+
# (the part in the tag name before the first colon).
|
13
|
+
attr_accessor :tag_prefix
|
14
|
+
|
15
|
+
# Creates a new parser object initialized with a Context.
|
16
|
+
def initialize(context = Context.new, options = {})
|
17
|
+
if context.kind_of?(Hash) and options.empty?
|
18
|
+
options = context
|
19
|
+
context = options[:context] || options['context'] || Context.new
|
20
|
+
end
|
21
|
+
options = Util.symbolize_keys(options)
|
22
|
+
@context = context
|
23
|
+
@tag_prefix = options[:tag_prefix]
|
24
|
+
end
|
25
|
+
|
26
|
+
# Parses string for tags, expands them, and returns the result.
|
27
|
+
def parse(string)
|
28
|
+
@stack = [ParseContainerTag.new { |t| Util.recurring_array_to_s(t.contents) }]
|
29
|
+
pre_parse(string)
|
30
|
+
@stack.last.to_s
|
31
|
+
end
|
32
|
+
|
33
|
+
protected
|
34
|
+
|
35
|
+
def pre_parse(text) # :nodoc:
|
36
|
+
re = %r{<#{@tag_prefix}:([\w:]+?)(\s+(?:\w+\s*=\s*(?:"[^"]*?"|'[^']*?')\s*)*|)>|</#{@tag_prefix}:([\w:]+?)\s*>}
|
37
|
+
if md = re.match(text)
|
38
|
+
start_tag, attr, end_tag = $1, $2, $3
|
39
|
+
@stack.last.contents << ParseTag.new { parse_individual(md.pre_match) }
|
40
|
+
remaining = md.post_match
|
41
|
+
if start_tag
|
42
|
+
parse_start_tag(start_tag, attr, remaining)
|
43
|
+
else
|
44
|
+
parse_end_tag(end_tag, remaining)
|
45
|
+
end
|
46
|
+
else
|
47
|
+
if @stack.length == 1
|
48
|
+
@stack.last.contents << ParseTag.new { parse_individual(text) }
|
49
|
+
else
|
50
|
+
raise MissingEndTagError.new(@stack.last.name)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def parse_start_tag(start_tag, attr, remaining) # :nodoc:
|
56
|
+
@stack.push(ParseContainerTag.new(start_tag, parse_attributes(attr)))
|
57
|
+
pre_parse(remaining)
|
58
|
+
end
|
59
|
+
|
60
|
+
def parse_end_tag(end_tag, remaining) # :nodoc:
|
61
|
+
popped = @stack.pop
|
62
|
+
if popped.name == end_tag
|
63
|
+
popped.on_parse do |t|
|
64
|
+
@context.render_tag(popped.name, popped.attributes) { Util.recurring_array_to_s(t.contents) }
|
65
|
+
end
|
66
|
+
tag = @stack.last
|
67
|
+
tag.contents << popped
|
68
|
+
pre_parse(remaining)
|
69
|
+
else
|
70
|
+
raise MissingEndTagError.new(popped.name)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def parse_individual(text) # :nodoc:
|
75
|
+
re = %r{<#{@tag_prefix}:([\w:]+?)(\s+(?:\w+\s*=\s*(?:"[^"]*?"|'[^']*?')\s*)*|)/>}
|
76
|
+
if md = re.match(text)
|
77
|
+
attr = parse_attributes($2)
|
78
|
+
replace = @context.render_tag($1, attr)
|
79
|
+
md.pre_match + replace + parse_individual(md.post_match)
|
80
|
+
else
|
81
|
+
text || ''
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def parse_attributes(text) # :nodoc:
|
86
|
+
attr = {}
|
87
|
+
re = /(\w+?)\s*=\s*('|")(.*?)\2/
|
88
|
+
while md = re.match(text)
|
89
|
+
attr[$1] = $3
|
90
|
+
text = md.post_match
|
91
|
+
end
|
92
|
+
attr
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
class ParseTag # :nodoc:
|
2
|
+
def initialize(&b)
|
3
|
+
@block = b
|
4
|
+
end
|
5
|
+
|
6
|
+
def on_parse(&b)
|
7
|
+
@block = b
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_s
|
11
|
+
@block.call(self)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class ParseContainerTag < ParseTag # :nodoc:
|
16
|
+
attr_accessor :name, :attributes, :contents
|
17
|
+
|
18
|
+
def initialize(name = "", attributes = {}, contents = [], &b)
|
19
|
+
@name, @attributes, @contents = name, attributes, contents
|
20
|
+
super(&b)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Radius
|
2
|
+
#
|
3
|
+
# A tag binding is passed into each tag definition and contains helper methods for working
|
4
|
+
# with tags. Use it to gain access to the attributes that were passed to the tag, to
|
5
|
+
# render the tag contents, and to do other tasks.
|
6
|
+
#
|
7
|
+
class TagBinding
|
8
|
+
# The Context that the TagBinding is associated with. Used internally. Try not to use
|
9
|
+
# this object directly.
|
10
|
+
attr_reader :context
|
11
|
+
|
12
|
+
# The locals object for the current tag.
|
13
|
+
attr_reader :locals
|
14
|
+
|
15
|
+
# The name of the tag (as used in a template string).
|
16
|
+
attr_reader :name
|
17
|
+
|
18
|
+
# The attributes of the tag. Also aliased as TagBinding#attr.
|
19
|
+
attr_reader :attributes
|
20
|
+
alias :attr :attributes
|
21
|
+
|
22
|
+
# The render block. When called expands the contents of the tag. Use TagBinding#expand
|
23
|
+
# instead.
|
24
|
+
attr_reader :block
|
25
|
+
|
26
|
+
# Creates a new TagBinding object.
|
27
|
+
def initialize(context, locals, name, attributes, block)
|
28
|
+
@context, @locals, @name, @attributes, @block = context, locals, name, attributes, block
|
29
|
+
end
|
30
|
+
|
31
|
+
# Evaluates the current tag and returns the rendered contents.
|
32
|
+
def expand
|
33
|
+
double? ? block.call : ''
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns true if the current tag is a single tag.
|
37
|
+
def single?
|
38
|
+
block.nil?
|
39
|
+
end
|
40
|
+
|
41
|
+
# Returns true if the current tag is a container tag.
|
42
|
+
def double?
|
43
|
+
not single?
|
44
|
+
end
|
45
|
+
|
46
|
+
# The globals object from which all locals objects ultimately inherit their values.
|
47
|
+
def globals
|
48
|
+
@context.globals
|
49
|
+
end
|
50
|
+
|
51
|
+
# Returns a list of the way tags are nested around the current tag as a string.
|
52
|
+
def nesting
|
53
|
+
@context.current_nesting
|
54
|
+
end
|
55
|
+
|
56
|
+
# Fires off Context#tag_missing for the current tag.
|
57
|
+
def missing!
|
58
|
+
@context.tag_missing(name, attributes, &block)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Renders the tag using the current context .
|
62
|
+
def render(tag, attributes = {}, &block)
|
63
|
+
@context.render_tag(tag, attributes, &block)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Radius
|
2
|
+
module TagDefinitions # :nodoc:
|
3
|
+
class TagFactory # :nodoc:
|
4
|
+
def initialize(context)
|
5
|
+
@context = context
|
6
|
+
end
|
7
|
+
|
8
|
+
def define_tag(name, options, &block)
|
9
|
+
options = prepare_options(name, options)
|
10
|
+
validate_params(name, options, &block)
|
11
|
+
construct_tag_set(name, options, &block)
|
12
|
+
expose_methods_as_tags(name, options)
|
13
|
+
end
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
# Normalizes options pased to tag definition. Override in decendants to preform
|
18
|
+
# additional normalization.
|
19
|
+
def prepare_options(name, options)
|
20
|
+
options = Util.symbolize_keys(options)
|
21
|
+
options[:expose] = expand_array_option(options[:expose])
|
22
|
+
object = options[:for]
|
23
|
+
options[:attributes] = object.respond_to?(:attributes) unless options.has_key? :attributes
|
24
|
+
options[:expose] += object.attributes.keys if options[:attributes]
|
25
|
+
options
|
26
|
+
end
|
27
|
+
|
28
|
+
# Validates parameters passed to tag definition. Override in decendants to add custom
|
29
|
+
# validations.
|
30
|
+
def validate_params(name, options, &block)
|
31
|
+
unless options.has_key? :for
|
32
|
+
raise ArgumentError.new("tag definition must contain a :for option or a block") unless block
|
33
|
+
raise ArgumentError.new("tag definition must contain a :for option when used with the :expose option") unless options[:expose].empty?
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Adds the tag definition to the context. Override in subclasses to add additional tags
|
38
|
+
# (child tags) when the tag is created.
|
39
|
+
def construct_tag_set(name, options, &block)
|
40
|
+
if block
|
41
|
+
@context.definitions[name.to_s] = block
|
42
|
+
else
|
43
|
+
lp = last_part(name)
|
44
|
+
@context.define_tag(name) do |tag|
|
45
|
+
if tag.single?
|
46
|
+
options[:for]
|
47
|
+
else
|
48
|
+
tag.locals.send("#{ lp }=", options[:for]) unless options[:for].nil?
|
49
|
+
tag.expand
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Exposes the methods of an object as child tags.
|
56
|
+
def expose_methods_as_tags(name, options)
|
57
|
+
options[:expose].each do |method|
|
58
|
+
tag_name = "#{name}:#{method}"
|
59
|
+
lp = last_part(name)
|
60
|
+
@context.define_tag(tag_name) do |tag|
|
61
|
+
object = tag.locals.send(lp)
|
62
|
+
object.send(method)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
protected
|
68
|
+
|
69
|
+
def expand_array_option(value)
|
70
|
+
[*value].compact.map { |m| m.to_s.intern }
|
71
|
+
end
|
72
|
+
|
73
|
+
def last_part(name)
|
74
|
+
name.split(':').last
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
data/lib/radius/util.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
module Radius
|
2
|
+
module Util # :nodoc:
|
3
|
+
def self.symbolize_keys(hash)
|
4
|
+
new_hash = {}
|
5
|
+
hash.keys.each do |k|
|
6
|
+
new_hash[k.to_s.intern] = hash[k]
|
7
|
+
end
|
8
|
+
new_hash
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.impartial_hash_delete(hash, key)
|
12
|
+
string = key.to_s
|
13
|
+
symbol = string.intern
|
14
|
+
value1 = hash.delete(symbol)
|
15
|
+
value2 = hash.delete(string)
|
16
|
+
value1 || value2
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.constantize(camelized_string)
|
20
|
+
raise "invalid constant name `#{camelized_string}'" unless camelized_string.split('::').all? { |part| part =~ /^[A-Za-z]+$/ }
|
21
|
+
Object.module_eval(camelized_string)
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.camelize(underscored_string)
|
25
|
+
string = ''
|
26
|
+
underscored_string.split('_').each { |part| string << part.capitalize }
|
27
|
+
string
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.recurring_array_to_s(ary)
|
31
|
+
ary.map{|x| x.is_a?(Array) ? recurring_array_to_s(x) : x.to_s }.join
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/radius19.rb
ADDED
@@ -0,0 +1,24 @@
|
|
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
|
+
|
22
|
+
dir = File.join(File.dirname(__FILE__), 'radius')
|
23
|
+
require_files = %w{errors tagdefs dostruct tagbinding context parsetag parser util}
|
24
|
+
require_files.each {|f| require File.join(dir, f)}
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper'
|
2
|
+
|
3
|
+
class RadiusContextTest < Test::Unit::TestCase
|
4
|
+
include RadiusTestHelper
|
5
|
+
|
6
|
+
def setup
|
7
|
+
@context = new_context
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_initialize
|
11
|
+
@context = Radius::Context.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_initialize_with_block
|
15
|
+
@context = Radius::Context.new do |c|
|
16
|
+
assert_kind_of Radius::Context, c
|
17
|
+
c.define_tag('test') { 'just a test' }
|
18
|
+
end
|
19
|
+
assert_not_equal Hash.new, @context.definitions
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_with
|
23
|
+
got = @context.with do |c|
|
24
|
+
assert_equal @context, c
|
25
|
+
end
|
26
|
+
assert_equal @context, got
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_render_tag
|
30
|
+
define_tag "hello" do |tag|
|
31
|
+
"Hello #{tag.attr['name'] || 'World'}!"
|
32
|
+
end
|
33
|
+
assert_render_tag_output 'Hello World!', 'hello'
|
34
|
+
assert_render_tag_output 'Hello John!', 'hello', 'name' => 'John'
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_render_tag__undefined_tag
|
38
|
+
e = assert_raises(Radius::UndefinedTagError) { @context.render_tag('undefined_tag') }
|
39
|
+
assert_equal "undefined tag `undefined_tag'", e.message
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_tag_missing
|
43
|
+
class << @context
|
44
|
+
def tag_missing(tag, attr, &block)
|
45
|
+
"undefined tag `#{tag}' with attributes #{attr.inspect}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
text = ''
|
50
|
+
expected = %{undefined tag `undefined_tag' with attributes {"cool"=>"beans"}}
|
51
|
+
assert_nothing_raised { text = @context.render_tag('undefined_tag', 'cool' => 'beans') }
|
52
|
+
assert_equal expected, text
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def assert_render_tag_output(output, *render_tag_params)
|
58
|
+
assert_equal output, @context.render_tag(*render_tag_params)
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|