323 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
		
		
			
		
	
	
			323 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
|  | = Radius Quick Start | ||
|  | 
 | ||
|  | 
 | ||
|  | == Defining Tags | ||
|  | 
 | ||
|  | Before you can parse a template with Radius you need to create a Context object which defines | ||
|  | the tags that will be used in the template. This is actually quite simple: | ||
|  | 
 | ||
|  |   require 'radius' | ||
|  |    | ||
|  |   context = Radius::Context.new | ||
|  |   context.define_tag "hello" do |tag| | ||
|  |     "Hello #{tag.attr['name'] || 'World'}!" | ||
|  |   end | ||
|  | 
 | ||
|  | Once you have defined a context you can easily create a Parser: | ||
|  | 
 | ||
|  |   parser = Radius::Parser.new(context) | ||
|  |   puts parser.parse('<p><radius:hello /></p>') | ||
|  |   puts parser.parse('<p><radius:hello name="John" /></p>') | ||
|  | 
 | ||
|  | This code will output: | ||
|  | 
 | ||
|  |   <p>Hello World!</p> | ||
|  |   <p>Hello John!</p> | ||
|  | 
 | ||
|  | Note how you can pass attributes from the template to the context using the attributes hash. | ||
|  | Above, the first tag that was parsed didn't have a name attribute so the code in the +hello+ | ||
|  | tag definition uses "World" instead. The second time the tag is parsed the name attribute is | ||
|  | set to "John" which is used to create the string "Hello John!". Tags that do not follow this | ||
|  | rule will be treated as if they were undefined (like normal methods). | ||
|  | 
 | ||
|  | 
 | ||
|  | == Container Tags | ||
|  | 
 | ||
|  | Radius also allows you to define "container" tags. That is, tags that contain content and | ||
|  | that may optionally manipulate it in some way. For example, if you have RedCloth installed | ||
|  | you could define another tag to parse and create Textile output: | ||
|  | 
 | ||
|  |   require 'redcloth' | ||
|  |    | ||
|  |   context.define_tag "textile" do |tag| | ||
|  |     contents = tag.expand | ||
|  |     RedCloth.new(contents).to_html | ||
|  |   end | ||
|  | 
 | ||
|  | (The code <tt>tag.expand</tt> above returns the contents of the template between the start and end | ||
|  | tags.) | ||
|  | 
 | ||
|  | With the code above your parser can easily handle Textile: | ||
|  | 
 | ||
|  |   parser.parse('<radius:textile>h1. Hello **World**!</radius:textile>') | ||
|  | 
 | ||
|  | This code will output: | ||
|  | 
 | ||
|  |   <h1>Hello <b>World</b>!</h1> | ||
|  | 
 | ||
|  | 
 | ||
|  | == Nested Tags | ||
|  | 
 | ||
|  | But wait!--it gets better. Because container tags can manipulate the content they contain | ||
|  | you can use them to iterate over collections: | ||
|  | 
 | ||
|  |   context = Radius::Context.new | ||
|  |    | ||
|  |   context.define_tag "stooge" do |tag| | ||
|  |     content = '' | ||
|  |     ["Larry", "Moe", "Curly"].each do |name| | ||
|  |       tag.locals.name = name | ||
|  |       content << tag.expand | ||
|  |     end | ||
|  |     content | ||
|  |   end | ||
|  |    | ||
|  |   context.define_tag "stooge:name" do |tag| | ||
|  |     tag.locals.name | ||
|  |   end | ||
|  |    | ||
|  |   parser = Radius::Parser.new(context) | ||
|  |    | ||
|  |   template = <<-TEMPLATE | ||
|  |   <ul> | ||
|  |   <radius:stooge> | ||
|  |     <li><radius:name /></li> | ||
|  |   </radius:stooge> | ||
|  |   </ul> | ||
|  |   TEMPLATE | ||
|  |    | ||
|  |   puts parser.parse(template) | ||
|  |    | ||
|  | This code will output: | ||
|  | 
 | ||
