module Radius # # A context contains the tag definitions which are available for use in a template. # See the QUICKSTART for a detailed explaination its # usage. # class Context # A hash of tag definition blocks that define tags accessible on a Context. attr_accessor :definitions # :nodoc: attr_accessor :globals # :nodoc: # Creates a new Context object. def initialize(*args, &block) @definitions = {} @tag_binding_stack = [] @globals = DelegatingOpenStruct.new with(&block) if block_given? end # Yield an instance of self for tag definitions: # # context.with do |c| # c.define_tag 'test' do # 'test' # end # end # def with yield self self end # Creates a tag definition on a context. Several options are available to you # when creating a tag: # # +for+:: Specifies an object that the tag is in reference to. This is # applicable when a block is not passed to the tag, or when the # +expose+ option is also used. # # +expose+:: Specifies that child tags should be set for each of the methods # contained in this option. May be either a single symbol/string or # an array of symbols/strings. # # +attributes+:: Specifies whether or not attributes should be exposed # automatically. Useful for ActiveRecord objects. Boolean. Defaults # to +true+. # def define_tag(name, options = {}, &block) type = Utility.impartial_hash_delete(options, :type).to_s klass = Utility.constantize('Radius::TagDefinitions::' + Utility.camelize(type) + 'TagFactory') rescue raise(ArgumentError.new("Undefined type `#{type}' in options hash")) klass.new(self).define_tag(name, options, &block) end # Returns the value of a rendered tag. Used internally by Parser#parse. def render_tag(name, attributes = {}, &block) if name =~ /^(.+?):(.+)$/ render_tag($1) { render_tag($2, attributes, &block) } else tag_definition_block = @definitions[qualified_tag_name(name.to_s)] if tag_definition_block # puts name: name, attributes: attributes stack(name, attributes, block) do |tag| tag_definition_block.call(tag).to_s end else tag_missing(name, attributes, &block) end end end # Like method_missing for objects, but fired when a tag is undefined. # Override in your own Context to change what happens when a tag is # undefined. By default this method raises an UndefinedTagError. def tag_missing(name, attributes, &block) raise UndefinedTagError.new(name) end # Returns the state of the current render stack. Useful from inside # a tag definition. Normally just use TagBinding#nesting. def current_nesting @tag_binding_stack.collect { |tag| tag.name }.join(':') end # make a usable copy of this context def dup # :nodoc: rv = self.class.new rv.globals = globals.dup rv.definitions = definitions.dup rv end private # A convienence method for managing the various parts of the # tag binding stack. def stack(name, attributes, block) previous = @tag_binding_stack.last previous_locals = previous.nil? ? globals : previous.locals locals = previous_locals.dup binding = TagBinding.new(self, locals, name, attributes, block) @tag_binding_stack.push(binding) result = yield(binding) @tag_binding_stack.pop result end # Returns a fully qualified tag name based on state of the # tag binding stack. def qualified_tag_name(name) nesting_parts = @tag_binding_stack.collect { |tag| tag.name } nesting_parts << name unless nesting_parts.last == name specific_name = nesting_parts.join(':') # specific_name always has the highest specificity unless @definitions.has_key? specific_name possible_matches = @definitions.keys.grep(/(^|:)#{name}$/) specificity = possible_matches.inject({}) { |hash, tag| hash[numeric_specificity(tag, nesting_parts)] = tag; hash } max = specificity.keys.max if max != 0 specificity[max] else name end else specific_name end end # Returns the specificity for +tag_name+ at nesting defined # by +nesting_parts+ as a number. def numeric_specificity(tag_name, nesting_parts) nesting_parts = nesting_parts.dup name_parts = tag_name.split(':') specificity = 0 value = 1 if nesting_parts.last == name_parts.last while nesting_parts.size > 0 if nesting_parts.last == name_parts.last specificity += value name_parts.pop end nesting_parts.pop value *= 0.1 end specificity = 0 if (name_parts.size > 0) end specificity end end end