├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG ├── CONTRIBUTORS ├── Gemfile ├── LICENSE ├── QUICKSTART.rdoc ├── README.rdoc ├── Rakefile ├── lib ├── radius.rb └── radius │ ├── context.rb │ ├── delegating_open_struct.rb │ ├── error.rb │ ├── ord_string.rb │ ├── parse_tag.rb │ ├── parser.rb │ ├── parser │ ├── JavaScanner$Flavor.class │ ├── JavaScanner$Tag.class │ ├── JavaScanner.class │ ├── JavaScanner.java │ ├── JavaScanner.rl │ ├── java_scanner.jar │ ├── scanner.rb │ └── squiggle_scanner.rb │ ├── tag_binding.rb │ ├── tag_definitions.rb │ ├── utility.rb │ └── version.rb ├── radius.gemspec ├── tasks ├── rdoc.rake ├── rubinius.rake └── test.rake └── test ├── benchmarks.rb ├── context_test.rb ├── multithreaded_test.rb ├── ord_string_test.rb ├── parser_test.rb ├── quickstart_test.rb ├── squiggle_test.rb ├── test_helper.rb └── utility_test.rb /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | types: [opened, synchronize, reopened] 8 | branches: [ master ] 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | include: 16 | - ruby-version: '2.7.8' 17 | bundler-version: '2.4.22' 18 | - ruby-version: '3.0.7' 19 | bundler-version: '2.5.3' 20 | - ruby-version: '3.1.6' 21 | bundler-version: '2.6.2' 22 | - ruby-version: '3.3.6' 23 | bundler-version: '2.6.2' 24 | - ruby-version: '3.4.2' 25 | bundler-version: '2.6.2' 26 | - ruby-version: 'jruby' 27 | bundler-version: '2.6.1' 28 | 29 | steps: 30 | - uses: actions/checkout@v4 31 | - name: Set up Ruby 32 | uses: ruby/setup-ruby@v1 33 | with: 34 | ruby-version: ${{ matrix.ruby-version }} 35 | bundler: ${{ matrix.bundler-version }} 36 | bundler-cache: true 37 | - name: Run tests 38 | env: 39 | COVERALLS: ${{ matrix.ruby-version != 'jruby' }} 40 | JRUBY_OPTS: ${{ matrix.ruby-version == 'jruby' && '--debug' || '' }} 41 | run: bundle exec rake 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | doc 3 | *.dot 4 | pkg 5 | *.rbc 6 | *.gem 7 | coverage/* 8 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | = Change Log 2 | 3 | == Unreleased 4 | 5 | * Add OrdString coercion method to avoid extra allocations [saturnflyer] 6 | 7 | == 0.8.0 8 | * Ensure that the DelegatingOpenStruct is a true copy of the original [saturnflyer] 9 | * Avoid errors with frozen strings in Ruby 3.4+ [jeremyevans] 10 | * Drop support for EOL versions of Ruby 11 | 12 | == 0.7.5 13 | * Added support for tags and attributes containing dashes [yurivm, ronald] 14 | 15 | == 0.7.4 16 | * added support for Ruby 2+ 17 | 18 | == 0.7.1 19 | * Running tests with bundler requires 'rake' to be specified [jgay] 20 | 21 | == 0.7.0 22 | * Improve performance by switching back to regular expression parsing [jfahrenkrug] 23 | * Support for Rubinius [jlong] 24 | * Support for Ruby 1.9 [aemadrid] 25 | * More tests [aemadrid] 26 | * Fixed issue #5 - problem with other namespace tags [jemmyw] 27 | * Switched to Jeweler for better gem management [jlong] 28 | * Allow operation in a threaded environment (parser per-thread, shared context) 29 | * Allow switching scanners that tokenize templates. 30 | * Include SquiggleScanner to parse tags that look like "{ hello /}" 31 | 32 | == 0.6.1 33 | * Fixed a problem with non-tags that have no prefix or tag name (see test_parse_chirpy_bird) 34 | 35 | == 0.6.0 (private release) 36 | * Split radius.rb into multiple files. 37 | * Ported the really hairy regexes from Radius::Parser to a single Ragel machine. 38 | * Added and refactored tests. 39 | * Refactored Rakefile and other administrativia. 40 | 41 | == 0.5.1 42 | * Fixed a problem with parsing quotes where a single tag preceding a double tag would consume the start tag of the double tag if both contained attributes. 43 | 44 | == 0.5.0 45 | * Created a DSL for tag definitions (introducing a DSL makes this version of Radiant incompatible with the last). The DSL has the following features: 46 | - full support for nested tags 47 | - global and local tag variables 48 | - Contexts can now be defined dynamically (instead of being subclassed) 49 | - see the QUICKSTART for more info 50 | * Many refactorings of the library and unit tests. 51 | * Changed the license to the MIT-LICENSE. 52 | * Updated documentation to reflect the changes. 53 | * Updated the version number to reflect the maturity of the code base. 54 | 55 | == 0.0.2 56 | * Refactored Parser to use Context#render_tag instead of #send when rendering tags defined on a Context. 57 | * UndefinedTagError is now thrown when Parser tries to render a tag which doesn't exist on a Context. 58 | * Added Context#tag_missing which works like method_method missing on Object, but is tag specific. 59 | 60 | == 0.0.1 61 | * First release. 62 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | CONTRIBUTORS 2 | ============ 3 | 4 | The following people have committed code to Radius. Thank you! 5 | 6 | John W. Long 7 | Todd Willey 8 | Bryce Kerley 9 | Jim Gay 10 | Johannes Fahrenkrug 11 | Adrian Madrid 12 | Benny Degezelle 13 | Moritz Heidkamp 14 | Jeremy 15 | Jason Garber 16 | David Chelimsky 17 | Yuri Veremeyenko 18 | Mr Ronald 19 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | group :test do 6 | gem 'rdoc' 7 | gem 'rake' 8 | gem 'kramdown' 9 | gem "simplecov" 10 | gem 'coveralls_reborn', :require => false 11 | gem 'minitest' 12 | end 13 | 14 | 15 | platforms :rbx do 16 | gem 'racc' # if using gems like ruby_parser or parser 17 | # gem 'rubysl', '~> 2.0' 18 | # gem 'psych' 19 | gem 'rubinius-developer_tools' 20 | end 21 | 22 | platforms :jruby do 23 | gem 'jar-dependencies', '~> 0.4.1' 24 | gem 'ruby-maven', '~> 3.3.11' 25 | end 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2006-2010, John W. Long 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /QUICKSTART.rdoc: -------------------------------------------------------------------------------- 1 | = Radius Quick Start 2 | 3 | 4 | == Defining Tags 5 | 6 | Before you can parse a template with Radius you need to create a Context object which defines 7 | the tags that will be used in the template. This is actually quite simple: 8 | 9 | require 'radius' 10 | 11 | context = Radius::Context.new 12 | context.define_tag "hello" do |tag| 13 | "Hello #{tag.attr['name'] || 'World'}!" 14 | end 15 | 16 | Once you have defined a context you can easily create a Parser: 17 | 18 | parser = Radius::Parser.new(context) 19 | puts parser.parse('

') 20 | puts parser.parse('

') 21 | 22 | This code will output: 23 | 24 |

Hello World!

25 |

Hello John!

26 | 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 | 34 | == Container Tags 35 | 36 | Radius also allows you to define "container" tags. That is, tags that contain content and 37 | that may optionally manipulate it in some way. For example, if you have RedCloth installed 38 | you could define another tag to parse and create Textile output: 39 | 40 | require 'redcloth' 41 | 42 | context.define_tag "textile" do |tag| 43 | contents = tag.expand 44 | RedCloth.new(contents).to_html 45 | end 46 | 47 | (The code tag.expand above returns the contents of the template between the start and end 48 | tags.) 49 | 50 | With the code above your parser can easily handle Textile: 51 | 52 | parser.parse('h1. Hello **World**!') 53 | 54 | This code will output: 55 | 56 |

Hello World!

57 | 58 | 59 | == Nested Tags 60 | 61 | But wait!--it gets better. Because container tags can manipulate the content they contain 62 | you can use them to iterate over collections: 63 | 64 | context = Radius::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 | end 72 | content 73 | end 74 | 75 | context.define_tag "stooge:name" do |tag| 76 | tag.locals.name 77 | end 78 | 79 | parser = Radius::Parser.new(context) 80 | 81 | template = <<-TEMPLATE 82 | 87 | TEMPLATE 88 | 89 | puts parser.parse(template) 90 | 91 | This code will output: 92 | 93 | 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 1 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 user 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 | 135 | 136 | If "John" was the value stored in user.name the template would render as "John". 137 | 138 | 139 | == Tag Shorthand 140 | 141 | In the example above we made reference to user.name in our template by using the 142 | following code: 143 | 144 | 145 | 146 | There is a much easer way to refer to the user.name variable. Use the colon operator 147 | to "scope" the reference to name: 148 | 149 | 150 | 151 | Radius allows you to use this shortcut for all tags. 152 | 153 | 154 | == Changing the Tag Prefix 155 | 156 | By default, all Radius tags must begin with "radius". You can change this by altering the 157 | tag_prefix attribute on a Parser. For example: 158 | 159 | parser = Radius::Parser.new(context, :tag_prefix => 'r') 160 | 161 | Now, when parsing templates with +parser+, Radius will require that every tag begin with "r" 162 | instead of "radius". 163 | 164 | 165 | == Custom Behavior for Undefined Tags 166 | 167 | Context#tag_missing behaves much like Object#method_missing only it allows you to define 168 | specific behavior for when a tag is not defined on a Context. For example: 169 | 170 | class LazyContext < Radius::Context 171 | def tag_missing(tag, attr, &block) 172 | "ERROR: Undefined tag `#{tag}' with attributes #{attr.inspect}" 173 | end 174 | end 175 | 176 | parser = Radius::Parser.new(LazyContext.new, :tag_prefix => 'lazy') 177 | puts parser.parse('') 178 | 179 | This will output: 180 | 181 | ERROR: Undefined tag `weird' with attributes {"value"=>"true"} 182 | 183 | Normally, when the Radius Parser encounters an undefined tag for a Context it raises an 184 | UndefinedTagError, but since we have defined #tag_missing on LazyContext the Parser now 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 | "" 210 | end 211 | 212 | context.define_tag "count" do |tag| 213 | tag.globals.count || 0 214 | end 215 | 216 | TagBinding#locals mirrors the variables that are in TagBinding#globals, but allows child 217 | tags to redefine variables. This is valuable when defining context sensitive tags: 218 | 219 | class Person 220 | attr_accessor :name, :friend 221 | def initialize(name) 222 | @name = name 223 | end 224 | end 225 | 226 | jack = Person.new('Jack') 227 | jill = Person.new('Jill') 228 | jack.friend = jill 229 | jill.friend = jack 230 | 231 | context = Radius::Context.new do |c| 232 | c.define_tag "jack" do |tag| 233 | tag.locals.person = jack 234 | tag.expand 235 | end 236 | c.define_tag "jill" do |tag| 237 | tag.locals.person = jill 238 | tag.expand 239 | end 240 | c.define_tag "name" do |tag| 241 | tag.locals.person.name rescue tag.missing! 242 | end 243 | c.define_tag "friend" do |tag| 244 | tag.locals.person = tag.locals.person.friend rescue tag.missing! 245 | tag.expand 246 | end 247 | end 248 | 249 | parser = Radius::Parser.new(context, :tag_prefix => 'r') 250 | 251 | parser.parse('') #=> "Jack" 252 | parser.parse('') #=> "Jill" 253 | parser.parse('') #=> "Jack" 254 | parser.parse('') #=> "Jack" 255 | parser.parse(' and ') #=> "Jack and Jill" 256 | parser.parse('') # raises a Radius::UndefinedTagError exception 257 | 258 | Notice how TagBinding#locals enables intelligent nesting. "" evaluates to 259 | "Jill", but "" evaluates to "Jack". Locals lose scope as soon as 260 | the tag they were defined in closes. Globals on the other hand, never lose scope. 261 | 262 | The final line in the example above demonstrates that calling "" raises a 263 | TagMissing error. This is because of the way the name tag was defined: 264 | 265 | tag.locals.person.name rescue tag.missing! 266 | 267 | If person is not defined on locals it will return nil. Calling #name on nil would normally 268 | raise a NoMethodError exception, but because of the 'rescue' clause the TagBinding#missing! 269 | method is called which fires off Context#tag_missing. By default Context#tag_missing raises 270 | a UndefinedTagError exception. The 'rescue tag.missing!' idiom is extremly useful for adding 271 | simple error checking to context sensitive tags. 272 | 273 | 274 | == Tag Specificity 275 | 276 | When Radius is presented with two tags that have the same name, but different nesting 277 | Radius uses an algorithm similar to the way winning rules are calculated in Cascading Style 278 | Sheets (CSS) to determine which definition should be used. Each time a tag is encountered 279 | in a template potential tags are assigned specificity values and the tag with the highest 280 | specificity wins. 281 | 282 | For example, given the following tag definitions: 283 | 284 | nesting 285 | extra:nesting 286 | parent:child:nesting 287 | 288 | And template: 289 | 290 | 291 | 292 | Radius will calculate specificity values like this: 293 | 294 | nesting => 1.0.0.0 295 | extra:nesting => 1.0.1.0 296 | parent:child:nesting => 1.1.0.1 297 | 298 | Meaning that parent:child:nesting will win. If a template contained: 299 | 300 | 301 | 302 | The following specificity values would be assigned to each of the tag definitions: 303 | 304 | nesting => 1.0.0.0 305 | extra:nesting => 1.1.0.0 306 | parent:child:nesting => 1.0.1.1 307 | 308 | Meaning that extra:nesting would win because it is more "specific". 309 | 310 | Values are assigned by assigning points to each of the tags from right to left. 311 | Given a tag found in a template with nesting four levels deep, the maximum 312 | specificity a tag could be assigned would be: 313 | 314 | 1.1.1.1 315 | 316 | One point for each of the levels. 317 | 318 | In practice, you don't need to understand this topic to be effective with Radius. 319 | For the most part you will find that Radius resolves tags precisely the way that 320 | you would expect. If you find this section confusing forget about it and refer 321 | back to it if you find that tags are resolving differently from the way that you 322 | expected. 323 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = Radius -- Powerful Tag-Based Templates 2 | 3 | {rdoc-image:https://github.com/jlong/radius/workflows/CI/badge.svg}[https://github.com/jlong/radius/actions] {rdoc-image:https://codeclimate.com/github/jlong/radius.png}[https://codeclimate.com/github/jlong/radius] {rdoc-image:https://coveralls.io/repos/jlong/radius/badge.png}[https://coveralls.io/r/jlong/radius] 4 | 5 | 6 | Radius is a powerful tag-based template language for Ruby inspired by the 7 | template languages used in MovableType[http://www.movabletype.org] and 8 | TextPattern[http://www.textpattern.com]. It uses tags similar to XML, but can 9 | be used to generate any form of plain text (HTML, e-mail, etc...). 10 | 11 | == Usage 12 | 13 | With Radius, it is extremely easy to create custom tags and parse them. Here's a small 14 | example: 15 | 16 | require 'radius' 17 | 18 | # Define tags on a context that will be available to a template: 19 | context = Radius::Context.new do |c| 20 | c.define_tag 'hello' do 21 | 'Hello world' 22 | end 23 | c.define_tag 'repeat' do |tag| 24 | number = (tag.attr['times'] || '1').to_i 25 | result = '' 26 | number.times { result << tag.expand } 27 | result 28 | end 29 | end 30 | 31 | # Create a parser to parse tags that begin with 'r:' 32 | parser = Radius::Parser.new(context, :tag_prefix => 'r') 33 | 34 | # Parse tags and output the result 35 | puts parser.parse(%{A small example:\n* !\n}) 36 | 37 | Output: 38 | 39 | A small example: 40 | * Hello world! 41 | * Hello world! 42 | * Hello world! 43 | 44 | 45 | == Quick Start 46 | 47 | Read the QUICKSTART file to get up and running with Radius. 48 | 49 | 50 | == Requirements 51 | 52 | Radius does not have any external requirements for using the library in your 53 | own programs. 54 | 55 | Ragel is required to create the ruby parser from the Ragel specification, 56 | and both Ragel and Graphviz are required to draw the state graph for the 57 | parser. 58 | 59 | 60 | == Installation 61 | 62 | It is recommended that you install Radius using the RubyGems packaging system: 63 | 64 | % gem install --remote radius 65 | 66 | 67 | == License 68 | 69 | Radius is released under the MIT license and is copyright (c) 2006-2025 70 | John W. Long. A copy of the MIT license can be found in the LICENSE file. 71 | 72 | 73 | == Roadmap 74 | 75 | This is a prioritized roadmap for future releases: 76 | 77 | 1. Clean up the current code base. [Done] 78 | 79 | 2. Add support for multi-level contexts: tags should be able to be 80 | defined to only be valid within other sets of tags. [Done] 81 | 82 | 3. Create a simple DSL for defining contexts. [Done] 83 | 84 | 4. Optimize for speed, modify scan.rl to emit C. 85 | 86 | 87 | == Development 88 | 89 | The latest version of Radius can be found on RubyForge: 90 | 91 | http://github.com/jlong/radius 92 | 93 | If you are interested in helping with the development of Radius, feel free to 94 | fork the project on GitHub and send me a pull request. 95 | 96 | 97 | John Long :: 98 | http://wiseheartdesign.com 99 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rake' 3 | 4 | require File.dirname(__FILE__) + '/lib/radius/version' 5 | 6 | Dir['tasks/**/*.rake'].each { |t| load t } 7 | 8 | task :default => :test 9 | 10 | namespace :build do 11 | desc "Build both Ruby and Java platform gems" 12 | task :all do 13 | version = ::Radius.version 14 | system "gem build radius.gemspec --platform ruby -o radius-#{version}.gem" 15 | system "gem build radius.gemspec --platform java -o radius-#{version}-java.gem" 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/radius.rb: -------------------------------------------------------------------------------- 1 | require_relative "radius/version" 2 | require_relative "radius/error" 3 | require_relative "radius/tag_definitions" 4 | require_relative "radius/delegating_open_struct" 5 | require_relative "radius/tag_binding" 6 | require_relative "radius/context" 7 | require_relative "radius/parse_tag" 8 | require_relative "radius/ord_string" 9 | require_relative "radius/parser/scanner" 10 | require_relative "radius/parser" 11 | require_relative "radius/utility" 12 | -------------------------------------------------------------------------------- /lib/radius/context.rb: -------------------------------------------------------------------------------- 1 | module Radius 2 | # 3 | # A context contains the tag definitions which are available for use in a template. 4 | # See the QUICKSTART for a detailed explaination its 5 | # usage. 6 | # 7 | class Context 8 | # A hash of tag definition blocks that define tags accessible on a Context. 9 | attr_accessor :definitions # :nodoc: 10 | attr_accessor :globals # :nodoc: 11 | 12 | # Creates a new Context object. 13 | def initialize(*args, &block) 14 | @definitions = {} 15 | @tag_binding_stack = [] 16 | @globals = DelegatingOpenStruct.new 17 | with(&block) if block_given? 18 | end 19 | 20 | # Yield an instance of self for tag definitions: 21 | # 22 | # context.with do |c| 23 | # c.define_tag 'test' do 24 | # 'test' 25 | # end 26 | # end 27 | # 28 | def with 29 | yield self 30 | self 31 | end 32 | 33 | # Creates a tag definition on a context. Several options are available to you 34 | # when creating a tag: 35 | # 36 | # +for+:: Specifies an object that the tag is in reference to. This is 37 | # applicable when a block is not passed to the tag, or when the 38 | # +expose+ option is also used. 39 | # 40 | # +expose+:: Specifies that child tags should be set for each of the methods 41 | # contained in this option. May be either a single symbol/string or 42 | # an array of symbols/strings. 43 | # 44 | # +attributes+:: Specifies whether or not attributes should be exposed 45 | # automatically. Useful for ActiveRecord objects. Boolean. Defaults 46 | # to +true+. 47 | # 48 | def define_tag(name, options = {}, &block) 49 | type = Utility.impartial_hash_delete(options, :type).to_s 50 | klass = Utility.constantize('Radius::TagDefinitions::' + Utility.camelize(type) + 'TagFactory') rescue raise(ArgumentError.new("Undefined type `#{type}' in options hash")) 51 | klass.new(self).define_tag(name, options, &block) 52 | end 53 | 54 | # Returns the value of a rendered tag. Used internally by Parser#parse. 55 | def render_tag(name, attributes = {}, &block) 56 | if name =~ /^(.+?):(.+)$/ 57 | render_tag($1) { render_tag($2, attributes, &block) } 58 | else 59 | tag_definition_block = @definitions[qualified_tag_name(name.to_s)] 60 | if tag_definition_block 61 | # puts name: name, attributes: attributes 62 | stack(name, attributes, block) do |tag| 63 | tag_definition_block.call(tag).to_s 64 | end 65 | else 66 | tag_missing(name, attributes, &block) 67 | end 68 | end 69 | end 70 | 71 | # Like method_missing for objects, but fired when a tag is undefined. 72 | # Override in your own Context to change what happens when a tag is 73 | # undefined. By default this method raises an UndefinedTagError. 74 | def tag_missing(name, attributes, &block) 75 | raise UndefinedTagError.new(name) 76 | end 77 | 78 | # Returns the state of the current render stack. Useful from inside 79 | # a tag definition. Normally just use TagBinding#nesting. 80 | def current_nesting 81 | @tag_binding_stack.collect { |tag| tag.name }.join(':') 82 | end 83 | 84 | # make a usable copy of this context 85 | def dup # :nodoc: 86 | rv = self.class.new 87 | rv.globals = globals.dup 88 | rv.definitions = definitions.dup 89 | rv 90 | end 91 | 92 | private 93 | 94 | # A convienence method for managing the various parts of the 95 | # tag binding stack. 96 | def stack(name, attributes, block) 97 | previous = @tag_binding_stack.last 98 | previous_locals = previous.nil? ? globals : previous.locals 99 | locals = previous_locals.dup 100 | binding = TagBinding.new(self, locals, name, attributes, block) 101 | @tag_binding_stack.push(binding) 102 | result = yield(binding) 103 | @tag_binding_stack.pop 104 | result 105 | end 106 | 107 | # Returns a fully qualified tag name based on state of the 108 | # tag binding stack. 109 | def qualified_tag_name(name) 110 | nesting_parts = @tag_binding_stack.collect { |tag| tag.name } 111 | nesting_parts << name unless nesting_parts.last == name 112 | specific_name = nesting_parts.join(':') # specific_name always has the highest specificity 113 | unless @definitions.has_key? specific_name 114 | possible_matches = @definitions.keys.grep(/(^|:)#{name}$/) 115 | specificity = possible_matches.inject({}) { |hash, tag| hash[numeric_specificity(tag, nesting_parts)] = tag; hash } 116 | max = specificity.keys.max 117 | if max != 0 118 | specificity[max] 119 | else 120 | name 121 | end 122 | else 123 | specific_name 124 | end 125 | end 126 | 127 | # Returns the specificity for +tag_name+ at nesting defined 128 | # by +nesting_parts+ as a number. 129 | def numeric_specificity(tag_name, nesting_parts) 130 | nesting_parts = nesting_parts.dup 131 | name_parts = tag_name.split(':') 132 | specificity = 0 133 | value = 1 134 | if nesting_parts.last == name_parts.last 135 | while nesting_parts.size > 0 136 | if nesting_parts.last == name_parts.last 137 | specificity += value 138 | name_parts.pop 139 | end 140 | nesting_parts.pop 141 | value *= 0.1 142 | end 143 | specificity = 0 if (name_parts.size > 0) 144 | end 145 | specificity 146 | end 147 | end 148 | end 149 | -------------------------------------------------------------------------------- /lib/radius/delegating_open_struct.rb: -------------------------------------------------------------------------------- 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 dup 11 | self.class.new.tap do |copy| 12 | copy.instance_variable_set(:@hash, @hash.dup) 13 | copy.object = @object 14 | end 15 | end 16 | 17 | def method_missing(method, *args, &block) 18 | return super if args.size > 1 19 | 20 | symbol = method.to_s.chomp('=').to_sym 21 | 22 | if method.to_s.end_with?('=') 23 | @hash[symbol] = args.first 24 | else 25 | @hash.fetch(symbol) { @object&.public_send(method, *args, &block) } 26 | end 27 | end 28 | 29 | def respond_to_missing?(method, include_private = false) 30 | (args.size <= 1) || super 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/radius/error.rb: -------------------------------------------------------------------------------- 1 | module Radius 2 | # Abstract base class for all parsing errors. 3 | class ParseError < StandardError 4 | end 5 | 6 | # Occurs when the Parser expects an end tag for one tag and finds the end tag for another. 7 | class WrongEndTagError < ParseError 8 | def initialize(expected_tag, got_tag, stack) 9 | stack_message = " with stack #{stack.inspect}" if stack 10 | super("wrong end tag `#{got_tag}' found for start tag `#{expected_tag}'#{stack_message}") 11 | end 12 | end 13 | 14 | # Occurs when Parser cannot find an end tag for a given tag in a template or when 15 | # tags are miss-matched in a template. 16 | class MissingEndTagError < ParseError 17 | # Create a new MissingEndTagError object for +tag_name+. 18 | def initialize(tag_name, stack) 19 | stack_message = " with stack #{stack.inspect}" if stack 20 | super("end tag not found for start tag `#{tag_name}'#{stack_message}") 21 | end 22 | end 23 | 24 | # Occurs when Context#render_tag cannot find the specified tag on a Context. 25 | class UndefinedTagError < ParseError 26 | # Create a new UndefinedTagError object for +tag_name+. 27 | def initialize(tag_name) 28 | super("undefined tag `#{tag_name}'") 29 | end 30 | end 31 | 32 | class TastelessTagError < ParseError #:nodoc: 33 | def initialize(tag, stack) 34 | super("internal error with tasteless tag #{tag.inspect} and stack #{stack.inspect}") 35 | end 36 | end 37 | 38 | class UndefinedFlavorError < ParseError #:nodoc: 39 | def initialize(tag, stack) 40 | super("internal error with unknown flavored tag #{tag.inspect} and stack #{stack.inspect}") 41 | end 42 | end 43 | end -------------------------------------------------------------------------------- /lib/radius/ord_string.rb: -------------------------------------------------------------------------------- 1 | module Radius 2 | module_function def OrdString(string_or_ord_string) 3 | if string_or_ord_string.is_a?(OrdString) 4 | string_or_ord_string 5 | else 6 | OrdString.new(string_or_ord_string) 7 | end 8 | end 9 | 10 | class OrdString < String 11 | def [](*args) 12 | if args.size == 1 && args.first.is_a?(Integer) 13 | slice(args.first).ord 14 | else 15 | super 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/radius/parse_tag.rb: -------------------------------------------------------------------------------- 1 | module Radius 2 | class ParseTag # :nodoc: 3 | def initialize(&b) 4 | @block = b 5 | end 6 | 7 | def on_parse(&b) 8 | @block = b 9 | end 10 | 11 | def to_s 12 | @block.call(self) if @block 13 | end 14 | end 15 | 16 | class ParseContainerTag < ParseTag # :nodoc: 17 | attr_accessor :name, :attributes, :contents 18 | 19 | def initialize(name = "", attributes = {}, contents = [], &b) 20 | @name, @attributes, @contents = name, attributes, contents 21 | super(&b) 22 | end 23 | end 24 | end -------------------------------------------------------------------------------- /lib/radius/parser.rb: -------------------------------------------------------------------------------- 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 | # The class that performs tokenization of the input string 16 | attr_accessor :scanner 17 | 18 | # Creates a new parser object initialized with a Context. 19 | def initialize(context = Context.new, options = {}) 20 | context, options = normalize_initialization_params(context, options) 21 | @context = context ? context.dup : Context.new 22 | @tag_prefix = options[:tag_prefix] || 'radius' 23 | @scanner = options[:scanner] || default_scanner 24 | @stack = nil # Pre-initialize stack 25 | end 26 | 27 | # Parses string for tags, expands them, and returns the result. 28 | def parse(string) 29 | @stack = [create_root_container] 30 | process_tokens(scanner.operate(tag_prefix, string)) 31 | @stack.last.to_s 32 | end 33 | 34 | private 35 | 36 | def normalize_initialization_params(context, options) 37 | return [context['context'] || context[:context], context] if context.is_a?(Hash) && options.empty? 38 | [context, Utility.symbolize_keys(options)] 39 | end 40 | 41 | def create_root_container 42 | ParseContainerTag.new { |t| Utility.array_to_s(t.contents) } 43 | end 44 | 45 | def process_tokens(tokens) 46 | tokens.each { |token| process_token(token) } 47 | validate_final_stack 48 | end 49 | 50 | def process_token(token) 51 | return @stack.last.contents << token if token.is_a?(String) 52 | 53 | case token[:flavor] 54 | when :open then handle_open_tag(token) 55 | when :self then handle_self_tag(token) 56 | when :close then handle_close_tag(token) 57 | when :tasteless then raise TastelessTagError.new(token, @stack) 58 | else raise UndefinedFlavorError.new(token, @stack) 59 | end 60 | end 61 | 62 | def handle_open_tag(token) 63 | @stack.push(ParseContainerTag.new(token[:name], token[:attrs])) 64 | end 65 | 66 | def handle_self_tag(token) 67 | @stack.last.contents << ParseTag.new { @context.render_tag(token[:name], token[:attrs]) } 68 | end 69 | 70 | def handle_close_tag(token) 71 | popped = @stack.pop 72 | validate_tag_match(popped, token[:name]) 73 | wrap_and_push_tag(popped) 74 | end 75 | 76 | def validate_tag_match(popped, name) 77 | raise WrongEndTagError.new(popped.name, name, @stack) if popped.name != name 78 | end 79 | 80 | def wrap_and_push_tag(tag) 81 | tag.on_parse { |b| @context.render_tag(tag.name, tag.attributes) { Utility.array_to_s(b.contents) } } 82 | @stack.last.contents << tag 83 | end 84 | 85 | def validate_final_stack 86 | raise MissingEndTagError.new(@stack.last.name, @stack) if @stack.length != 1 87 | end 88 | 89 | def default_scanner 90 | if RUBY_PLATFORM == 'java' 91 | load_java_scanner 92 | else 93 | Radius::Scanner.new 94 | end 95 | end 96 | 97 | def load_java_scanner 98 | if Gem::Version.new(JRUBY_VERSION) >= Gem::Version.new('9.3') 99 | require 'jruby' 100 | else 101 | require 'java' 102 | end 103 | require 'radius/parser/java_scanner.jar' 104 | ::Radius.send(:include_package, 'radius.parser') 105 | Radius::JavaScanner.new(JRuby.runtime) 106 | end 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /lib/radius/parser/JavaScanner$Flavor.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlong/radius/7f7347b156d01ffd499dddd412b94a8eb2882c62/lib/radius/parser/JavaScanner$Flavor.class -------------------------------------------------------------------------------- /lib/radius/parser/JavaScanner$Tag.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlong/radius/7f7347b156d01ffd499dddd412b94a8eb2882c62/lib/radius/parser/JavaScanner$Tag.class -------------------------------------------------------------------------------- /lib/radius/parser/JavaScanner.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlong/radius/7f7347b156d01ffd499dddd412b94a8eb2882c62/lib/radius/parser/JavaScanner.class -------------------------------------------------------------------------------- /lib/radius/parser/JavaScanner.java: -------------------------------------------------------------------------------- 1 | 2 | // line 1 "JavaScanner.rl" 3 | 4 | // line 101 "JavaScanner.rl" 5 | 6 | 7 | package radius.parser; 8 | 9 | import java.util.HashMap; 10 | import java.util.LinkedList; 11 | import org.jruby.Ruby; // runtime 12 | import org.jruby.RubyObject; 13 | import org.jruby.runtime.builtin.IRubyObject; 14 | import org.jruby.RubyArray; 15 | import org.jruby.RubyString; 16 | import org.jruby.RubyHash; 17 | import org.jruby.RubySymbol; 18 | 19 | public class JavaScanner { 20 | 21 | Ruby runtime = null; 22 | RubyArray rv = null; 23 | 24 | void pass_through(String str) { 25 | RubyObject last = ((RubyObject)rv.last()); 26 | if ( rv.size() > 0 && last != null && (last instanceof RubyString) ){ 27 | // XXX concat changes for ruby 1.9 28 | ((RubyString) last).concat(RubyString.newString(runtime, str)); 29 | } else { 30 | rv.append(RubyString.newString(runtime, str)); 31 | } 32 | } 33 | 34 | void tag(String prefix, String name, RubyHash attr, RubySymbol flavor) { 35 | // Validate both prefix and name 36 | if ((prefix == null || prefix.trim().isEmpty()) && 37 | (name == null || name.trim().isEmpty())) { 38 | pass_through("<"); 39 | return; 40 | } 41 | 42 | if (name == null || name.trim().isEmpty()) { 43 | pass_through("<" + prefix + ":"); 44 | return; 45 | } 46 | 47 | RubyHash tag = RubyHash.newHash(runtime); 48 | tag.op_aset( 49 | runtime.getCurrentContext(), 50 | RubySymbol.newSymbol(runtime, "prefix"), 51 | RubyString.newString(runtime, prefix != null ? prefix : "") 52 | ); 53 | tag.op_aset( 54 | runtime.getCurrentContext(), 55 | RubySymbol.newSymbol(runtime, "name"), 56 | RubyString.newString(runtime, name) 57 | ); 58 | tag.op_aset( 59 | runtime.getCurrentContext(), 60 | RubySymbol.newSymbol(runtime, "attrs"), 61 | attr 62 | ); 63 | tag.op_aset( 64 | runtime.getCurrentContext(), 65 | RubySymbol.newSymbol(runtime, "flavor"), 66 | flavor 67 | ); 68 | rv.append(tag); 69 | } 70 | 71 | public JavaScanner(Ruby runtime) { 72 | this.runtime = runtime; 73 | } 74 | 75 | 76 | // line 77 "JavaScanner.java" 77 | private static byte[] init__parser_actions_0() 78 | { 79 | return new byte [] { 80 | 0, 1, 0, 1, 3, 1, 4, 1, 5, 1, 6, 1, 81 | 7, 1, 8, 1, 9, 1, 10, 1, 14, 1, 15, 1, 82 | 19, 1, 21, 1, 22, 1, 23, 2, 1, 2, 2, 5, 83 | 6, 2, 6, 7, 2, 9, 5, 2, 9, 10, 2, 10, 84 | 9, 2, 11, 20, 2, 12, 20, 2, 13, 20, 2, 16, 85 | 17, 2, 16, 18, 3, 5, 6, 7, 3, 9, 5, 6, 86 | 3, 16, 6, 17, 4, 9, 5, 6, 7, 4, 16, 5, 87 | 6, 17, 5, 16, 9, 5, 6, 17 88 | }; 89 | } 90 | 91 | private static final byte _parser_actions[] = init__parser_actions_0(); 92 | 93 | 94 | private static short[] init__parser_key_offsets_0() 95 | { 96 | return new short [] { 97 | 0, 0, 11, 21, 35, 48, 62, 66, 71, 73, 75, 88, 98 | 101, 102, 104, 119, 134, 150, 156, 162, 177, 180, 183, 186, 99 | 201, 203, 205, 220, 236, 242, 248, 251, 254, 270, 286, 303, 100 | 310, 316, 332, 336, 352, 367, 370, 372, 382, 392, 403, 413, 101 | 428, 432, 442, 442, 443, 452, 452, 452, 454, 456, 459, 462, 102 | 464, 466 103 | }; 104 | } 105 | 106 | private static final short _parser_key_offsets[] = init__parser_key_offsets_0(); 107 | 108 | 109 | private static char[] init__parser_trans_keys_0() 110 | { 111 | return new char [] { 112 | 58, 63, 95, 45, 46, 48, 57, 65, 90, 97, 122, 63, 113 | 95, 45, 46, 48, 57, 65, 90, 97, 122, 32, 47, 58, 114 | 62, 63, 95, 9, 13, 45, 57, 65, 90, 97, 122, 32, 115 | 47, 62, 63, 95, 9, 13, 45, 58, 65, 90, 97, 122, 116 | 32, 61, 63, 95, 9, 13, 45, 46, 48, 58, 65, 90, 117 | 97, 122, 32, 61, 9, 13, 32, 34, 39, 9, 13, 34, 118 | 92, 34, 92, 32, 47, 62, 63, 95, 9, 13, 45, 58, 119 | 65, 90, 97, 122, 32, 47, 62, 63, 95, 9, 13, 45, 120 | 58, 65, 90, 97, 122, 62, 34, 92, 32, 34, 47, 62, 121 | 63, 92, 95, 9, 13, 45, 58, 65, 90, 97, 122, 32, 122 | 34, 47, 62, 63, 92, 95, 9, 13, 45, 58, 65, 90, 123 | 97, 122, 32, 34, 61, 63, 92, 95, 9, 13, 45, 46, 124 | 48, 58, 65, 90, 97, 122, 32, 34, 61, 92, 9, 13, 125 | 32, 34, 39, 92, 9, 13, 32, 34, 47, 62, 63, 92, 126 | 95, 9, 13, 45, 58, 65, 90, 97, 122, 34, 62, 92, 127 | 34, 39, 92, 34, 39, 92, 32, 39, 47, 62, 63, 92, 128 | 95, 9, 13, 45, 58, 65, 90, 97, 122, 39, 92, 39, 129 | 92, 32, 39, 47, 62, 63, 92, 95, 9, 13, 45, 58, 130 | 65, 90, 97, 122, 32, 39, 61, 63, 92, 95, 9, 13, 131 | 45, 46, 48, 58, 65, 90, 97, 122, 32, 39, 61, 92, 132 | 9, 13, 32, 34, 39, 92, 9, 13, 34, 39, 92, 34, 133 | 39, 92, 32, 34, 39, 47, 62, 63, 92, 95, 9, 13, 134 | 45, 58, 65, 90, 97, 122, 32, 34, 39, 47, 62, 63, 135 | 92, 95, 9, 13, 45, 58, 65, 90, 97, 122, 32, 34, 136 | 39, 61, 63, 92, 95, 9, 13, 45, 46, 48, 58, 65, 137 | 90, 97, 122, 32, 34, 39, 61, 92, 9, 13, 32, 34, 138 | 39, 92, 9, 13, 32, 34, 39, 47, 62, 63, 92, 95, 139 | 9, 13, 45, 58, 65, 90, 97, 122, 34, 39, 62, 92, 140 | 32, 34, 39, 47, 62, 63, 92, 95, 9, 13, 45, 58, 141 | 65, 90, 97, 122, 32, 39, 47, 62, 63, 92, 95, 9, 142 | 13, 45, 58, 65, 90, 97, 122, 39, 62, 92, 39, 92, 143 | 63, 95, 45, 46, 48, 57, 65, 90, 97, 122, 63, 95, 144 | 45, 46, 48, 57, 65, 90, 97, 122, 58, 63, 95, 45, 145 | 46, 48, 57, 65, 90, 97, 122, 63, 95, 45, 46, 48, 146 | 57, 65, 90, 97, 122, 32, 58, 62, 63, 95, 9, 13, 147 | 45, 46, 48, 57, 65, 90, 97, 122, 32, 62, 9, 13, 148 | 63, 95, 45, 46, 48, 57, 65, 90, 97, 122, 60, 47, 149 | 63, 95, 45, 57, 65, 90, 97, 122, 34, 92, 34, 92, 150 | 34, 39, 92, 34, 39, 92, 39, 92, 39, 92, 0 151 | }; 152 | } 153 | 154 | private static final char _parser_trans_keys[] = init__parser_trans_keys_0(); 155 | 156 | 157 | private static byte[] init__parser_single_lengths_0() 158 | { 159 | return new byte [] { 160 | 0, 3, 2, 6, 5, 4, 2, 3, 2, 2, 5, 5, 161 | 1, 2, 7, 7, 6, 4, 4, 7, 3, 3, 3, 7, 162 | 2, 2, 7, 6, 4, 4, 3, 3, 8, 8, 7, 5, 163 | 4, 8, 4, 8, 7, 3, 2, 2, 2, 3, 2, 5, 164 | 2, 2, 0, 1, 3, 0, 0, 2, 2, 3, 3, 2, 165 | 2, 0 166 | }; 167 | } 168 | 169 | private static final byte _parser_single_lengths[] = init__parser_single_lengths_0(); 170 | 171 | 172 | private static byte[] init__parser_range_lengths_0() 173 | { 174 | return new byte [] { 175 | 0, 4, 4, 4, 4, 5, 1, 1, 0, 0, 4, 4, 176 | 0, 0, 4, 4, 5, 1, 1, 4, 0, 0, 0, 4, 177 | 0, 0, 4, 5, 1, 1, 0, 0, 4, 4, 5, 1, 178 | 1, 4, 0, 4, 4, 0, 0, 4, 4, 4, 4, 5, 179 | 1, 4, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 180 | 0, 0 181 | }; 182 | } 183 | 184 | private static final byte _parser_range_lengths[] = init__parser_range_lengths_0(); 185 | 186 | 187 | private static short[] init__parser_index_offsets_0() 188 | { 189 | return new short [] { 190 | 0, 0, 8, 15, 26, 36, 46, 50, 55, 58, 61, 71, 191 | 81, 83, 86, 98, 110, 122, 128, 134, 146, 150, 154, 158, 192 | 170, 173, 176, 188, 200, 206, 212, 216, 220, 233, 246, 259, 193 | 266, 272, 285, 290, 303, 315, 319, 322, 329, 336, 344, 351, 194 | 362, 366, 373, 374, 376, 383, 384, 385, 388, 391, 395, 399, 195 | 402, 405 196 | }; 197 | } 198 | 199 | private static final short _parser_index_offsets[] = init__parser_index_offsets_0(); 200 | 201 | 202 | private static byte[] init__parser_trans_targs_0() 203 | { 204 | return new byte [] { 205 | 2, 1, 1, 1, 1, 1, 1, 51, 3, 3, 3, 3, 206 | 3, 3, 51, 4, 12, 43, 54, 3, 3, 4, 3, 3, 207 | 3, 51, 4, 12, 54, 5, 5, 4, 5, 5, 5, 51, 208 | 6, 7, 5, 5, 6, 5, 5, 5, 5, 51, 6, 7, 209 | 6, 51, 7, 8, 42, 7, 51, 10, 13, 9, 10, 13, 210 | 9, 11, 12, 54, 5, 5, 11, 5, 5, 5, 51, 11, 211 | 12, 54, 5, 5, 11, 5, 5, 5, 51, 53, 51, 14, 212 | 13, 9, 15, 10, 20, 56, 16, 13, 16, 15, 16, 16, 213 | 16, 9, 15, 10, 20, 56, 16, 13, 16, 15, 16, 16, 214 | 16, 9, 17, 10, 18, 16, 13, 16, 17, 16, 16, 16, 215 | 16, 9, 17, 10, 18, 13, 17, 9, 18, 19, 21, 13, 216 | 18, 9, 15, 10, 20, 56, 16, 13, 16, 15, 16, 16, 217 | 16, 9, 10, 55, 13, 9, 23, 14, 31, 22, 23, 14, 218 | 31, 22, 26, 10, 41, 60, 27, 25, 27, 26, 27, 27, 219 | 27, 24, 10, 25, 24, 23, 25, 24, 26, 10, 41, 60, 220 | 27, 25, 27, 26, 27, 27, 27, 24, 28, 10, 29, 27, 221 | 25, 27, 28, 27, 27, 27, 27, 24, 28, 10, 29, 25, 222 | 28, 24, 29, 30, 40, 25, 29, 24, 23, 14, 31, 22, 223 | 32, 32, 31, 22, 33, 23, 14, 38, 58, 34, 31, 34, 224 | 33, 34, 34, 34, 22, 33, 23, 14, 38, 58, 34, 31, 225 | 34, 33, 34, 34, 34, 22, 35, 23, 14, 36, 34, 31, 226 | 34, 35, 34, 34, 34, 34, 22, 35, 23, 14, 36, 31, 227 | 35, 22, 36, 37, 39, 31, 36, 22, 33, 23, 14, 38, 228 | 58, 34, 31, 34, 33, 34, 34, 34, 22, 23, 14, 57, 229 | 31, 22, 33, 23, 14, 38, 58, 34, 31, 34, 33, 34, 230 | 34, 34, 22, 26, 10, 41, 60, 27, 25, 27, 26, 27, 231 | 27, 27, 24, 10, 59, 25, 24, 10, 25, 24, 3, 3, 232 | 3, 3, 3, 3, 51, 45, 45, 45, 45, 45, 45, 51, 233 | 46, 45, 45, 45, 45, 45, 45, 51, 47, 47, 47, 47, 234 | 47, 47, 51, 48, 49, 61, 47, 47, 48, 47, 47, 47, 235 | 47, 51, 48, 61, 48, 51, 47, 47, 47, 47, 47, 47, 236 | 51, 0, 52, 51, 44, 1, 1, 1, 1, 1, 51, 51, 237 | 51, 10, 13, 9, 10, 13, 9, 23, 14, 31, 22, 23, 238 | 14, 31, 22, 10, 25, 24, 10, 25, 24, 51, 51, 51, 239 | 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 240 | 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 241 | 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 242 | 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 243 | 51, 51, 51, 51, 51, 51, 51, 51, 51, 0 244 | }; 245 | } 246 | 247 | private static final byte _parser_trans_targs[] = init__parser_trans_targs_0(); 248 | 249 | 250 | private static byte[] init__parser_trans_actions_0() 251 | { 252 | return new byte [] { 253 | 31, 0, 0, 0, 0, 0, 0, 27, 3, 3, 3, 3, 254 | 3, 3, 27, 5, 5, 0, 5, 0, 0, 5, 0, 0, 255 | 0, 27, 0, 0, 0, 11, 11, 0, 11, 11, 11, 27, 256 | 13, 13, 0, 0, 13, 0, 0, 0, 0, 29, 0, 0, 257 | 0, 29, 0, 0, 0, 0, 29, 43, 15, 15, 17, 0, 258 | 0, 7, 34, 34, 64, 64, 7, 64, 64, 64, 29, 0, 259 | 9, 9, 37, 37, 0, 37, 37, 37, 29, 0, 29, 17, 260 | 0, 0, 7, 17, 34, 81, 64, 0, 64, 7, 64, 64, 261 | 64, 0, 0, 17, 9, 72, 37, 0, 37, 0, 37, 37, 262 | 37, 0, 13, 17, 13, 0, 0, 0, 13, 0, 0, 0, 263 | 0, 0, 0, 17, 0, 0, 0, 0, 0, 17, 0, 0, 264 | 0, 0, 40, 43, 68, 86, 76, 15, 76, 40, 76, 76, 265 | 76, 15, 17, 58, 0, 0, 46, 43, 15, 15, 17, 17, 266 | 0, 0, 7, 17, 34, 81, 64, 0, 64, 7, 64, 64, 267 | 64, 0, 17, 0, 0, 17, 0, 0, 0, 17, 9, 72, 268 | 37, 0, 37, 0, 37, 37, 37, 0, 13, 17, 13, 0, 269 | 0, 0, 13, 0, 0, 0, 0, 0, 0, 17, 0, 0, 270 | 0, 0, 0, 0, 17, 0, 0, 0, 43, 43, 15, 15, 271 | 17, 17, 0, 0, 7, 17, 17, 34, 81, 64, 0, 64, 272 | 7, 64, 64, 64, 0, 0, 17, 17, 9, 72, 37, 0, 273 | 37, 0, 37, 37, 37, 0, 13, 17, 17, 13, 0, 0, 274 | 0, 13, 0, 0, 0, 0, 0, 0, 17, 17, 0, 0, 275 | 0, 0, 0, 17, 17, 0, 0, 0, 40, 43, 43, 68, 276 | 86, 76, 15, 76, 40, 76, 76, 76, 15, 17, 17, 58, 277 | 0, 0, 40, 46, 43, 68, 86, 76, 15, 76, 40, 76, 278 | 76, 76, 15, 40, 43, 68, 86, 76, 15, 76, 40, 76, 279 | 76, 76, 15, 17, 58, 0, 0, 43, 15, 15, 0, 0, 280 | 0, 0, 0, 0, 27, 1, 1, 1, 1, 1, 1, 27, 281 | 31, 0, 0, 0, 0, 0, 0, 27, 3, 3, 3, 3, 282 | 3, 3, 27, 5, 0, 5, 0, 0, 5, 0, 0, 0, 283 | 0, 27, 0, 0, 0, 27, 0, 0, 0, 0, 0, 0, 284 | 27, 0, 61, 23, 0, 1, 1, 1, 1, 1, 25, 52, 285 | 49, 17, 0, 0, 17, 0, 0, 17, 17, 0, 0, 17, 286 | 17, 0, 0, 17, 0, 0, 17, 0, 0, 55, 27, 27, 287 | 27, 27, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 288 | 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 289 | 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 290 | 29, 29, 29, 29, 27, 27, 27, 27, 27, 27, 27, 25, 291 | 52, 49, 52, 49, 52, 49, 52, 49, 55, 0 292 | }; 293 | } 294 | 295 | private static final byte _parser_trans_actions[] = init__parser_trans_actions_0(); 296 | 297 | 298 | private static byte[] init__parser_to_state_actions_0() 299 | { 300 | return new byte [] { 301 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 302 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 303 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 304 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 305 | 0, 0, 19, 19, 0, 0, 0, 0, 0, 0, 0, 0, 306 | 0, 0 307 | }; 308 | } 309 | 310 | private static final byte _parser_to_state_actions[] = init__parser_to_state_actions_0(); 311 | 312 | 313 | private static byte[] init__parser_from_state_actions_0() 314 | { 315 | return new byte [] { 316 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 317 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 318 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 319 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 320 | 0, 0, 0, 21, 0, 0, 0, 0, 0, 0, 0, 0, 321 | 0, 0 322 | }; 323 | } 324 | 325 | private static final byte _parser_from_state_actions[] = init__parser_from_state_actions_0(); 326 | 327 | 328 | private static short[] init__parser_eof_trans_0() 329 | { 330 | return new short [] { 331 | 0, 455, 455, 455, 455, 448, 448, 448, 448, 448, 448, 448, 332 | 448, 448, 448, 448, 448, 448, 448, 448, 448, 448, 448, 448, 333 | 448, 448, 448, 448, 448, 448, 448, 448, 448, 448, 448, 448, 334 | 448, 448, 448, 448, 448, 448, 448, 455, 455, 455, 455, 455, 335 | 455, 455, 0, 0, 456, 463, 464, 463, 464, 463, 464, 463, 336 | 464, 465 337 | }; 338 | } 339 | 340 | private static final short _parser_eof_trans[] = init__parser_eof_trans_0(); 341 | 342 | 343 | static final int parser_start = 51; 344 | static final int parser_first_final = 51; 345 | static final int parser_error = 0; 346 | 347 | static final int parser_en_Closeout = 50; 348 | static final int parser_en_main = 51; 349 | 350 | 351 | // line 172 "JavaScanner.rl" 352 | 353 | public RubyArray operate(String tag_prefix, String input) { 354 | char[] data = input.toCharArray(); 355 | String disposable_string; 356 | 357 | String name = ""; 358 | String prefix = ""; 359 | RubySymbol flavor = RubySymbol.newSymbol(runtime, "tasteless".intern()); 360 | RubyHash attributes = RubyHash.newHash(runtime); 361 | 362 | int tagstart = 0; 363 | int mark_pfx = 0; 364 | int mark_stg = 0; 365 | int mark_attr = 0; 366 | int mark_nat = 0; 367 | int mark_vat = 0; 368 | 369 | String nat = ""; 370 | String vat = ""; 371 | 372 | int cs; 373 | int p = 0; 374 | int pe = data.length; 375 | int eof = pe; 376 | int act; 377 | int ts; 378 | int te; 379 | 380 | rv = RubyArray.newArray(runtime); 381 | char[] remainder = data; 382 | 383 | 384 | // line 385 "JavaScanner.java" 385 | { 386 | cs = parser_start; 387 | ts = -1; 388 | te = -1; 389 | act = 0; 390 | } 391 | 392 | // line 204 "JavaScanner.rl" 393 | 394 | // line 395 "JavaScanner.java" 395 | { 396 | int _klen; 397 | int _trans = 0; 398 | int _acts; 399 | int _nacts; 400 | int _keys; 401 | int _goto_targ = 0; 402 | 403 | _goto: while (true) { 404 | switch ( _goto_targ ) { 405 | case 0: 406 | if ( p == pe ) { 407 | _goto_targ = 4; 408 | continue _goto; 409 | } 410 | if ( cs == 0 ) { 411 | _goto_targ = 5; 412 | continue _goto; 413 | } 414 | case 1: 415 | _acts = _parser_from_state_actions[cs]; 416 | _nacts = (int) _parser_actions[_acts++]; 417 | while ( _nacts-- > 0 ) { 418 | switch ( _parser_actions[_acts++] ) { 419 | case 15: 420 | // line 1 "NONE" 421 | {ts = p;} 422 | break; 423 | // line 424 "JavaScanner.java" 424 | } 425 | } 426 | 427 | _match: do { 428 | _keys = _parser_key_offsets[cs]; 429 | _trans = _parser_index_offsets[cs]; 430 | _klen = _parser_single_lengths[cs]; 431 | if ( _klen > 0 ) { 432 | int _lower = _keys; 433 | int _mid; 434 | int _upper = _keys + _klen - 1; 435 | while (true) { 436 | if ( _upper < _lower ) 437 | break; 438 | 439 | _mid = _lower + ((_upper-_lower) >> 1); 440 | if ( data[p] < _parser_trans_keys[_mid] ) 441 | _upper = _mid - 1; 442 | else if ( data[p] > _parser_trans_keys[_mid] ) 443 | _lower = _mid + 1; 444 | else { 445 | _trans += (_mid - _keys); 446 | break _match; 447 | } 448 | } 449 | _keys += _klen; 450 | _trans += _klen; 451 | } 452 | 453 | _klen = _parser_range_lengths[cs]; 454 | if ( _klen > 0 ) { 455 | int _lower = _keys; 456 | int _mid; 457 | int _upper = _keys + (_klen<<1) - 2; 458 | while (true) { 459 | if ( _upper < _lower ) 460 | break; 461 | 462 | _mid = _lower + (((_upper-_lower) >> 1) & ~1); 463 | if ( data[p] < _parser_trans_keys[_mid] ) 464 | _upper = _mid - 2; 465 | else if ( data[p] > _parser_trans_keys[_mid+1] ) 466 | _lower = _mid + 2; 467 | else { 468 | _trans += ((_mid - _keys)>>1); 469 | break _match; 470 | } 471 | } 472 | _trans += _klen; 473 | } 474 | } while (false); 475 | 476 | case 3: 477 | cs = _parser_trans_targs[_trans]; 478 | 479 | if ( _parser_trans_actions[_trans] != 0 ) { 480 | _acts = _parser_trans_actions[_trans]; 481 | _nacts = (int) _parser_actions[_acts++]; 482 | while ( _nacts-- > 0 ) 483 | { 484 | switch ( _parser_actions[_acts++] ) 485 | { 486 | case 0: 487 | // line 4 "JavaScanner.rl" 488 | { mark_pfx = p; } 489 | break; 490 | case 1: 491 | // line 5 "JavaScanner.rl" 492 | { 493 | prefix = String.valueOf(input.substring(mark_pfx, p)); 494 | } 495 | break; 496 | case 2: 497 | // line 8 "JavaScanner.rl" 498 | { 499 | if ( !prefix.equals(tag_prefix) ) { 500 | // Pass through the entire tag markup as text 501 | pass_through(input.substring(tagstart, p + 1)); 502 | // Reset all state 503 | prefix = ""; 504 | name = ""; 505 | attributes = RubyHash.newHash(runtime); 506 | flavor = RubySymbol.newSymbol(runtime, "tasteless".intern()); 507 | tagstart = p + 1; 508 | {cs = 51; _goto_targ = 2; if (true) continue _goto;} 509 | } 510 | } 511 | break; 512 | case 3: 513 | // line 22 "JavaScanner.rl" 514 | { mark_stg = p; } 515 | break; 516 | case 4: 517 | // line 23 "JavaScanner.rl" 518 | { 519 | name = String.valueOf(input.substring(mark_stg, p)); 520 | if (name == null || name.trim().isEmpty()) { 521 | // Pass through the entire tag markup as text 522 | pass_through(input.substring(tagstart, p + 1)); 523 | // Reset all state 524 | prefix = ""; 525 | name = ""; 526 | attributes = RubyHash.newHash(runtime); 527 | flavor = RubySymbol.newSymbol(runtime, "tasteless".intern()); 528 | tagstart = p + 1; 529 | {cs = 51; _goto_targ = 2; if (true) continue _goto;} 530 | } 531 | } 532 | break; 533 | case 5: 534 | // line 37 "JavaScanner.rl" 535 | { mark_attr = p; } 536 | break; 537 | case 6: 538 | // line 38 "JavaScanner.rl" 539 | { 540 | attributes.op_aset( 541 | runtime.getCurrentContext(), 542 | RubyString.newString(runtime, nat), 543 | RubyString.newString(runtime, vat) 544 | ); 545 | } 546 | break; 547 | case 7: 548 | // line 46 "JavaScanner.rl" 549 | { mark_nat = p; } 550 | break; 551 | case 8: 552 | // line 47 "JavaScanner.rl" 553 | { nat = input.substring(mark_nat, p); } 554 | break; 555 | case 9: 556 | // line 48 "JavaScanner.rl" 557 | { mark_vat = p; } 558 | break; 559 | case 10: 560 | // line 49 "JavaScanner.rl" 561 | { vat = input.substring(mark_vat, p); } 562 | break; 563 | case 11: 564 | // line 51 "JavaScanner.rl" 565 | { flavor = RubySymbol.newSymbol(runtime, "open".intern()); } 566 | break; 567 | case 12: 568 | // line 52 "JavaScanner.rl" 569 | { flavor = RubySymbol.newSymbol(runtime, "self".intern()); } 570 | break; 571 | case 13: 572 | // line 53 "JavaScanner.rl" 573 | { flavor = RubySymbol.newSymbol(runtime, "close".intern()); } 574 | break; 575 | case 16: 576 | // line 1 "NONE" 577 | {te = p+1;} 578 | break; 579 | case 17: 580 | // line 89 "JavaScanner.rl" 581 | {act = 1;} 582 | break; 583 | case 18: 584 | // line 96 "JavaScanner.rl" 585 | {act = 2;} 586 | break; 587 | case 19: 588 | // line 96 "JavaScanner.rl" 589 | {te = p+1;{ 590 | pass_through(input.substring(p, p + 1)); 591 | tagstart = p + 1; 592 | }} 593 | break; 594 | case 20: 595 | // line 89 "JavaScanner.rl" 596 | {te = p;p--;{ 597 | tag(prefix, name, attributes, flavor); 598 | prefix = ""; 599 | name = ""; 600 | attributes = RubyHash.newHash(runtime); 601 | flavor = RubySymbol.newSymbol(runtime, "tasteless".intern()); 602 | }} 603 | break; 604 | case 21: 605 | // line 96 "JavaScanner.rl" 606 | {te = p;p--;{ 607 | pass_through(input.substring(p, p + 1)); 608 | tagstart = p + 1; 609 | }} 610 | break; 611 | case 22: 612 | // line 96 "JavaScanner.rl" 613 | {{p = ((te))-1;}{ 614 | pass_through(input.substring(p, p + 1)); 615 | tagstart = p + 1; 616 | }} 617 | break; 618 | case 23: 619 | // line 1 "NONE" 620 | { switch( act ) { 621 | case 1: 622 | {{p = ((te))-1;} 623 | tag(prefix, name, attributes, flavor); 624 | prefix = ""; 625 | name = ""; 626 | attributes = RubyHash.newHash(runtime); 627 | flavor = RubySymbol.newSymbol(runtime, "tasteless".intern()); 628 | } 629 | break; 630 | case 2: 631 | {{p = ((te))-1;} 632 | pass_through(input.substring(p, p + 1)); 633 | tagstart = p + 1; 634 | } 635 | break; 636 | } 637 | } 638 | break; 639 | // line 640 "JavaScanner.java" 640 | } 641 | } 642 | } 643 | 644 | case 2: 645 | _acts = _parser_to_state_actions[cs]; 646 | _nacts = (int) _parser_actions[_acts++]; 647 | while ( _nacts-- > 0 ) { 648 | switch ( _parser_actions[_acts++] ) { 649 | case 14: 650 | // line 1 "NONE" 651 | {ts = -1;} 652 | break; 653 | // line 654 "JavaScanner.java" 654 | } 655 | } 656 | 657 | if ( cs == 0 ) { 658 | _goto_targ = 5; 659 | continue _goto; 660 | } 661 | if ( ++p != pe ) { 662 | _goto_targ = 1; 663 | continue _goto; 664 | } 665 | case 4: 666 | if ( p == eof ) 667 | { 668 | if ( _parser_eof_trans[cs] > 0 ) { 669 | _trans = _parser_eof_trans[cs] - 1; 670 | _goto_targ = 3; 671 | continue _goto; 672 | } 673 | } 674 | 675 | case 5: 676 | } 677 | break; } 678 | } 679 | 680 | // line 205 "JavaScanner.rl" 681 | 682 | return rv; 683 | } 684 | } 685 | -------------------------------------------------------------------------------- /lib/radius/parser/JavaScanner.rl: -------------------------------------------------------------------------------- 1 | %%{ 2 | machine parser; 3 | 4 | action _prefix { mark_pfx = p; } 5 | action prefix { 6 | prefix = String.valueOf(input.substring(mark_pfx, p)); 7 | } 8 | action _check_prefix { 9 | if ( !prefix.equals(tag_prefix) ) { 10 | // Pass through the entire tag markup as text 11 | pass_through(input.substring(tagstart, p + 1)); 12 | // Reset all state 13 | prefix = ""; 14 | name = ""; 15 | attributes = RubyHash.newHash(runtime); 16 | flavor = RubySymbol.newSymbol(runtime, "tasteless".intern()); 17 | tagstart = p + 1; 18 | fgoto main; 19 | } 20 | } 21 | 22 | action _starttag { mark_stg = p; } 23 | action starttag { 24 | name = String.valueOf(input.substring(mark_stg, p)); 25 | if (name == null || name.trim().isEmpty()) { 26 | // Pass through the entire tag markup as text 27 | pass_through(input.substring(tagstart, p + 1)); 28 | // Reset all state 29 | prefix = ""; 30 | name = ""; 31 | attributes = RubyHash.newHash(runtime); 32 | flavor = RubySymbol.newSymbol(runtime, "tasteless".intern()); 33 | tagstart = p + 1; 34 | fgoto main; 35 | } 36 | } 37 | action _attr { mark_attr = p; } 38 | action attr { 39 | attributes.op_aset( 40 | runtime.getCurrentContext(), 41 | RubyString.newString(runtime, nat), 42 | RubyString.newString(runtime, vat) 43 | ); 44 | } 45 | 46 | action _nameattr { mark_nat = p; } 47 | action nameattr { nat = input.substring(mark_nat, p); } 48 | action _valattr { mark_vat = p; } 49 | action valattr { vat = input.substring(mark_vat, p); } 50 | 51 | action opentag { flavor = RubySymbol.newSymbol(runtime, "open".intern()); } 52 | action selftag { flavor = RubySymbol.newSymbol(runtime, "self".intern()); } 53 | action closetag { flavor = RubySymbol.newSymbol(runtime, "close".intern()); } 54 | 55 | Closeout := empty; 56 | 57 | # words 58 | PrefixChar = [\-A-Za-z0-9._?] ; 59 | NameChar = [\-A-Za-z0-9._:?] ; 60 | TagName = ([\-A-Za-z0-9._?]+ (':' [\-A-Za-z0-9._?]+)*) >_starttag %starttag; 61 | Prefix = PrefixChar+ >_prefix %prefix; 62 | Open = "<"; 63 | Sep = ":" >_check_prefix; 64 | End = "/"; 65 | Close = ">"; 66 | 67 | Name = Prefix Sep TagName; 68 | 69 | NameAttr = NameChar+ >_nameattr %nameattr; 70 | Q1Char = ( "\\\'" | [^'] ) ; 71 | Q1Attr = Q1Char* >_valattr %valattr; 72 | Q2Char = ( "\\\"" | [^"] ) ; 73 | Q2Attr = Q2Char* >_valattr %valattr; 74 | 75 | Attr = NameAttr space* "=" space* ('"' Q2Attr '"' | "'" Q1Attr "'") space* >_attr %attr; 76 | Attrs = (space+ Attr* | empty); 77 | 78 | CloseTrailer = End Close %selftag; 79 | OpenTrailer = Close %opentag; 80 | 81 | Trailer = (OpenTrailer | CloseTrailer); 82 | 83 | OpenOrSelfTag = Name Attrs? Trailer; 84 | CloseTag = End Name space* Close %closetag; 85 | 86 | SomeTag = Open (OpenOrSelfTag | CloseTag); 87 | 88 | main := |* 89 | SomeTag => { 90 | tag(prefix, name, attributes, flavor); 91 | prefix = ""; 92 | name = ""; 93 | attributes = RubyHash.newHash(runtime); 94 | flavor = RubySymbol.newSymbol(runtime, "tasteless".intern()); 95 | }; 96 | any => { 97 | pass_through(input.substring(p, p + 1)); 98 | tagstart = p + 1; 99 | }; 100 | *|; 101 | }%% 102 | 103 | package radius.parser; 104 | 105 | import java.util.HashMap; 106 | import java.util.LinkedList; 107 | import org.jruby.Ruby; // runtime 108 | import org.jruby.RubyObject; 109 | import org.jruby.runtime.builtin.IRubyObject; 110 | import org.jruby.RubyArray; 111 | import org.jruby.RubyString; 112 | import org.jruby.RubyHash; 113 | import org.jruby.RubySymbol; 114 | 115 | public class JavaScanner { 116 | 117 | Ruby runtime = null; 118 | RubyArray rv = null; 119 | 120 | void pass_through(String str) { 121 | RubyObject last = ((RubyObject)rv.last()); 122 | if ( rv.size() > 0 && last != null && (last instanceof RubyString) ){ 123 | // XXX concat changes for ruby 1.9 124 | ((RubyString) last).concat(RubyString.newString(runtime, str)); 125 | } else { 126 | rv.append(RubyString.newString(runtime, str)); 127 | } 128 | } 129 | 130 | void tag(String prefix, String name, RubyHash attr, RubySymbol flavor) { 131 | // Validate both prefix and name 132 | if ((prefix == null || prefix.trim().isEmpty()) && 133 | (name == null || name.trim().isEmpty())) { 134 | pass_through("<"); 135 | return; 136 | } 137 | 138 | if (name == null || name.trim().isEmpty()) { 139 | pass_through("<" + prefix + ":"); 140 | return; 141 | } 142 | 143 | RubyHash tag = RubyHash.newHash(runtime); 144 | tag.op_aset( 145 | runtime.getCurrentContext(), 146 | RubySymbol.newSymbol(runtime, "prefix"), 147 | RubyString.newString(runtime, prefix != null ? prefix : "") 148 | ); 149 | tag.op_aset( 150 | runtime.getCurrentContext(), 151 | RubySymbol.newSymbol(runtime, "name"), 152 | RubyString.newString(runtime, name) 153 | ); 154 | tag.op_aset( 155 | runtime.getCurrentContext(), 156 | RubySymbol.newSymbol(runtime, "attrs"), 157 | attr 158 | ); 159 | tag.op_aset( 160 | runtime.getCurrentContext(), 161 | RubySymbol.newSymbol(runtime, "flavor"), 162 | flavor 163 | ); 164 | rv.append(tag); 165 | } 166 | 167 | public JavaScanner(Ruby runtime) { 168 | this.runtime = runtime; 169 | } 170 | 171 | %% write data; 172 | 173 | public RubyArray operate(String tag_prefix, String input) { 174 | char[] data = input.toCharArray(); 175 | String disposable_string; 176 | 177 | String name = ""; 178 | String prefix = ""; 179 | RubySymbol flavor = RubySymbol.newSymbol(runtime, "tasteless".intern()); 180 | RubyHash attributes = RubyHash.newHash(runtime); 181 | 182 | int tagstart = 0; 183 | int mark_pfx = 0; 184 | int mark_stg = 0; 185 | int mark_attr = 0; 186 | int mark_nat = 0; 187 | int mark_vat = 0; 188 | 189 | String nat = ""; 190 | String vat = ""; 191 | 192 | int cs; 193 | int p = 0; 194 | int pe = data.length; 195 | int eof = pe; 196 | int act; 197 | int ts; 198 | int te; 199 | 200 | rv = RubyArray.newArray(runtime); 201 | char[] remainder = data; 202 | 203 | %% write init; 204 | %% write exec; 205 | 206 | return rv; 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /lib/radius/parser/java_scanner.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlong/radius/7f7347b156d01ffd499dddd412b94a8eb2882c62/lib/radius/parser/java_scanner.jar -------------------------------------------------------------------------------- /lib/radius/parser/scanner.rb: -------------------------------------------------------------------------------- 1 | module Radius 2 | class Scanner 3 | 4 | # The regular expression used to find (1) opening and self-enclosed tag names, (2) self-enclosing trailing slash, 5 | # (3) attributes and (4) closing tag 6 | def scanner_regex(prefix = nil) 7 | %r{<#{prefix}:([-\w:]+?)(\s+(?:[-\w]+\s*=\s*(?:"[^"]*?"|'[^']*?')\s*)*|)(\/?)>|<\/#{prefix}:([-\w:]+?)\s*>} 8 | end 9 | 10 | # Parses a given string and returns an array of nodes. 11 | # The nodes consist of strings and hashes that describe a Radius tag that was found. 12 | def operate(prefix, data) 13 | data = Radius::OrdString(data) 14 | @nodes = [''] 15 | 16 | re = scanner_regex(prefix) 17 | if md = re.match(data) 18 | remainder = '' 19 | while md 20 | start_tag, attributes, self_enclosed, end_tag = $1, $2, $3, $4 21 | 22 | flavor = self_enclosed == '/' ? :self : (start_tag ? :open : :close) 23 | 24 | # save the part before the current match as a string node 25 | @nodes << md.pre_match 26 | 27 | # save the tag that was found as a tag hash node 28 | @nodes << {:prefix=>prefix, :name=>(start_tag || end_tag), :flavor => flavor, :attrs => parse_attributes(attributes)} 29 | 30 | # remember the part after the current match 31 | remainder = md.post_match 32 | 33 | # see if we find another tag in the remaining string 34 | md = re.match(md.post_match) 35 | end 36 | 37 | # add the last remaining string after the last tag that was found as a string node 38 | @nodes << remainder 39 | else 40 | @nodes << data 41 | end 42 | 43 | return @nodes 44 | end 45 | 46 | private 47 | 48 | def parse_attributes(text) # :nodoc: 49 | attr = {} 50 | re = /([-\w]+?)\s*=\s*('|")(.*?)\2/ 51 | while md = re.match(text) 52 | attr[$1] = $3 53 | text = md.post_match 54 | end 55 | attr 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/radius/parser/squiggle_scanner.rb: -------------------------------------------------------------------------------- 1 | module Radius 2 | class SquiggleScanner < Scanner 3 | 4 | # The regular expression used to find (1) opening and self-enclosed tag names, (2) self-enclosing trailing slash, 5 | # (3) attributes and (4) closing tag 6 | def scanner_regex(prefix = nil) 7 | %r{\{\s*([\w:]+?)(\s+(?:\w+\s*=\s*(?:"[^"]*?"|'[^']*?')\s*)*|)(\/?)\}|\{\/([\w:]+?)\s*\}} 8 | end 9 | 10 | end 11 | end -------------------------------------------------------------------------------- /lib/radius/tag_binding.rb: -------------------------------------------------------------------------------- 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 | 66 | # Shortcut for accessing tag.attr[key] 67 | def [](key) 68 | attr[key] 69 | end 70 | end 71 | end -------------------------------------------------------------------------------- /lib/radius/tag_definitions.rb: -------------------------------------------------------------------------------- 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 | # Adds the tag definition to the context. Override in subclasses to add additional tags 18 | # (child tags) when the tag is created. 19 | def construct_tag_set(name, options, &block) 20 | if block 21 | @context.definitions[name.to_s] = block 22 | else 23 | lp = last_part(name) 24 | @context.define_tag(name) do |tag| 25 | if tag.single? 26 | options[:for] 27 | else 28 | tag.locals.send("#{ lp }=", options[:for]) unless options[:for].nil? 29 | tag.expand 30 | end 31 | end 32 | end 33 | end 34 | 35 | # Normalizes options pased to tag definition. Override in decendants to preform 36 | # additional normalization. 37 | def prepare_options(name, options) 38 | options = Utility.symbolize_keys(options) 39 | options[:expose] = expand_array_option(options[:expose]) 40 | object = options[:for] 41 | options[:attributes] = object.respond_to?(:attributes) unless options.has_key? :attributes 42 | options[:expose] += object.attributes.keys if options[:attributes] 43 | options 44 | end 45 | 46 | # Validates parameters passed to tag definition. Override in decendants to add custom 47 | # validations. 48 | def validate_params(name, options, &block) 49 | unless options.has_key? :for 50 | raise ArgumentError.new("tag definition must contain a :for option or a block") unless block 51 | raise ArgumentError.new("tag definition must contain a :for option when used with the :expose option") unless options[:expose].empty? 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 79 | -------------------------------------------------------------------------------- /lib/radius/utility.rb: -------------------------------------------------------------------------------- 1 | module Radius 2 | module Utility # :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 | underscored_string.split('_').map(&:capitalize).join 26 | end 27 | 28 | def self.array_to_s(c) 29 | +c.map do |x| 30 | x.is_a?(Array) ? array_to_s(x) : x.to_s 31 | end.join 32 | end 33 | end 34 | end -------------------------------------------------------------------------------- /lib/radius/version.rb: -------------------------------------------------------------------------------- 1 | module Radius #:nodoc: 2 | VERSION = "0.8.1" 3 | def self.version 4 | VERSION 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /radius.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | require File.join(File.dirname(__FILE__), 'lib', 'radius', 'version') 4 | Gem::Specification.new do |s| 5 | s.name = "radius" 6 | s.version = ::Radius.version 7 | s.licenses = ["MIT"] 8 | 9 | s.required_ruby_version = ">= 2.6.0" 10 | 11 | s.authors = ["John W. Long", "David Chelimsky", "Bryce Kerley"] 12 | s.description = %q{Radius is a powerful tag-based template language for Ruby inspired by the template languages used in MovableType and TextPattern. It uses tags similar to XML, but can be used to generate any form of plain text (HTML, e-mail, etc...).} 13 | s.email = ["me@johnwlong.com", "dchelimsky@gmail.com", "bkerley@brycekerley.net"] 14 | s.extra_rdoc_files = [ 15 | "CHANGELOG", 16 | "CONTRIBUTORS", 17 | "LICENSE", 18 | "QUICKSTART.rdoc", 19 | "README.rdoc" 20 | ] 21 | 22 | ignores = File.read('.gitignore').split("\n").inject([]) {|a,p| a + Dir[p] } 23 | s.files = Dir['**/*','.gitignore'] - ignores 24 | 25 | s.homepage = %q{http://github.com/jlong/radius} 26 | s.summary = %q{A tag-based templating language for Ruby.} 27 | 28 | s.metadata = { 29 | "homepage_uri" => "https://github.com/jlong/radius", 30 | "changelog_uri" => "https://github.com/jlong/radius/blob/master/CHANGELOG", 31 | "bug_tracker_uri" => "https://github.com/jlong/radius/issues" 32 | } 33 | end 34 | 35 | -------------------------------------------------------------------------------- /tasks/rdoc.rake: -------------------------------------------------------------------------------- 1 | require 'rdoc/task' 2 | Rake::RDocTask.new do |rdoc| 3 | version = File.exist?('VERSION') ? File.read('VERSION') : "" 4 | 5 | rdoc.rdoc_dir = 'rdoc' 6 | rdoc.title = "Radius #{version}" 7 | rdoc.main = "README.rdoc" 8 | rdoc.rdoc_files.include('*.rdoc') 9 | rdoc.rdoc_files.include('LICENSE') 10 | rdoc.rdoc_files.include('CHANGELOG') 11 | rdoc.rdoc_files.include('QUICKSTART.rdoc') 12 | rdoc.rdoc_files.include('lib/**/*.rb') 13 | end -------------------------------------------------------------------------------- /tasks/rubinius.rake: -------------------------------------------------------------------------------- 1 | desc "remove Rubinius rbc files" 2 | task "rubinius:clean" do 3 | (Dir['**/*.rbc']).each { |f| rm f } 4 | end -------------------------------------------------------------------------------- /tasks/test.rake: -------------------------------------------------------------------------------- 1 | require 'rake/testtask' 2 | 3 | Rake::TestTask.new do |t| 4 | t.libs << "lib" << "test" 5 | t.test_files = FileList['test/*_test.rb'] 6 | t.verbose = true 7 | end -------------------------------------------------------------------------------- /test/benchmarks.rb: -------------------------------------------------------------------------------- 1 | $: << File.join(File.dirname(__FILE__), '..', 'lib') 2 | require 'radius' 3 | 4 | if RUBY_PLATFORM == 'java' 5 | require 'java' 6 | require 'radius/parser/jscanner' 7 | end 8 | 9 | require 'benchmark' 10 | 11 | document = < 14 | Middle Top 15 | 16 | Middle Bottom 17 | 18 | After it all 19 | EOF 20 | 21 | amount = 1000 22 | 23 | Benchmark.bmbm do |bm| 24 | bm.report('vanilla') do 25 | scanner = RUBY_VERSION =~ /1\.9/ ? Radius::Scanner.new(:scanner => Radius::Scanner) : Radius::Scanner.new 26 | amount.times { scanner.operate('r', document) } 27 | end 28 | 29 | bm.report('vanilla (huge)') do 30 | scanner = RUBY_VERSION =~ /1\.9/ ? Radius::Scanner.new(:scanner => Radius::Scanner) : Radius::Scanner.new 31 | scanner.operate('r', 'a' * 460000) 32 | end 33 | 34 | if RUBY_PLATFORM == 'java' 35 | bm.report('JavaScanner') do 36 | scanner = Radius::JavaScanner.new(JRuby.runtime) 37 | amount.times { scanner.operate('r', document) } 38 | end 39 | 40 | bm.report('JavaScanner (huge)') do 41 | scanner = Radius::JavaScanner.new(JRuby.runtime) 42 | scanner.operate('r', 'a' * 460000) 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /test/context_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class RadiusContextTest < Minitest::Test 4 | include RadiusTestHelper 5 | 6 | class SuperContext < Radius::Context 7 | attr_accessor :name 8 | def initialize(name) 9 | self.name = name 10 | super 11 | end 12 | end 13 | 14 | def setup 15 | @context = new_context 16 | end 17 | 18 | def test_initialize 19 | @context = Radius::Context.new 20 | end 21 | 22 | def test_initialize_with_block 23 | @context = Radius::Context.new do |c| 24 | assert_kind_of Radius::Context, c 25 | c.define_tag('test') { 'just a test' } 26 | end 27 | refute_equal Hash.new, @context.definitions 28 | end 29 | 30 | def test_initialize_with_arguments 31 | @context = SuperContext.new('arg') do |c| 32 | assert_kind_of Radius::Context, c 33 | c.define_tag('test') { 'just a test' } 34 | end 35 | assert_equal 'arg', @context.name 36 | end 37 | 38 | def test_dup_preserves_delegated_values 39 | @context = Radius::Context.new 40 | @context.globals.object = Object.new.tap { |o| def o.special_method; "special"; end } 41 | duped = @context.dup 42 | 43 | assert_equal "special", duped.globals.special_method, "Duped context should preserve delegated object methods" 44 | end 45 | 46 | def test_with 47 | got = @context.with do |c| 48 | assert_equal @context, c 49 | end 50 | assert_equal @context, got 51 | end 52 | 53 | def test_render_tag 54 | define_global_tag "hello" do |tag| 55 | "Hello #{tag.attr['name'] || 'World'}!" 56 | end 57 | assert_render_tag_output 'Hello World!', 'hello' 58 | assert_render_tag_output 'Hello John!', 'hello', 'name' => 'John' 59 | end 60 | 61 | def test_render_tag__undefined_tag 62 | e = assert_raises(Radius::UndefinedTagError) { @context.render_tag('undefined_tag') } 63 | assert_equal "undefined tag `undefined_tag'", e.message 64 | end 65 | 66 | def test_tag_missing 67 | class << @context 68 | def tag_missing(tag, attr, &block) 69 | "undefined tag `#{tag}' with attributes #{attr.inspect.sub(" => ", "=>")}" 70 | end 71 | end 72 | 73 | text = '' 74 | expected = %{undefined tag `undefined_tag' with attributes {"cool"=>"beans"}} 75 | text = @context.render_tag('undefined_tag', 'cool' => 'beans') 76 | assert_equal expected, text 77 | end 78 | 79 | private 80 | 81 | def assert_render_tag_output(output, *render_tag_params) 82 | assert_equal output, @context.render_tag(*render_tag_params) 83 | end 84 | 85 | end 86 | -------------------------------------------------------------------------------- /test/multithreaded_test.rb: -------------------------------------------------------------------------------- 1 | require 'thread' 2 | require 'test_helper' 3 | require 'radius' 4 | require 'etc' 5 | 6 | class MultithreadTest < Minitest::Test 7 | 8 | def setup 9 | Thread.abort_on_exception = true 10 | @context = Radius::Context.new do |c| 11 | c.define_tag('thread') do |tag| 12 | "#{tag.locals.thread_id} / #{tag.globals.object_id}" 13 | end 14 | end 15 | end 16 | 17 | def test_runs_multithreaded 18 | thread_count = [Etc.nprocessors, 16].min 19 | iterations_per_thread = 500 20 | failures = Queue.new 21 | results = Array.new(thread_count) { [] } 22 | threads = [] 23 | 24 | thread_count.times do |i| 25 | threads << Thread.new do 26 | local_results = [] 27 | iterations_per_thread.times do 28 | begin 29 | thread_context = @context.dup 30 | parser = Radius::Parser.new(thread_context, :tag_prefix => 'r') 31 | parser.context.globals.thread_id = Thread.current.object_id 32 | expected = "#{Thread.current.object_id} / #{parser.context.globals.object_id}" 33 | result = parser.parse('') 34 | 35 | local_results << { 36 | result: result, 37 | thread_id: Thread.current.object_id, 38 | iteration: local_results.size 39 | } 40 | 41 | failures << "Expected: #{expected}, Got: #{result}" unless result == expected 42 | rescue => e 43 | failures << "Thread #{Thread.current.object_id} failed: #{e.message}\n#{e.backtrace.join("\n")}" 44 | end 45 | end 46 | results[i] = local_results 47 | end 48 | end 49 | 50 | threads.each(&:join) 51 | 52 | # Only try to show failures if there are any 53 | failure_message = if failures.empty? 54 | nil 55 | else 56 | failed_items = [] 57 | 5.times { failed_items << failures.pop unless failures.empty? } 58 | "Thread failures detected:\n#{failures.size} times:\n#{failed_items.join("\n")}" 59 | end 60 | 61 | assert(failures.empty?, failure_message) 62 | total_results = results.flatten.uniq.size 63 | expected_unique_results = thread_count * iterations_per_thread 64 | 65 | if total_results != expected_unique_results 66 | duplicates = results.flatten.group_by { |r| r[:result] } 67 | .select { |_, v| v.size > 1 } 68 | 69 | puts "\nDuplicates found:" 70 | duplicates.each do |result, occurrences| 71 | puts "\nResult: #{result[:result]}" 72 | occurrences.each { |o| puts " Thread: #{o[:thread_id]}, Iteration: #{o[:iteration]}" } 73 | end 74 | end 75 | 76 | assert_equal expected_unique_results, total_results, 77 | "Expected #{expected_unique_results} unique results (#{thread_count} threads × #{iterations_per_thread} iterations)" 78 | end 79 | 80 | end 81 | -------------------------------------------------------------------------------- /test/ord_string_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'radius' 3 | 4 | class RadiusOrdStringTest < Minitest::Test 5 | 6 | def test_string_slice_integer 7 | str = Radius::OrdString.new "abc" 8 | assert_equal str[0], 97 9 | assert_equal str[1], 98 10 | assert_equal str[2], 99 11 | end 12 | 13 | def test_string_slice_range 14 | str = Radius::OrdString.new "abc" 15 | assert_equal str[0..-1], "abc" 16 | end 17 | 18 | end 19 | -------------------------------------------------------------------------------- /test/parser_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/test_helper') 2 | 3 | class RadiusParserTest < Minitest::Test 4 | include RadiusTestHelper 5 | 6 | def setup 7 | @context = new_context 8 | @parser = Radius::Parser.new(@context, :tag_prefix => 'r') 9 | end 10 | 11 | def test_initialize 12 | @parser = Radius::Parser.new 13 | assert_kind_of Radius::Context, @parser.context 14 | end 15 | 16 | def test_initialize_with_params 17 | @parser = Radius::Parser.new(TestContext.new) 18 | assert_kind_of TestContext, @parser.context 19 | 20 | @parser = Radius::Parser.new(:context => TestContext.new) 21 | assert_kind_of TestContext, @parser.context 22 | 23 | @parser = Radius::Parser.new('context' => TestContext.new) 24 | assert_kind_of TestContext, @parser.context 25 | 26 | @parser = Radius::Parser.new(:tag_prefix => 'r') 27 | assert_kind_of Radius::Context, @parser.context 28 | assert_equal 'r', @parser.tag_prefix 29 | 30 | @parser = Radius::Parser.new(TestContext.new, :tag_prefix => 'r') 31 | assert_kind_of TestContext, @parser.context 32 | assert_equal 'r', @parser.tag_prefix 33 | end 34 | 35 | def test_parse_tag_with_dashes 36 | define_tag 'some-tag' do |tag| 37 | 'ok' 38 | end 39 | 40 | assert_parse_output 'ok', 'nope' 41 | end 42 | 43 | def test_parse_tag_with_dashed_attributes 44 | define_tag 'tag-with-dashed-attributes' do |tag| 45 | "dashed: #{tag.attr['data-dashed']} regular: #{tag.attr['regular']}" 46 | end 47 | assert_parse_output 'dashed: dashed-value regular: value', '' 48 | end 49 | 50 | 51 | def test_parse_individual_tags_and_parameters 52 | define_tag "add" do |tag| 53 | tag.attr["param1"].to_i + tag.attr["param2"].to_i 54 | end 55 | assert_parse_output "<3>", %{<>} 56 | end 57 | 58 | def test_parse_attributes 59 | attributes = %{{"a"=>"1", "b"=>"2", "c"=>"3", "d"=>"'"}} 60 | assert_parse_output attributes, %{} 61 | assert_parse_output attributes, %{} 62 | end 63 | 64 | def test_parse_attributes_with_slashes_or_angle_brackets 65 | slash = %{{"slash"=>"/"}} 66 | angle = %{{"angle"=>">"}} 67 | assert_parse_output slash, %{} 68 | assert_parse_output slash, %{} 69 | assert_parse_output angle, %{} 70 | end 71 | 72 | def test_parse_quotes 73 | assert_parse_output "test []", %{ } 74 | end 75 | 76 | def test_things_that_should_be_left_alone 77 | [ 78 | %{ test="2"="4" }, 79 | %{="2" } 80 | ].each do |middle| 81 | assert_parsed_is_unchanged "" 82 | assert_parsed_is_unchanged "" 83 | end 84 | end 85 | 86 | def test_tags_inside_html_tags 87 | assert_parse_output %{
tags in yo tags
}, 88 | %{
tags in yo tags
} 89 | end 90 | 91 | def test_parse_result_is_always_a_string 92 | define_tag("twelve") { 12 } 93 | assert_parse_output "12", "" 94 | end 95 | 96 | def test_parse_double_tags 97 | assert_parse_output "test".reverse, "test" 98 | assert_parse_output "tset TEST", "test test" 99 | end 100 | 101 | def test_parse_tag_nesting 102 | define_tag("parent", :for => '') 103 | define_tag("parent:child", :for => '') 104 | define_tag("extra", :for => '') 105 | define_tag("nesting") { |tag| tag.nesting } 106 | define_tag("extra:nesting") { |tag| tag.nesting.gsub(':', ' > ') } 107 | define_tag("parent:child:nesting") { |tag| tag.nesting.gsub(':', ' * ') } 108 | assert_parse_output "nesting", "" 109 | assert_parse_output "parent:nesting", "" 110 | assert_parse_output "extra > nesting", "" 111 | assert_parse_output "parent * child * nesting", "" 112 | assert_parse_output "parent > extra > nesting", "" 113 | assert_parse_output "parent > child > extra > nesting", "" 114 | assert_parse_output "parent * extra * child * nesting", "" 115 | assert_parse_output "parent > extra > child > extra > nesting", "" 116 | assert_parse_output "parent > extra > child > extra > nesting", "" 117 | assert_parse_output "extra * parent * child * nesting", "" 118 | assert_parse_output "extra > parent > nesting", "" 119 | assert_parse_output "extra * parent * child * nesting", "" 120 | assert_raises(Radius::UndefinedTagError) { @parser.parse("") } 121 | end 122 | def test_parse_tag_nesting_2 123 | define_tag("parent", :for => '') 124 | define_tag("parent:child", :for => '') 125 | define_tag("content") { |tag| tag.nesting } 126 | assert_parse_output 'parent:child:content', '' 127 | end 128 | 129 | def test_parse_tag__binding_do_missing 130 | define_tag 'test' do |tag| 131 | tag.missing! 132 | end 133 | e = assert_raises(Radius::UndefinedTagError) { @parser.parse("") } 134 | assert_equal "undefined tag `test'", e.message 135 | end 136 | 137 | def test_parse_chirpy_bird 138 | # :> chirp chirp 139 | assert_parse_output "<:", "<:" 140 | end 141 | 142 | def test_parse_tag__binding_render_tag 143 | define_tag('test') { |tag| "Hello #{tag.attr['name']}!" } 144 | define_tag('hello') { |tag| tag.render('test', tag.attr) } 145 | assert_parse_output 'Hello John!', '' 146 | end 147 | 148 | def test_accessing_tag_attributes_through_tag_indexer 149 | define_tag('test') { |tag| "Hello #{tag['name']}!" } 150 | assert_parse_output 'Hello John!', '' 151 | end 152 | 153 | def test_parse_tag__binding_render_tag_with_block 154 | define_tag('test') { |tag| "Hello #{tag.expand}!" } 155 | define_tag('hello') { |tag| tag.render('test') { tag.expand } } 156 | assert_parse_output 'Hello John!', 'John' 157 | end 158 | 159 | def test_tag_locals 160 | define_tag "outer" do |tag| 161 | tag.locals.var = 'outer' 162 | tag.expand 163 | end 164 | define_tag "outer:inner" do |tag| 165 | tag.locals.var = 'inner' 166 | tag.expand 167 | end 168 | define_tag "outer:var" do |tag| 169 | tag.locals.var 170 | end 171 | assert_parse_output 'outer', "" 172 | assert_parse_output 'outer:inner:outer', "::" 173 | assert_parse_output 'outer:inner:outer:inner:outer', "::::" 174 | assert_parse_output 'outer', "" 175 | end 176 | 177 | def test_tag_globals 178 | define_tag "set" do |tag| 179 | tag.globals.var = tag.attr['value'] 180 | '' 181 | end 182 | define_tag "var" do |tag| 183 | tag.globals.var 184 | end 185 | assert_parse_output " true false", %{ } 186 | end 187 | 188 | def test_parse_loops 189 | @item = nil 190 | define_tag "each" do |tag| 191 | result = [] 192 | ["Larry", "Moe", "Curly"].each do |item| 193 | tag.locals.item = item 194 | result << tag.expand 195 | end 196 | result.join(tag.attr["between"] || "") 197 | end 198 | define_tag "each:item" do |tag| 199 | tag.locals.item 200 | end 201 | assert_parse_output %{Three Stooges: "Larry", "Moe", "Curly"}, %{Three Stooges: ""} 202 | end 203 | 204 | def test_parse_speed 205 | define_tag "set" do |tag| 206 | tag.globals.var = tag.attr['value'] 207 | '' 208 | end 209 | define_tag "var" do |tag| 210 | tag.globals.var 211 | end 212 | parts = %w{decima nobis augue at facer processus commodo legentis odio lectorum dolore nulla esse lius qui nonummy ullamcorper erat ii notare} 213 | multiplier = parts.map{|p| "#{p}=\"#{rand}\""}.join(' ') 214 | assert ->{ 215 | Timeout.timeout(10) do 216 | assert_parse_output " false", %{ } 217 | end 218 | }.call 219 | end 220 | 221 | def test_tag_option_for 222 | define_tag 'fun', :for => 'just for kicks' 223 | assert_parse_output 'just for kicks', '' 224 | end 225 | 226 | def test_tag_expose_option 227 | define_tag 'user', :for => users.first, :expose => ['name', :age] 228 | assert_parse_output 'John', '' 229 | assert_parse_output '25', '' 230 | e = assert_raises(Radius::UndefinedTagError) { @parser.parse "" } 231 | assert_equal "undefined tag `email'", e.message 232 | end 233 | 234 | def test_tag_expose_attributes_option_on_by_default 235 | define_tag 'user', :for => user_with_attributes 236 | assert_parse_output 'John', '' 237 | end 238 | def test_tag_expose_attributes_set_to_false 239 | define_tag 'user_without_attributes', :for => user_with_attributes, :attributes => false 240 | assert_raises(Radius::UndefinedTagError) { @parser.parse "" } 241 | end 242 | 243 | def test_tag_options_must_contain_a_for_option_if_methods_are_exposed 244 | e = assert_raises(ArgumentError) { define_tag('fun', :expose => :today) { 'test' } } 245 | assert_equal "tag definition must contain a :for option when used with the :expose option", e.message 246 | end 247 | 248 | def test_parse_fail_on_missing_end_tag 249 | assert_raises(Radius::MissingEndTagError) { @parser.parse("") } 250 | end 251 | 252 | def test_parse_fail_on_wrong_end_tag 253 | assert_raises(Radius::WrongEndTagError) { @parser.parse("") } 254 | end 255 | 256 | def test_parse_with_default_tag_prefix 257 | @parser = Radius::Parser.new(@context) 258 | define_tag("hello") { |tag| "Hello world!" } 259 | assert_equal "