|  |   <ul> | ||
|  |    | ||
|  |     <li>Larry</li> | ||
|  |    | ||
|  |     <li>Moe</li> | ||
|  |    | ||
|  |     <li>Curly</li> | ||
|  |    | ||
|  |   </ul> | ||
|  | 
 | ||
|  | Note how the definition for the +name+ tag is defined. Because "name" is prefixed | ||
|  | with "stooge:" the +name+ tag cannot appear outside the +stooge+ tag. Had it been defined | ||
|  | simply as "name" it would be valid anywhere, even outside the +stooge+ tag (which was | ||
|  | not what we wanted). Using the colon operator you can define tags with any amount of | ||
|  | nesting. | ||
|  | 
 | ||
|  | 
 | ||
|  | == Exposing Objects to Templates | ||
|  | 
 | ||
|  | During normal operation, you will often want to expose certain objects to your templates. | ||
|  | Writing the tags to do this all by hand would be cumbersome of Radius did not provide | ||
|  | several mechanisms to make this easier. The first is a way of exposing objects as tags | ||
|  | on the context object. To expose an object simply call the +define_tag+ | ||
|  | method with the +for+ option: | ||
|  | 
 | ||
|  |   context.define_tag "count", :for => 1 | ||
|  | 
 | ||
|  | This would expose the object <tt>1</tt> to the template as the +count+ tag. It's basically the | ||
|  | equivalent of writing: | ||
|  | 
 | ||
|  |   context.define_tag("count") { 1 } | ||
|  | 
 | ||
|  | So far this doesn't save you a whole lot of typing, but suppose you want to expose certain | ||
|  | methods that are on that object? You could do this: | ||
|  | 
 | ||
|  |   context.define_tag "user", :for => user, :expose => [ :name, :age, :email ] | ||
|  | 
 | ||
|  | This will add a total of four tags to the context. One for the <tt>user</tt> variable, and | ||
|  | one for each of the three methods listed in the +expose+ clause. You could now get the user's | ||
|  | name inside your template like this: | ||
|  | 
 | ||
|  |   <radius:user><radius:name /></radius:user> | ||
|  | 
 | ||
|  | If "John" was the value stored in <tt>user.name</tt> the template would render as "John". | ||
|  | 
 | ||
|  | 
 | ||
|  | == Tag Shorthand | ||
|  | 
 | ||
|  | In the example above we made reference to <tt>user.name</tt> in our template by using the | ||
|  | following code: | ||
|  | 
 | ||
|  |   <radius:user><radius:name /></radius:user> | ||
|  |    | ||
|  | There is a much easer way to refer to the <tt>user.name</tt> variable. Use the colon operator | ||
|  | to "scope" the reference to <tt>name</tt>: | ||
|  | 
 | ||
|  |   <radius:user:name /> | ||
|  | 
 | ||
|  | Radius allows you to use this shortcut for all tags. | ||
|  |    | ||
|  | 
 | ||
|  | == Changing the Tag Prefix | ||
|  | 
 | ||
|  | By default, all Radius tags must begin with "radius". You can change this by altering the | ||
|  | tag_prefix attribute on a Parser. For example: | ||
|  | 
 | ||
|  |   parser = Radius::Parser.new(context, :tag_prefix => 'r') | ||
|  | 
 | ||
|  | Now, when parsing templates with +parser+, Radius will require that every tag begin with "r" | ||
|  | instead of "radius". | ||
|  | 
 | ||
|  | 
 | ||
|  | == Custom Behavior for Undefined Tags | ||
|  | 
 | ||
|  | Context#tag_missing behaves much like Object#method_missing only it allows you to define | ||
|  | specific behavior for when a tag is not defined on a Context. For example: | ||
|  | 
 | ||
