Skip to content

Commit

Permalink
more changes and refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
jlong committed Mar 14, 2006
1 parent efa6389 commit 8a0d3b6
Show file tree
Hide file tree
Showing 3 changed files with 186 additions and 51 deletions.
12 changes: 6 additions & 6 deletions radius/DSL-SPEC
Expand Up @@ -21,7 +21,7 @@ only valid within certain containing tags.
"*" * @user.password.size
end
tag "basket", :for => @cart, :expose => :total
tag "basket:items", :for => proc { @cart.items }, :type => :interable
tag "basket:items", :for => proc { @cart.items }, :type => :iterable, :item_expose => [:name, :description, :quantity, :price]
end
end

Expand Down Expand Up @@ -80,12 +80,12 @@ only valid within certain containing tags.
<r:items:each>
<tr>
<td>
<strong><r:name /></strong><br >
<r:description />
<strong><r:item:name /></strong><br >
<r:item:description />
</td>
<td><r:price /></td>
<td><r:quantity /></td>
<td><r:full_price /></td>
<td><r:item:price /></td>
<td><r:item:quantity /></td>
<td><r:item:full_price /></td>
</tr>
</r:items:each>
</tbody>
Expand Down
170 changes: 135 additions & 35 deletions radius/lib/radius.rb
Expand Up @@ -25,6 +25,7 @@ def initialize(tag_name)
# are available for use in a template.
#
class Context

# The prefix attribute controls the string of text that is helps the parser
# identify template tags. By default this attribute is set to "radius", but
# you may want to override this when creating your own contexts.
Expand All @@ -33,38 +34,102 @@ class Context
# Creates a new Context object.
def initialize
@prefix = 'radius'
@tags = {}
@tag_name_stack = []
@attr_stack = []
@block_stack = []
end

# Creates a tag definition on a context.
def tag(name, options = {}, &block)
options = Util.symbolize_keys(options)
name = name.to_s
for_name = options.has_key?(:for) ? options[:for].to_s : name
@tags[name] = block || proc do
if single?
instance_variable_get("@#{for_name}").to_s
@tag_definitions = {}
@tag_render_stack = []
@enumerable_each_items = {}
end

# Creates a tag definition on a context. Several options are available to you
# when creating a tag:
#
# +expose+:: Specifieds that child tags should be set for each of the methods
# contained in this option. May be either a single symbol or an
# array of symbols.
#
# +for+:: Specifies the name for the instance variable that the main
# tag is in reference to. This is applical when a block is not
# passed to the tag, or when the +expose+ option is also used.
#
# +item_tag+:: Specifies the name of the item tag (only applicable when the type
# option is set to 'enumerable').
#
# +item_expose+:: Works like +expose+ except that it exposes methods on items
# referenced by tags with a type of 'enumerable'.
#
# +type+:: When this option is set to 'enumerable' the following additional
# tags are added as child tags: +each+, +each:item+, +max+, +min+,
# +size+, +length+, and +count+.
#
def define_tag(name, options = {}, &block)
options = prepare_tag_options(name, options)
type = options.delete(:type)
case type.to_s
when 'enumerable'
define_enumerable_tag(name, options, &block)
else
unless block
define_ivar_tag(name, options)
else
work
@tag_definitions[name.to_s] = block
expose_methods_as_tags(name, options)
end
end
if options[:expose]
[*options[:expose]].each do |method|
@tags["#{name}:#{method}"] = proc {
object = instance_variable_get("@#{for_name}")
object.send(method).to_s
}
end

def define_ivar_tag(name, options) # :nodoc:
options = prepare_tag_options(name, options)
define_tag(name, options) do
if single?
get_for_object(options[:for]).to_s
else
expand
end
end
end

def define_enumerable_tag(name, options, &block) # :nodoc:
options = prepare_tag_options(name, options)

options[:expose] += ['min', 'max']
define_tag(name, options, &block)

define_tag("#{name}:size") do
object = get_for_object(options[:for])
object.entries.size
end