Hello world!

", @parser.parse('

') 260 | end 261 | 262 | def test_parse_with_other_radius_like_tags 263 | @parser = Radius::Parser.new(@context, :tag_prefix => "ralph") 264 | define_tag('hello') { "hello" } 265 | assert_equal "", @parser.parse("") 266 | end 267 | 268 | def test_copyin_global_values 269 | @context.globals.foo = 'bar' 270 | assert_equal 'bar', Radius::Parser.new(@context).context.globals.foo 271 | end 272 | 273 | def test_does_not_pollute_copied_globals 274 | @context.globals.foo = 'bar' 275 | parser = Radius::Parser.new(@context) 276 | parser.context.globals.foo = '[baz]' 277 | assert_equal 'bar', @context.globals.foo 278 | end 279 | 280 | def test_parse_with_other_namespaces 281 | @parser = Radius::Parser.new(@context, :tag_prefix => 'r') 282 | assert_equal "hello world", @parser.parse("hello world") 283 | end 284 | 285 | protected 286 | 287 | def assert_parse_output(output, input, message = nil) 288 | r = @parser.parse(input) 289 | assert_equal(output, r, message) 290 | end 291 | 292 | def assert_parsed_is_unchanged(something) 293 | assert_parse_output something, something 294 | end 295 | 296 | class User 297 | attr_accessor :name, :age, :email, :friend 298 | def initialize(name, age, email) 299 | @name, @age, @email = name, age, email 300 | end 301 | def <=>(other) 302 | name <=> other.name 303 | end 304 | end 305 | 306 | class UserWithAttributes < User 307 | def attributes 308 | { :name => name, :age => age, :email => email } 309 | end 310 | end 311 | 312 | def users 313 | [ 314 | User.new('John', 25, 'test@johnwlong.com'), 315 | User.new('James', 27, 'test@jameslong.com') 316 | ] 317 | end 318 | 319 | def user_with_attributes 320 | UserWithAttributes.new('John', 25, 'test@johnwlong.com') 321 | end 322 | 323 | end 324 | -------------------------------------------------------------------------------- /test/quickstart_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/test_helper') 2 | 3 | class QuickstartTest < Minitest::Test 4 | 5 | def test_hello_world 6 | context = Radius::Context.new 7 | context.define_tag "hello" do |tag| 8 | "Hello #{tag.attr['name'] || 'World'}!" 9 | end 10 | parser = Radius::Parser.new(context) 11 | assert_equal "