|  |   class LazyContext < Radius::Context | ||
|  |     def tag_missing(tag, attr, &block) | ||
|  |       "<strong>ERROR: Undefined tag `#{tag}' with attributes #{attr.inspect}</strong>" | ||
|  |     end | ||
|  |   end | ||
|  |    | ||
|  |   parser = Radius::Parser.new(LazyContext.new, :tag_prefix => 'lazy') | ||
|  |   puts parser.parse('<lazy:weird value="true" />') | ||
|  | 
 | ||
|  | This will output: | ||
|  | 
 | ||
|  |   <strong>ERROR: Undefined tag `weird' with attributes {"value"=>"true"}</strong> | ||
|  | 
 | ||
|  | Normally, when the Radius Parser encounters an undefined tag for a Context it raises an | ||
|  | UndefinedTagError, but since we have defined #tag_missing on LazyContext the Parser now | ||
|  | outputs a nicely formated error message when we parse a string that does not contain a | ||
|  | valid tag. | ||
|  | 
 | ||
|  | 
 | ||
|  | == Tag Bindings | ||
|  | 
 | ||
|  | Radius passes a TagBinding into the block of the Context#define_tag method. The tag | ||
|  | binding is useful for a number of tasks. A tag binding has an #expand instance method | ||
|  | which processes a tag's contents and returns the result. It also has a #attr method | ||
|  | which returns a hash of the attributes that were passed into the tag. TagBinding also | ||
|  | contains the TagBinding#single? and TagBinding#double? methods which return true or false | ||
|  | based on wether the tag is a container tag or not. More about the methods which are | ||
|  | available on tag bindings can be found on the Radius::TagBinding documentation page. | ||
|  | 
 | ||
|  | 
 | ||
|  | == Tag Binding Locals, Globals, and Context Sensitive Tags | ||
|  | 
 | ||
|  | A TagBinding also contains two OpenStruct-like objects which are useful when developing | ||
|  | tags. TagBinding#globals is useful for storing variables which you would like to be | ||
|  | accessible to all tags: | ||
|  | 
 | ||
|  |   context.define_tag "inc" do |tag| | ||
|  |     tag.globals.count ||= 0 | ||
|  |     tag.globals.count += 1 | ||
|  |     "" | ||
|  |   end | ||
|  |    | ||
|  |   context.define_tag "count" do |tag| | ||
|  |     tag.globals.count || 0 | ||
|  |   end | ||
|  | 
 | ||
|  | TagBinding#locals mirrors the variables that are in TagBinding#globals, but allows child | ||
|  | tags to redefine variables. This is valuable when defining context sensitive tags: | ||
|  | 
 | ||
|  |   class Person | ||
|  |     attr_accessor :name, :friend | ||
|  |     def initialize(name) | ||
|  |       @name = name | ||
|  |     end | ||
|  |   end | ||
|  |    | ||
|  |   jack = Person.new('Jack') | ||
|  |   jill = Person.new('Jill') | ||
|  |   jack.friend = jill | ||
|  |   jill.friend = jack | ||
|  |    | ||
|  |   context = Radius::Context.new do |c| | ||
|  |     c.define_tag "jack" do |tag| | ||
|  |       tag.locals.person = jack | ||
|  |       tag.expand | ||
|  |     end | ||
|  |     c.define_tag "jill" do |tag| | ||
|  |       tag.locals.person = jill | ||
|  |       tag.expand | ||
|  |     end | ||
|  |     c.define_tag "name" do |tag| | ||
|  |       tag.locals.person.name rescue tag.missing! | ||
|  |     end | ||
|  |     c.define_tag "friend" do |tag| | ||
|  |       tag.locals.person = tag.locals.person.friend rescue tag.missing! | ||
|  |       tag.expand | ||
|  |     end | ||
|  |   end | ||
|  |    | ||
|  |   parser = Radius::Parser.new(context, :tag_prefix => 'r') | ||
|  |    | ||
|  |   parser.parse('<r:jack:name />') #=> "Jack" | ||
|  |   parser.parse('<r:jill:name />') #=> "Jill" | ||
|  |   parser.parse('<r:jill:friend:name />') #=> "Jack" | ||
|  |   parser.parse('<r:jack:friend:friend:name />') #=> "Jack" | ||
|  |   parser.parse('<r:jill><r:friend:name /> and <r:name /></r:jill>') #=> "Jack and Jill" | ||
|  |   parser.parse('<r:name />') # raises a Radius::UndefinedTagError exception | ||
|  | 
 | ||
|  | Notice how TagBinding#locals enables intelligent nesting. "<r:jill:name />" evaluates to | ||
|  | "Jill", but "<r:jill:friend:name />" evaluates to "Jack". Locals lose scope as soon as | ||
|  | the tag they were defined in closes. Globals on the other hand, never lose scope. | ||
|  | 
 | ||
|  | The final line in the example above demonstrates that calling "<r:name />" raises a | ||
|  | TagMissing error. This is because of the way the name tag was defined: | ||
|  | 
 | ||
|  |   tag.locals.person.name rescue tag.missing! | ||
|  | 
 | ||
|  | If person is not defined on locals it will return nil. Calling #name on nil would normally | ||
|  | raise a NoMethodError exception, but because of the 'rescue' clause the TagBinding#missing! | ||
|  | method is called which fires off Context#tag_missing. By default Context#tag_missing raises | ||
|  | a UndefinedTagError exception. The 'rescue tag.missing!' idiom is extremly useful for adding | ||
|  | simple error checking to context sensitive tags. | ||
|  | 
 | ||
|  | 
 | ||
|  | == Tag Specificity | ||
|  | 
 | ||
|  | When Radius is presented with two tags that have the same name, but different nesting | ||
|  | Radius uses an algorithm similar to the way winning rules are calculated in Cascading Style | ||
|  | Sheets (CSS) to determine which definition should be used. Each time a tag is encountered | ||
|  | in a template potential tags are assigned specificity values and the tag with the highest | ||
|  | specificity wins. | ||
|  | 
 | ||
|  | For example, given the following tag definitions: | ||
|  | 
 | ||
|  |   nesting | ||
|  |   extra:nesting | ||
|  |   parent:child:nesting | ||
|  | 
 | ||
|  | And template: | ||
|  | 
 | ||
|  |   <r:parent:extra:child:nesting /> | ||
|  | 
 | ||
|  | Radius will calculate specificity values like this: | ||
|  | 
 | ||
|  |   nesting => 1.0.0.0 | ||
|  |   extra:nesting => 1.0.1.0 | ||
|  |   parent:child:nesting => 1.1.0.1 | ||
|  | 
 | ||
|  | Meaning that parent:child:nesting will win. If a template contained: | ||
|  | 
 | ||
|  |   <r:parent:child:extra:nesting /> | ||
|  | 
 | ||
|  | The following specificity values would be assigned to each of the tag definitions: | ||
|  | 
 | ||
|  |   nesting => 1.0.0.0 | ||
|  |   extra:nesting => 1.1.0.0 | ||
|  |   parent:child:nesting => 1.0.1.1 | ||
|  | 
 | ||
|  | Meaning that extra:nesting would win because it is more "specific". | ||
|  | 
 | ||
|  | Values are assigned by assigning points to each of the tags from right to left. | ||
|  | Given a tag found in a template with nesting four levels deep, the maximum | ||
|  | specificity a tag could be assigned would be: | ||
|  | 
 | ||
|  |   1.1.1.1 | ||
|  | 
 | ||
|  | One point for each of the levels. | ||
|  | 
 | ||
|  | In practice, you don't need to understand this topic to be effective with Radius. | ||
|  | For the most part you will find that Radius resolves tags precisely the way that | ||
|  | you would expect. If you find this section confusing forget about it and refer | ||
|  | back to it if you find that tags are resolving differently from the way that you | ||
|  | expected. |