define_tag("#{name}:count") do
render_tag "#{name}:size"
end

define_tag("#{name}:length") do
render_tag "#{name}:size"
end

define_tag("#{name}:each") do
object = get_for_object(options[:for])
n = qualified_tag_name(name)
result = []
object.each do |item|
@enumerable_each_items[n] = item
result << expand
end
result
end

define_tag(
"#{name}:each:#{options[:item_tag]}",
:for => proc { enumerable_item_for(name) },
:expose => options[:item_expose]
) do
enumerable_item_for(name)
end
end

# Returns the value of a rendered tag. Used internally by Parser#parse.
def render_tag(tag, attributes = {}, &block)
name = qualified_tag_name(tag.to_s)
tag_block = @tags[name]
tag_block = @tag_definitions[name]
if tag_block
stack(name, attributes, block) do
instance_eval(&tag_block).to_s
Expand All @@ -85,53 +150,88 @@ def tag_missing(tag, attributes, &block)

# Returns the attributes for the current tag.
def attr
@attr_stack.last
@tag_render_stack.last[:attr]
end
alias :attributes :attr

# Returns the render block for the current tag.
def block
@block_stack.last
@tag_render_stack.last[:block]
end

# Executes the render block for the current tag and returns the
# result. Returns and empty string if block is nil.
def work
def expand
double? ? block.call : ''
end

# Returns true if the current tag is a single tag
def single?
block.nil?
end

# Returns true if the current tag is a double tag
def double?
not single?
end

# Returns the current item in the named enumerable loop
def enumerable_item_for(name)
n = qualified_tag_name(name)
@enumerable_each_items[n]
end

# A convienence method for managing the various parts of the
# rendering stack.
# tag render stack.
def stack(name, attributes, block)
@tag_name_stack.push name
@attr_stack.push attributes
@block_stack.push block
@tag_render_stack.push(:name => name, :attr => attributes, :block => block)
result = yield
@block_stack.pop
@attr_stack.pop
@tag_name_stack.pop
@tag_render_stack.pop
result
end

# Returns a fully qualified tag name based on state of the
# rendering stack.
# tag render stack.
def qualified_tag_name(name)
names = @tag_name_stack.dup
names = @tag_render_stack.collect { |item| item[:name] }
while names.size > 0
try = (names + [name]).join(':')
return try if @tags.has_key? try
return try if @tag_definitions.has_key? try
names.pop
end
name
end

# Helper method for normalizing options pased to tag definition methods
def prepare_tag_options(name, options)
options = Util.symbolize_keys(options)
options[:for] = (options.has_key?(:for) ? options[:for] : name)
options[:for] = options[:for].to_s unless options[:for].kind_of? Proc
options[:expose] = [*options[:expose]].compact.map { |m| m.to_s }
options[:item_tag] = (options.has_key?(:item_tag) ? options[:item_tag] : 'item').to_s
options
end

# Helper method for exposing the methods of an object as tags
def expose_methods_as_tags(name, options)
options = prepare_tag_options(name, options)
options[:expose].each do |method|
define_tag("#{name}:#{method}") do
object = get_for_object(options[:for])
object.send(method).to_s
end
end
end

# Helper method to return for option object.
def get_for_object(for_option)
case for_option
when Proc
instance_eval &for_option
else
instance_variable_get "@#{for_option}"
end
end
end

class Tag # :nodoc:
Expand Down
55 changes: 45 additions & 10 deletions radius/test/radius_test.rb
Expand Up @@ -3,18 +3,18 @@

module RadiusTestHelper
class TestContext < Radius::Context
attr_accessor :user
attr_accessor :user, :items

def initialize
super
@prefix = "r"
tag("reverse" ) { work.reverse }
tag("capitalize") { work.upcase }
define_tag("reverse" ) { expand.reverse }
define_tag("capitalize") { expand.upcase }
end
end

def define_tag(name, options = {}, &block)
@context.tag name, options, &block
@context.define_tag name, options, &block
end
end