Hello World!

", parser.parse('

') 12 | assert_equal "

Hello John!

", parser.parse('

') 13 | end 14 | 15 | def test_example_2 16 | require 'kramdown' 17 | context = Radius::Context.new 18 | context.define_tag "markdown" do |tag| 19 | contents = tag.expand 20 | Kramdown::Document.new(contents).to_html 21 | end 22 | parser = Radius::Parser.new(context) 23 | assert_equal "

Hello World!

\n", parser.parse('Hello **World**!') 24 | end 25 | 26 | def test_nested_example 27 | context = Radius::Context.new 28 | 29 | context.define_tag "stooge" do |tag| 30 | content = '' 31 | ["Larry", "Moe", "Curly"].each do |name| 32 | tag.locals.name = name 33 | content << tag.expand 34 | end 35 | content 36 | end 37 | 38 | context.define_tag "stooge:name" do |tag| 39 | tag.locals.name 40 | end 41 | 42 | parser = Radius::Parser.new(context) 43 | 44 | template = <<-TEMPLATE 45 |
    46 | 47 |
  • 48 |
    49 |
50 | TEMPLATE 51 | 52 | output = <<-OUTPUT 53 |
    54 | 55 |
  • Larry
  • 56 | 57 |
  • Moe
  • 58 | 59 |
  • Curly
  • 60 | 61 |
