diff --git a/lib/radius/parser/mixed_scanner.rb b/lib/radius/parser/mixed_scanner.rb
new file mode 100644
index 0000000..d157316
--- /dev/null
+++ b/lib/radius/parser/mixed_scanner.rb
@@ -0,0 +1,56 @@
+module Radius
+ class MixedScanner < Radius::Scanner
+
+ def scanner_regex(prefix = nil)
+ # allow for {prefix:tag}{/prefix:tag} and {tag} style syntax
+ %r{<#{prefix}:([\w:]+?)(\s+(?:\w+\s*=\s*(?:"[^"]*?"|'[^']*?')\s*)*|)(\/?)>|<\/#{prefix}:([\w:]+?)\s*>|\{#{prefix}:([\w:]+?)(\s+(?:\w+\s*=\s*(?:"[^"]*?"|'[^']*?')\s*)*|)(\/?)\}|\{\/#{prefix}:([\w:]+?)\s*\}|\{\s*([\w:]+?)(\s+(?:\w+\s*=\s*(?:"[^"]*?"|'[^']*?')\s*)*|)\}}
+ end
+
+ def operate(prefix, data)
+ data = Radius::OrdString.new data
+ @nodes = ['']
+
+ re = scanner_regex(prefix)
+ if md = re.match(data)
+ remainder = ''
+ while md
+ start_tag, attributes, self_enclosed, end_tag = $1, $2, $3, $4
+
+ flavor = self_enclosed == '/' ? :self : (start_tag ? :open : :close)
+
+ # if {prefix:tag}..{/prefix:tag} style syntax
+ if $5 or $8
+ start_tag, attributes, self_enclosed, end_tag = $5, $6, $7, $8
+ flavor = self_enclosed == '/' ? :self : (start_tag ? :open : :close)
+ end
+
+ # if {tag} style syntax without prefix and end tags
+ if $9
+ start_tag = $9
+ attributes = $10
+ flavor = :self
+ end
+
+ # save the part before the current match as a string node
+ @nodes << md.pre_match
+
+ # save the tag that was found as a tag hash node
+ @nodes << {:prefix=>prefix, :name=>(start_tag || end_tag), :flavor => flavor, :attrs => parse_attributes(attributes)}
+
+ # remember the part after the current match
+ remainder = md.post_match
+
+ # see if we find another tag in the remaining string
+ md = re.match(md.post_match)
+ end
+
+ # add the last remaining string after the last tag that was found as a string node
+ @nodes << remainder
+ else
+ @nodes << data
+ end
+
+ return @nodes
+ end
+ end
+end
\ No newline at end of file
diff --git a/test/mixed_test.rb b/test/mixed_test.rb
new file mode 100644
index 0000000..5e4d3c3
--- /dev/null
+++ b/test/mixed_test.rb
@@ -0,0 +1,324 @@
+require File.expand_path(File.dirname(__FILE__) + '/test_helper')
+require 'radius/parser/mixed_scanner'
+
+class RadiusMixedTest < Test::Unit::TestCase
+ include RadiusTestHelper
+
+ def setup
+ @context = new_context
+ @parser = Radius::Parser.new(@context, :tag_prefix => 'r',:scanner => Radius::MixedScanner.new)
+ end
+
+ def test_initialize
+ @parser = Radius::Parser.new
+ assert_kind_of Radius::Context, @parser.context
+ end
+
+ def test_sane_scanner_default
+ assert !Radius::Parser.new.scanner.is_a?(Radius::MixedScanner)
+ end
+
+ def test_initialize_with_params
+ @parser = Radius::Parser.new(:scanner => Radius::MixedScanner.new)
+ assert_kind_of Radius::MixedScanner, @parser.scanner
+ end
+
+
+ def test_parse_individual_tags_and_parameters
+ define_tag "add" do |tag|
+ tag.attr["param1"].to_i + tag.attr["param2"].to_i
+ end
+ assert_parse_output "<3>", %{<>}
+ assert_parse_output "{3}", %[{{r:add param1="1" param2='2'/}}]
+ assert_parse_output "{3}", %[{{add param1="1" param2='2'}}]
+ end
+
+ def test_parse_attributes
+ attributes = %{{"a"=>"1", "b"=>"2", "c"=>"3", "d"=>"'"}}
+ assert_parse_output attributes, %{}
+ assert_parse_output attributes, %{}
+ assert_parse_output attributes, %[{r:attr a="1" b='2'c="3"d="'" /}]
+ assert_parse_output attributes, %[{r:attr a="1" b='2'c="3"d="'"}{/r:attr}]
+ assert_parse_output attributes, %[{attr a="1" b='2'c="3"d="'"}]
+ end
+
+ def test_parse_attributes_with_slashes_or_angle_brackets
+ slash = %{{"slash"=>"/"}}
+ angle = %{{"angle"=>">"}}
+ assert_parse_output slash, %{}
+ assert_parse_output slash, %{}
+ assert_parse_output angle, %{}
+
+ assert_parse_output slash, %[{r:attr slash="/"}{/r:attr}]
+ assert_parse_output slash, %[{r:attr slash="/"}{r:attr /}{/r:attr}]
+ assert_parse_output angle, %[{r:attr angle=">"}{/r:attr}]
+
+ assert_parse_output slash, %[{attr slash="/"}]
+ assert_parse_output slash, %[{attr slash="/"}]
+ assert_parse_output angle, %[{attr angle=">"}]
+ end
+
+ def test_parse_quotes
+ assert_parse_output "test []", %{ }
+ assert_parse_output "test []", %[{r:echo value="test" /} {r:wrap attr="test"}{/r:wrap}]
+ assert_parse_output "test []", %[{echo value="test"} {wrap attr="test"}]
+ end
+
+ def test_things_that_should_be_left_alone
+ [
+ %{ test="2"="4" },
+ %{="2" }
+ ].each do |middle|
+ assert_parsed_is_unchanged ""
+ assert_parsed_is_unchanged ""
+ assert_parsed_is_unchanged "{r:attr#{middle}/}"
+ assert_parsed_is_unchanged "{r:attr#{middle}}"
+ assert_parsed_is_unchanged "{attr#{middle}}"
+ end
+ end
+
+ def test_tags_inside_html_tags
+ assert_parse_output %{tags in yo tags
},%{tags in yo tags
}
+ assert_parse_output %{tags in yo tags
},%{tags in yo tags
}
+ end
+
+ def test_parse_result_is_always_a_string
+ define_tag("twelve") { 12 }
+ assert_parse_output "12", ""
+ assert_parse_output "12", "{r:twelve /}"
+ assert_parse_output "12", "{twelve}"
+ end
+
+ def test_parse_double_tags
+ assert_parse_output "test".reverse, "test"
+ assert_parse_output "tset TEST", "test test"
+
+ assert_parse_output "test".reverse, "{r:reverse}test{/r:reverse}"
+ assert_parse_output "tset TEST", "{r:reverse}test{/r:reverse} {r:capitalize}test{/r:capitalize}"
+
+ end
+
+
+ def test_parse_tag_nesting
+ define_tag("parent", :for => '')
+ define_tag("parent:child", :for => '')
+ define_tag("extra", :for => '')
+ define_tag("nesting") { |tag| tag.nesting }
+ define_tag("extra:nesting") { |tag| tag.nesting.gsub(':', ' > ') }
+ define_tag("parent:child:nesting") { |tag| tag.nesting.gsub(':', ' * ') }
+ assert_parse_output "nesting", ""
+ assert_parse_output "parent:nesting", ""
+ assert_parse_output "extra > nesting", ""
+ assert_parse_output "parent * child * nesting", ""
+ assert_parse_output "parent > extra > nesting", ""
+ assert_parse_output "parent > child > extra > nesting", ""
+ assert_parse_output "parent * extra * child * nesting", ""
+ assert_parse_output "parent > extra > child > extra > nesting", ""
+ assert_parse_output "parent > extra > child > extra > nesting", ""
+ assert_parse_output "extra * parent * child * nesting", ""
+ assert_parse_output "extra > parent > nesting", ""
+ assert_parse_output "extra * parent * child * nesting", ""
+ assert_raises(Radius::UndefinedTagError) { @parser.parse("") }
+ end
+ def test_parse_tag_nesting_2
+ define_tag("parent", :for => '')
+ define_tag("parent:child", :for => '')
+ define_tag("content") { |tag| tag.nesting }
+ assert_parse_output 'parent:child:content', ''
+ end
+
+ def test_parse_tag__binding_do_missing
+ define_tag 'test' do |tag|
+ tag.missing!
+ end
+ e = assert_raises(Radius::UndefinedTagError) { @parser.parse("") }
+ assert_equal "undefined tag `test'", e.message
+ end
+
+ def test_parse_chirpy_bird
+ # :> chirp chirp
+ assert_parse_output "<:", "<:"
+ end
+
+ def test_parse_tag__binding_render_tag
+ define_tag('test') { |tag| "Hello #{tag.attr['name']}!" }
+ define_tag('hello') { |tag| tag.render('test', tag.attr) }
+ assert_parse_output 'Hello John!', ''
+ end
+
+ def test_accessing_tag_attributes_through_tag_indexer
+ define_tag('test') { |tag| "Hello #{tag['name']}!" }
+ assert_parse_output 'Hello John!', ''
+ end
+
+ def test_parse_tag__binding_render_tag_with_block
+ define_tag('test') { |tag| "Hello #{tag.expand}!" }
+ define_tag('hello') { |tag| tag.render('test') { tag.expand } }
+ assert_parse_output 'Hello John!', 'John'
+ end
+
+ def test_tag_locals
+ define_tag "outer" do |tag|
+ tag.locals.var = 'outer'
+ tag.expand
+ end
+ define_tag "outer:inner" do |tag|
+ tag.locals.var = 'inner'
+ tag.expand
+ end
+ define_tag "outer:var" do |tag|
+ tag.locals.var
+ end
+ assert_parse_output 'outer', ""
+ assert_parse_output 'outer:inner:outer', "::"
+ assert_parse_output 'outer:inner:outer:inner:outer', "::::"
+ assert_parse_output 'outer', ""
+ end
+
+ def test_tag_globals
+ define_tag "set" do |tag|
+ tag.globals.var = tag.attr['value']
+ ''
+ end
+ define_tag "var" do |tag|
+ tag.globals.var
+ end
+ assert_parse_output " true false", %{ }
+ end
+
+ def test_parse_loops
+ @item = nil
+ define_tag "each" do |tag|
+ result = []
+ ["Larry", "Moe", "Curly"].each do |item|
+ tag.locals.item = item
+ result << tag.expand
+ end
+ result.join(tag.attr["between"] || "")
+ end
+ define_tag "each:item" do |tag|
+ tag.locals.item
+ end
+ assert_parse_output %{Three Stooges: "Larry", "Moe", "Curly"}, %{Three Stooges: ""}
+ end
+
+ def test_parse_speed
+ define_tag "set" do |tag|
+ tag.globals.var = tag.attr['value']
+ ''
+ end
+ define_tag "var" do |tag|
+ tag.globals.var
+ end
+ parts = %w{decima nobis augue at facer processus commodo legentis odio lectorum dolore nulla esse lius qui nonummy ullamcorper erat ii notare}
+ multiplier = parts.map{|p| "#{p}=\"#{rand}\""}.join(' ')
+ assert_nothing_raised do
+ Timeout.timeout(10) do
+ assert_parse_output " false", %{ }
+ end
+ end
+ end
+
+ def test_tag_option_for
+ define_tag 'fun', :for => 'just for kicks'
+ assert_parse_output 'just for kicks', ''
+ end
+
+ def test_tag_expose_option
+ define_tag 'user', :for => users.first, :expose => ['name', :age]
+ assert_parse_output 'John', ''
+ assert_parse_output '25', ''
+ e = assert_raises(Radius::UndefinedTagError) { @parser.parse "" }
+ assert_equal "undefined tag `email'", e.message
+ end
+
+ def test_tag_expose_attributes_option_on_by_default
+ define_tag 'user', :for => user_with_attributes
+ assert_parse_output 'John', ''
+ end
+ def test_tag_expose_attributes_set_to_false
+ define_tag 'user_without_attributes', :for => user_with_attributes, :attributes => false
+ assert_raises(Radius::UndefinedTagError) { @parser.parse "" }
+ end
+
+ def test_tag_options_must_contain_a_for_option_if_methods_are_exposed
+ e = assert_raises(ArgumentError) { define_tag('fun', :expose => :today) { 'test' } }
+ assert_equal "tag definition must contain a :for option when used with the :expose option", e.message
+ end
+
+ def test_parse_fail_on_missing_end_tag
+ assert_raises(Radius::MissingEndTagError) { @parser.parse("") }
+ end
+
+ def test_parse_fail_on_wrong_end_tag
+ assert_raises(Radius::WrongEndTagError) { @parser.parse("") }
+ end
+
+ def test_parse_with_default_tag_prefix
+ @parser = Radius::Parser.new(@context)
+ define_tag("hello") { |tag| "Hello world!" }
+ assert_equal "Hello world!
", @parser.parse('
')
+ end
+
+ def test_parse_with_other_radius_like_tags
+ @parser = Radius::Parser.new(@context, :tag_prefix => "ralph")
+ define_tag('hello') { "hello" }
+ assert_equal "", @parser.parse("")
+ end
+
+ def test_copyin_global_values
+ @context.globals.foo = 'bar'
+ assert_equal 'bar', Radius::Parser.new(@context).context.globals.foo
+ end
+
+ def test_does_not_pollute_copied_globals
+ @context.globals.foo = 'bar'
+ parser = Radius::Parser.new(@context)
+ parser.context.globals.foo = '[baz]'
+ assert_equal 'bar', @context.globals.foo
+ end
+
+ def test_parse_with_other_namespaces
+ @parser = Radius::Parser.new(@context, :tag_prefix => 'r')
+ assert_equal "hello world", @parser.parse("hello world")
+ end
+
+ protected
+
+ def assert_parse_output(output, input, message = nil)
+ r = @parser.parse(input)
+ assert_equal(output, r, message)
+ end
+
+ def assert_parsed_is_unchanged(something)
+ assert_parse_output something, something
+ end
+
+ class User
+ attr_accessor :name, :age, :email, :friend
+ def initialize(name, age, email)
+ @name, @age, @email = name, age, email
+ end
+ def <=>(other)
+ name <=> other.name
+ end
+ end
+
+ class UserWithAttributes < User
+ def attributes
+ { :name => name, :age => age, :email => email }
+ end
+ end
+
+ def users
+ [
+ User.new('John', 25, 'test@johnwlong.com'),
+ User.new('James', 27, 'test@jameslong.com')
+ ]
+ end
+
+ def user_with_attributes
+ UserWithAttributes.new('John', 25, 'test@johnwlong.com')
+ end
+
+end