Expand Down Expand Up @@ -69,6 +69,8 @@ class RadiusParserTest < Test::Unit::TestCase

def setup
@context = TestContext.new
@context.items = [4,2,8,5]

@parser = Radius::Parser.new(@context)
end

Expand All @@ -95,8 +97,8 @@ def test_parse_double_tags
end

def test_parse_nested
define_tag("outer") { work.reverse }
define_tag("outer:inner") { ["renni", work].join }
define_tag("outer") { expand.reverse }
define_tag("outer:inner") { ["renni", expand].join }
define_tag("outer:inner:heart") { "heart" }
define_tag("outer:branch") { "branch" }
assert_parse_output "inner", "<r:outer><r:inner /></r:outer>"
Expand All @@ -109,15 +111,15 @@ def test_parse_nested
def test_parse_loops
define_tag "each" do
result = []
["Larry", "Moe", "Curly"].each { |@item| result << work }
["Larry", "Moe", "Curly"].each { |@item| result << expand }
result.join(attr["between"] || "")
end
define_tag "item"
assert_parse_output %{Three Stooges: "Larry", "Moe", "Curly"}, %{Three Stooges: <r:each between=", ">"<r:item />"</r:each>}
end

class User
attr_accessor :name, :age, :email
attr_accessor :name, :age, :email, :friend
def initialize(name, age, email)
@name, @age, @email = name, age, email
end
Expand All @@ -131,15 +133,48 @@ def test_tag_expose_option
assert_raises(Radius::UndefinedTagError) { @parser.parse "<r:user:email />" }
end

def test_tag_for_option
def test_tag_option_for
define_tag 'tag_prefix', :for => 'prefix'
assert_parse_output 'r', '<r:tag_prefix />'

end

def test_tag_options_for_and_expose
@context.user = User.new('John', 25, 'test@johnwlong.com')
define_tag 'author', 'for' => :user, 'expose' => :name
assert_parse_output 'John', '<r:author:name />'
end

def test_tag_option_for_with_proc
@context.user = User.new('John', 25, 'test@johnwlong.com')
@context.user.friend = User.new('Jake', 23, 'test@jake.com')
define_tag 'author:friend', :for => proc { self.user.friend }, 'expose' => 'name'
assert_parse_output 'Jake', '<r:author:friend:name />'
end

def test_tag_option_type_is_enumerable
define_tag 'items', :type => :enumerable
assert_parse_output '4', '<r:items:size />'
assert_parse_output '4', '<r:items:count />'
assert_parse_output '4', '<r:items:length />'
assert_parse_output '8', '<r:items:max />'
assert_parse_output '2', '<r:items:min />'
assert_parse_output '(4)(2)(8)(5)', '<r:items:each>(<r:item />)</r:items:each>'
end
def test_tag_option_for_and_type_is_enumerable
define_tag 'array', :for => :items, :type => :enumerable, :item_tag => 'number', :expose => [:first, :last]
assert_parse_output '4', '<r:array:first />'
assert_parse_output '5', '<r:array:last />'
assert_parse_output '(4)(2)(8)(5)', '<r:array:each>(<r:number />)</r:array:each>'
end
def test_tag_option_type_is_enumerable_and_exposed_item
@context.items = [
User.new('John', 25, 'test@johnwlong.com'),
User.new('James', 27, 'test@jameslong.com')
]
define_tag 'users', :for => :items, :type => :enumerable, :item_tag => :user, :item_expose => [:name, :age]
assert_parse_output "* John (25)\n* James (27)\n", "<r:users:each>* <r:user:name /> (<r:user:age />)\n</r:users:each>"
end

def test_parse_fail_on_missing_end_tag
assert_raises(Radius::MissingEndTagError) { @parser.parse("<r:reverse>") }
assert_raises(Radius::MissingEndTagError) { @parser.parse("<r:reverse><r:capitalize></r:reverse>") }
Expand Down

0 comments on commit 8a0d3b6

Please sign in to comment.