62 | OUTPUT 63 | 64 | assert_equal output, parser.parse(template) 65 | end 66 | 67 | class User 68 | attr_accessor :name, :age, :email 69 | end 70 | def test_exposing_objects_example 71 | parser = Radius::Parser.new 72 | 73 | parser.context.define_tag "count", :for => 1 74 | assert_equal "1", parser.parse("") 75 | 76 | user = User.new 77 | user.name, user.age, user.email = "John", 29, "john@example.com" 78 | parser.context.define_tag "user", :for => user, :expose => [ :name, :age, :email ] 79 | assert_equal "John", parser.parse("") 80 | 81 | assert_equal "John", parser.parse("") 82 | end 83 | 84 | class LazyContext < Radius::Context 85 | def tag_missing(tag, attr, &block) 86 | "ERROR: Undefined tag `#{tag}' with attributes #{attr.inspect.sub(" => ", "=>")}" 87 | end 88 | end 89 | def test_tag_missing_example 90 | parser = Radius::Parser.new(LazyContext.new, :tag_prefix => 'lazy') 91 | output = %{ERROR: Undefined tag `weird' with attributes {"value"=>"true"}} 92 | assert_equal output, parser.parse('') 93 | end 94 | 95 | def test_tag_globals_example 96 | parser = Radius::Parser.new 97 | 98 | parser.context.define_tag "inc" do |tag| 99 | tag.globals.count ||= 0 100 | tag.globals.count += 1 101 | "" 102 | end 103 | 104 | parser.context.define_tag "count" do |tag| 105 | tag.globals.count || 0 106 | end 107 | 108 | assert_equal "0 1", parser.parse(" ") 109 | end 110 | 111 | class Person 112 | attr_accessor :name, :friend 113 | def initialize(name) 114 | @name = name 115 | end 116 | end 117 | def test_tag_locals_and_globals_example 118 | jack = Person.new('Jack') 119 | jill = Person.new('Jill') 120 | jack.friend = jill 121 | jill.friend = jack 122 | 123 | context = Radius::Context.new do |c| 124 | c.define_tag "jack" do |tag| 125 | tag.locals.person = jack 126 | tag.expand 127 | end 128 | c.define_tag "jill" do |tag| 129 | tag.locals.person = jill 130 | tag.expand 131 | end 132 | c.define_tag "name" do |tag| 133 | tag.locals.person.name rescue tag.missing! 134 | end 135 | c.define_tag "friend" do |tag| 136 | tag.locals.person = tag.locals.person.friend rescue tag.missing! 137 | tag.expand 138 | end 139 | end 140 | 141 | parser = Radius::Parser.new(context, :tag_prefix => 'r') 142 | 143 | assert_equal "Jack", parser.parse('') #=> "Jack" 144 | assert_equal "Jill", parser.parse('') #=> "Jill" 145 | assert_equal "Jack", parser.parse('') #=> "Jack" 146 | assert_equal "Jack", parser.parse('') #=> "Jack" 147 | assert_equal "Jack and Jill", parser.parse(' and ') #=> "Jack and Jill" 148 | assert_raises(Radius::UndefinedTagError) { parser.parse('') } # raises a Radius::UndefinedTagError exception 149 | end 150 | 151 | end 152 | -------------------------------------------------------------------------------- /test/squiggle_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/test_helper') 2 | require 'radius/parser/squiggle_scanner' 3 | 4 | class RadiusSquiggleTest < Minitest::Test 5 | include RadiusTestHelper 6 | 7 | def setup 8 | @context = new_context 9 | @parser = Radius::Parser.new(@context, :scanner => Radius::SquiggleScanner.new) 10 | end 11 | 12 | def test_sane_scanner_default 13 | assert !Radius::Parser.new.scanner.is_a?(Radius::SquiggleScanner) 14 | end 15 | 16 | def test_initialize_with_params 17 | @parser = Radius::Parser.new(:scanner => Radius::SquiggleScanner.new) 18 | assert_kind_of Radius::SquiggleScanner, @parser.scanner 19 | end 20 | 21 | def test_parse_individual_tags_and_parameters 22 | define_tag "add" do |tag| 23 | tag.attr["param1"].to_i + tag.attr["param2"].to_i 24 | end 25 | assert_parse_output "{3}", %[{{ add param1="1" param2='2'/}}] 26 | end 27 | 28 | def test_parse_attributes 29 | attributes = %[{"a"=>"1", "b"=>"2", "c"=>"3", "d"=>"'"}] 30 | assert_parse_output attributes, %[{attr a="1" b='2'c="3"d="'" /}] 31 | assert_parse_output attributes, %[{attr a="1" b='2'c="3"d="'"}{/attr}] 32 | end 33 | 34 | def test_parse_attributes_with_slashes_or_angle_brackets 35 | slash = %[{"slash"=>"/"}] 36 | angle = %[{"angle"=>">"}] 37 | assert_parse_output slash, %[{attr slash="/"}{/attr}] 38 | assert_parse_output slash, %[{attr slash="/"}{attr /}{/attr}] 39 | assert_parse_output angle, %[{attr angle=">"}{/attr}] 40 | end 41 | 42 | def test_parse_quotes 43 | assert_parse_output "test []", %[{echo value="test" /} {wrap attr="test"}{/wrap}] 44 | end 45 | 46 | def test_things_that_should_be_left_alone 47 | [ 48 | %[ test="2"="4" ], 49 | %[="2" ] 50 | ].each do |middle| 51 | assert_parsed_is_unchanged "{attr#{middle}/}" 52 | assert_parsed_is_unchanged "{attr#{middle}}" 53 | end 54 | end 55 | 56 | def test_tags_inside_html_tags 57 | assert_parse_output %[
tags in yo tags
], 58 | %[
tags in yo tags
] 59 | end 60 | 61 | def test_parse_result_is_always_a_string 62 | define_tag("twelve") { 12 } 63 | assert_parse_output "12", "{ twelve /}" 64 | end 65 | 66 | def test_parse_double_tags 67 | assert_parse_output "test".reverse, "{reverse}test{/reverse}" 68 | assert_parse_output "tset TEST", "{reverse}test{/reverse} {capitalize}test{/capitalize}" 69 | end 70 | 71 | def test_parse_tag_nesting 72 | define_tag("parent", :for => '') 73 | define_tag("parent:child", :for => '') 74 | define_tag("extra", :for => '') 75 | define_tag("nesting") { |tag| tag.nesting } 76 | define_tag("extra:nesting") { |tag| tag.nesting.gsub(':', ' > ') } 77 | define_tag("parent:child:nesting") { |tag| tag.nesting.gsub(':', ' * ') } 78 | assert_parse_output "nesting", "{nesting /}" 79 | assert_parse_output "parent:nesting", "{parent:nesting /}" 80 | assert_parse_output "extra > nesting", "{extra:nesting /}" 81 | assert_parse_output "parent * child * nesting", "{parent:child:nesting /}" 82 | assert_parse_output "parent > extra > nesting", "{parent:extra:nesting /}" 83 | assert_parse_output "parent > child > extra > nesting", "{parent:child:extra:nesting /}" 84 | assert_parse_output "parent * extra * child * nesting", "{parent:extra:child:nesting /}" 85 | assert_parse_output "parent > extra > child > extra > nesting", "{parent:extra:child:extra:nesting /}" 86 | assert_parse_output "parent > extra > child > extra > nesting", "{parent}{extra}{child}{extra}{nesting /}{/extra}{/child}{/extra}{/parent}" 87 | assert_parse_output "extra * parent * child * nesting", "{extra:parent:child:nesting /}" 88 | assert_parse_output "extra > parent > nesting", "{extra}{parent:nesting /}{/extra}" 89 | assert_parse_output "extra * parent * child * nesting", "{extra:parent}{child:nesting /}{/extra:parent}" 90 | assert_raises(Radius::UndefinedTagError) { @parser.parse("{child /}") } 91 | end 92 | def test_parse_tag_nesting_2 93 | define_tag("parent", :for => '') 94 | define_tag("parent:child", :for => '') 95 | define_tag("content") { |tag| tag.nesting } 96 | assert_parse_output 'parent:child:content', '{parent}{child:content /}{/parent}' 97 | end 98 | 99 | def test_parse_tag__binding_do_missing 100 | define_tag 'test' do |tag| 101 | tag.missing! 102 | end 103 | e = assert_raises(Radius::UndefinedTagError) { @parser.parse("{test /}") } 104 | assert_equal "undefined tag `test'", e.message 105 | end 106 | 107 | def test_parse_chirpy_bird 108 | # :> chirp chirp 109 | assert_parse_output "<:", "<:" 110 | end 111 | 112 | def test_parse_tag__binding_render_tag 113 | define_tag('test') { |tag| "Hello #{tag.attr['name']}!" } 114 | define_tag('hello') { |tag| tag.render('test', tag.attr) } 115 | assert_parse_output 'Hello John!', '{hello name="John" /}' 116 | end 117 | 118 | def test_accessing_tag_attributes_through_tag_indexer 119 | define_tag('test') { |tag| "Hello #{tag['name']}!" } 120 | assert_parse_output 'Hello John!', '{test name="John" /}' 121 | end 122 | 123 | def test_parse_tag__binding_render_tag_with_block 124 | define_tag('test') { |tag| "Hello #{tag.expand}!" } 125 | define_tag('hello') { |tag| tag.render('test') { tag.expand } } 126 | assert_parse_output 'Hello John!', '{hello}John{/hello}' 127 | end 128 | 129 | def test_tag_locals 130 | define_tag "outer" do |tag| 131 | tag.locals.var = 'outer' 132 | tag.expand 133 | end 134 | define_tag "outer:inner" do |tag| 135 | tag.locals.var = 'inner' 136 | tag.expand 137 | end 138 | define_tag "outer:var" do |tag| 139 | tag.locals.var 140 | end 141 | assert_parse_output 'outer', "{outer}{var /}{/outer}" 142 | assert_parse_output 'outer:inner:outer', "{outer}{var /}:{inner}{var /}{/inner}:{var /}{/outer}" 143 | assert_parse_output 'outer:inner:outer:inner:outer', "{outer}{var /}:{inner}{var /}:{outer}{var /}{/outer}:{var /}{/inner}:{var /}{/outer}" 144 | assert_parse_output 'outer', "{outer:var /}" 145 | end 146 | 147 | def test_tag_globals 148 | define_tag "set" do |tag| 149 | tag.globals.var = tag.attr['value'] 150 | '' 151 | end 152 | define_tag "var" do |tag| 153 | tag.globals.var 154 | end 155 | assert_parse_output " true false", %[{var /} {set value="true" /} {var /} {set value="false" /} {var /}] 156 | end 157 | 158 | def test_parse_loops 159 | @item = nil 160 | define_tag "each" do |tag| 161 | result = [] 162 | ["Larry", "Moe", "Curly"].each do |item| 163 | tag.locals.item = item 164 | result << tag.expand 165 | end 166 | result.join(tag.attr["between"] || "") 167 | end 168 | define_tag "each:item" do |tag| 169 | tag.locals.item 170 | end 171 | assert_parse_output %[Three Stooges: "Larry", "Moe", "Curly"], %[Three Stooges: {each between=", "}"{item /}"{/each}] 172 | end 173 | 174 | def test_parse_speed 175 | define_tag "set" do |tag| 176 | tag.globals.var = tag.attr['value'] 177 | '' 178 | end 179 | define_tag "var" do |tag| 180 | tag.globals.var 181 | end 182 | parts = %w{decima nobis augue at facer processus commodo legentis odio lectorum dolore nulla esse lius qui nonummy ullamcorper erat ii notare} 183 | multiplier = parts.map{|p| "#{p}=\"#{rand}\""}.join(' ') 184 | assert ->{ 185 | Timeout.timeout(10) do 186 | assert_parse_output " false", %[{set value="false" #{multiplier} /} {var /}] 187 | end 188 | }.call 189 | end 190 | 191 | def test_tag_option_for 192 | define_tag 'fun', :for => 'just for kicks' 193 | assert_parse_output 'just for kicks', '{fun /}' 194 | end 195 | 196 | def test_tag_expose_option 197 | define_tag 'user', :for => users.first, :expose => ['name', :age] 198 | assert_parse_output 'John', '{user:name /}' 199 | assert_parse_output '25', '{user}{age /}{/user}' 200 | e = assert_raises(Radius::UndefinedTagError) { @parser.parse "{user:email /}" } 201 | assert_equal "undefined tag `email'", e.message 202 | end 203 | 204 | def test_tag_expose_attributes_option_on_by_default 205 | define_tag 'user', :for => user_with_attributes 206 | assert_parse_output 'John', '{user:name /}' 207 | end 208 | def test_tag_expose_attributes_set_to_false 209 | define_tag 'user_without_attributes', :for => user_with_attributes, :attributes => false 210 | assert_raises(Radius::UndefinedTagError) { @parser.parse "{user_without_attributes:name /}" } 211 | end 212 | 213 | def test_tag_options_must_contain_a_for_option_if_methods_are_exposed 214 | e = assert_raises(ArgumentError) { define_tag('fun', :expose => :today) { 'test' } } 215 | assert_equal "tag definition must contain a :for option when used with the :expose option", e.message 216 | end 217 | 218 | def test_parse_fail_on_missing_end_tag 219 | assert_raises(Radius::MissingEndTagError) { @parser.parse("{reverse}") } 220 | end 221 | 222 | def test_parse_fail_on_wrong_end_tag 223 | assert_raises(Radius::WrongEndTagError) { @parser.parse("{reverse}{capitalize}{/reverse}") } 224 | end 225 | 226 | def test_copyin_global_values 227 | @context.globals.foo = 'bar' 228 | assert_equal 'bar', Radius::Parser.new(@context).context.globals.foo 229 | end 230 | 231 | def test_does_not_pollute_copied_globals 232 | @context.globals.foo = 'bar' 233 | parser = Radius::Parser.new(@context) 234 | parser.context.globals.foo = '[baz]' 235 | assert_equal 'bar', @context.globals.foo 236 | end 237 | 238 | def test_parse_with_other_namespaces 239 | @parser = Radius::Parser.new(@context, :tag_prefix => 'r') 240 | assert_equal "{fb:test}hello world{/fb:test}", @parser.parse("{fb:test}hello world{/fb:test}") 241 | end 242 | 243 | protected 244 | 245 | def assert_parse_output(output, input, message = nil) 246 | r = @parser.parse(input) 247 | assert_equal(output, r, message) 248 | end 249 | 250 | def assert_parsed_is_unchanged(something) 251 | assert_parse_output something, something 252 | end 253 | 254 | class User 255 | attr_accessor :name, :age, :email, :friend 256 | def initialize(name, age, email) 257 | @name, @age, @email = name, age, email 258 | end 259 | def <=>(other) 260 | name <=> other.name 261 | end 262 | end 263 | 264 | class UserWithAttributes < User 265 | def attributes 266 | { :name => name, :age => age, :email => email } 267 | end 268 | end 269 | 270 | def users 271 | [ 272 | User.new('John', 25, 'test@johnwlong.com'), 273 | User.new('James', 27, 'test@jameslong.com') 274 | ] 275 | end 276 | 277 | def user_with_attributes 278 | UserWithAttributes.new('John', 25, 'test@johnwlong.com') 279 | end 280 | 281 | end 282 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'timeout' 2 | require 'simplecov' 3 | SimpleCov.start do 4 | add_filter 'test' 5 | end 6 | 7 | require 'coveralls' 8 | if ENV['COVERALLS'] 9 | Coveralls.wear! 10 | end 11 | 12 | $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib') 13 | 14 | require 'radius' 15 | 16 | module RadiusTestHelper 17 | class TestContext < Radius::Context; end 18 | 19 | def new_context 20 | Radius::Context.new do |c| 21 | c.define_tag("reverse" ) { |tag| tag.expand.reverse } 22 | c.define_tag("capitalize") { |tag| tag.expand.upcase } 23 | c.define_tag("echo" ) { |tag| tag.attr['value'] } 24 | c.define_tag("wrap" ) { |tag| "[#{tag.expand}]" } 25 | c.define_tag("attr") do |tag| 26 | kv = tag.attr.keys.sort.collect{|k| "#{k.inspect}=>#{tag[k].inspect}"} 27 | "{#{kv.join(', ')}}" 28 | end 29 | end 30 | end 31 | 32 | def define_tag(name, options = {}, &block) 33 | @parser.context.define_tag name, options, &block 34 | end 35 | 36 | def define_global_tag(name, options = {}, &block) 37 | @context.define_tag name, options, &block 38 | end 39 | end 40 | require 'minitest/autorun' 41 | -------------------------------------------------------------------------------- /test/utility_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'radius' 3 | 4 | class RadiusUtilityTest < Minitest::Test 5 | 6 | def test_symbolize_keys 7 | h = Radius::Utility.symbolize_keys({ 'a' => 1, :b => 2 }) 8 | assert_equal h[:a], 1 9 | assert_equal h[:b], 2 10 | end 11 | 12 | def test_impartial_hash_delete 13 | h = { 'a' => 1, :b => 2 } 14 | assert_equal Radius::Utility.impartial_hash_delete(h, :a), 1 15 | assert_equal Radius::Utility.impartial_hash_delete(h, 'b'), 2 16 | assert_equal h.empty?, true 17 | end 18 | 19 | def test_constantize 20 | assert_equal Radius::Utility.constantize('String'), String 21 | end 22 | 23 | def test_camelize 24 | assert_equal Radius::Utility.camelize('ab_cd_ef'), 'AbCdEf' 25 | end 26 | 27 | def test_array_to_s 28 | assert_equal Radius::Utility.array_to_s(['a', 1, [:c], 'brain'.freeze]), 'a1cbrain' 29 | end 30 | end 31 | --------------------------------------------------------------------------------