=begin Directive DSL class MyDirective < DirectiveBase attr_arg :obj, :quote => true attr_arg :method { 'override' } attr_arg :foo { |x| x.downcase } attr_arg :collection, :quote => true attr_arg :value_method, :quote => true attr_arg :text_method, :quote => true attr_arg :options, :default => {} attr_arg :html_options, :append_element_attrs => [:common_html, :size] attr_arg :name do |value, args| if value.respond_to?(:include?) and value.include?('=>') args.unshift value nil else value end end attr_arg :params, :varargs => true #collect remaining args (if any) put in array, like *params event :before_stag do render erb_content( 'text_area', :obj, :method, :foo ) end event :content, :render => :nothing #rolls up all content between after_stag and before_etag also can be done event :content do render :nothing # causes nothing to be rendered end event :stag do # unless render something or render :nothing is called, then dcs.render will still occur implicitely end event :etag do con = render_result() # get the dcs.render result so we can modify it con.gsub!( /foo/, 'bar' ) render con end event :element do # this will allow replacement of entire element and children render erb_eval( 'execute something' ) end end event listing ---------------- before_stag stag after_stag content before_etag etag after_etag element these might not be exposed as events but still using direct methods characters comment =end module MasterView # The DirectiveProcessing module contains the internal mechanisms # which support the DirectiveDSL notation for attribute argument definition # and document processing event processing definition in directive # implementation classes. module DirectiveProcessing # holds the attr_arg definition, how a positional subargument will be # parsed out of the directive attr_value class AttrArgDef attr_reader :name, :options, :proc def initialize(name, options, proc) @name = name @options = options @proc = proc end # returns the optional default if one was specified def optional_default (@options.nil?) ? nil : @options[:default] end end # holds the event definition which contains the meat of what will happen # for each parser event. The events are more finely grained than the rexml # listener events. class EventDef BaseEventMapping = { /^(before_|after_)?(child_.*|descendant_.*)?stag$/ => ['\2stag'], /^(before_|after_)?(child_.*|descendant_.*)?etag$/ => ['\2etag'], /^(child_.*|descendant_.*)?content$/ => ['\1etag'], /^(child_.*|descendant_.*)?element$/ => ['\1stag', '\1etag'] } # true if is valid event name def self.valid_event_mapping?(event_name_sym) event_name_str = event_name_sym.to_s BaseEventMapping.keys.any? { |x| x =~ event_name_str } end # returns base method sym array for event_name_sym # :before_stag => :stag, # :stag => :stag, # :after_stag => :stag, # :content => :etag, # :before_etag => :etag, # :etag => :etag, # :after_etag => :etag, # :element => :stag, :etag def self.base_methods_from_event(event_name_sym) event_name_str = event_name_sym.to_s base_pair = BaseEventMapping.find {|k,v| k =~ event_name_str } base_name_unsub_array = base_pair[1] base_name_unsub_array.collect { |x| event_name_str.sub(base_pair[0], x).to_sym } end # return array of events to check for the base # :stag => [:before_stag, :stag, :after_stag], # :etag => [:content, :before_etag, :etag, :after_etag, :element] def self.method_events_from_base(base_sym) base_str = base_sym.to_s if match = base_str.match( /(.*)stag/ ) [ ('before_'+base_str).to_sym, base_sym, ('after_'+base_str).to_sym ] elsif match = base_str.match( /(.*)etag/ ) [ (match[1]+'content').to_sym, ('before_'+base_str).to_sym, base_sym, ('after_'+base_str).to_sym, (match[1]+'element').to_sym ] end end attr_reader :name, :options, :proc def initialize(name, options, proc) raise "Invalid event name=#{name}" unless self.class.valid_event_mapping?(name) @name = name @options = options @proc = proc end # execute proc/block in the context of an instance object (instance_eval) def exec(instance_obj) instance_obj.instance_eval &proc unless proc.nil? end end class DirectiveClassDef attr_reader :attr_arg_defs, :event_defs def initialize @attr_arg_defs = [] #list in order of position @attr_arg_def_map = {} #map to def from symname @event_defs = {} end def add_attr_arg_def(attr_arg_def) if @attr_arg_def_map[attr_arg_def.name].nil? # prevent multiple calls to this, rake calling multiple times? @attr_arg_def_map[attr_arg_def.name] = attr_arg_def @attr_arg_defs.push attr_arg_def end end def set_event_def(event_def) @event_defs[event_def.name] = event_def end def find_attr_arg_def_by_name(name) @attr_arg_def_map[name] end end class RenderAccumulator attr_accessor :render_call_found attr_writer :render_result, :render_result_block def initialize @method_level_accumulator = [] reset_event_level_vars end def reset_event_level_vars reset_event_level_content_array @render_call_found = false @render_result = nil @render_result_block = nil end def reset_event_level_content_array @event_level_accumulator = [] end def reset_event_level_content_array_render_nothing reset_event_level_content_array @render_call_found = true # we don't want to render anything end def method_level_content_array @method_level_accumulator end def event_level_content_array @event_level_accumulator end def add_event_content_to_method_content @method_level_accumulator.concat @event_level_accumulator @event_level_accumulator = [] end def replace_all_method_content_with_event_content @method_level_accumulator = @event_level_accumulator @event_level_accumulator = [] end def call_render_result_block result = nil unless @render_result_block.nil? result = @render_result_block.call @render_result_block = nil #only allow this call once end result end def add_to_event_content(value) @render_call_found = true @event_level_accumulator.push(value) end end end # Mixin for directive implementation classes to support the DSL for # attr_arg attribute value argument parsing and event handler # registration for document template event processing. # # Relies on the mixing class to provide accessor services to # element processing context information (element_attrs, element_tag) # and the attr_value directive attribute value string, # along with erb output services (erb_content, erb_eval). # Assumes DirectiveHelpers also mixed in. # module DirectiveDSL module ClassMethods # retrieve my class specific DirectiveClassDef, creating if necessary def directive_class_def @@directive_class_defs ||= {} @@directive_class_defs[object_id] ||= DirectiveProcessing::DirectiveClassDef.new end # add a postional subargument parsing def which will be applied to attr_value def attr_arg(name, options={}, &block) self.directive_class_def.add_attr_arg_def(DirectiveProcessing::AttrArgDef.new(name, options, block)) end # set the definition for an event which will be invoked at appropriate time in # directive rendering lifecycle, also create the base method if it does not # already exist. def event(name, options={}, &block) directive_class_def = self.directive_class_def directive_class_def.set_event_def(DirectiveProcessing::EventDef.new(name, options, block)) base_event_name_sym_array = DirectiveProcessing::EventDef.base_methods_from_event(name) base_event_name_sym_array.each do |base_event_name_sym| base_event_name = base_event_name_sym.to_s directive_class_def.set_event_def(DirectiveProcessing::EventDef.new(base_event_name_sym,{},nil)) if directive_class_def.event_defs[base_event_name_sym].nil? event_mapping = DirectiveProcessing::EventDef.method_events_from_base(base_event_name_sym) unless self.class.respond_to?( base_event_name_sym ) or event_mapping.nil? # unless already defined method or no mapping event_eval_code = <<-END def #{base_event_name}(dcs) @method_content_ref ||= {} # create references to content created by method prepare_arg_instance_vars @render_accumulator = DirectiveProcessing::RenderAccumulator.new #{event_mapping.inspect}.each do |full_event_name| event_def = self.class.directive_class_def.event_defs[full_event_name] unless event_def.nil? prepare(event_def) event_def.exec(self) handle_render(event_def) end end @method_content_ref[:#{base_event_name}] = @render_accumulator.method_level_content_array # add reference @method_content_ref[:#{base_event_name}] # return the reference so we can empty later if necessary end END self.class_eval event_eval_code # todo could pass in filename, but would have to use self.to_s.downcase and trim off leading modules end end end end # add class methods for DSL declarations to class which is mixing in DirectiveDSL def self.included(mixing_class) #:nodoc: mixing_class.extend(ClassMethods) end def prepare_arg_instance_vars attr_arg_defs = self.class.directive_class_def.attr_arg_defs unless attr_arg_defs.empty? # if we have any attr_arg_defs, prepare the parsed instance vars for these args args = parse(self.attr_value) attr_arg_defs.each do |attr_arg_def| eval_attr_arg(args, attr_arg_def) end end end def prepare(event_def) @render_accumulator.reset_event_level_vars case event_def.name.to_s when /^before_(.*_)?stag$/ when /^after_(.*_)?stag$/ when /^(.*_)?stag$/ @render_accumulator.render_result_block = lambda { @render_accumulator.render_result = @directive_call_stack.render } when /^(.*_)?content$/ @render_accumulator.render_result_block = lambda { @directive_call_stack.context[:tag].content.join } when /^before_(.*_)?etag$/ when /^after_(.*_)?etag$/ when /^(.*_)?etag$/ @render_accumulator.render_result_block = lambda { @render_accumulator.render_result = @directive_call_stack.render } when /^(.*_)?element$/ @render_accumulator.render_result_block = lambda { ( @method_content_ref[event_def.name.to_s.sub(/^(.*_)?element$/, '\1stag').to_sym] + # fine proper stag content ref @directive_call_stack.context[:tag].content + @render_accumulator.event_level_content_array).join } end end def render_result @render_accumulator.call_render_result_block end def render(value) if value == :nothing @render_accumulator.reset_event_level_content_array_render_nothing else @render_accumulator.add_to_event_content(value) end end def handle_render(event_def) return if event_def.options[:render] == :nothing and (event_def.name != :content and event_def.name != :element) case event_def.name.to_s when /^before_(.*_)?stag$/ @render_accumulator.add_event_content_to_method_content when /^after_(.*_)?stag$/ @render_accumulator.add_event_content_to_method_content when /^(.*_)?stag$/ render(render_result) unless @render_accumulator.render_call_found # call default dcs.render if render call not made @render_accumulator.add_event_content_to_method_content when /^(.*_)?content$/ if event_def.options[:render] == :nothing @directive_call_stack.context[:tag].content = nil else @directive_call_stack.context[:tag].content = @render_accumulator.event_level_content_array end when /^before_(.*_)?etag$/ @render_accumulator.add_event_content_to_method_content when /^after_(.*_)?etag$/ @render_accumulator.add_event_content_to_method_content when /^(.*_)?etag$/ render(render_result) unless @render_accumulator.render_call_found # call default dcs.render if render call not made @render_accumulator.add_event_content_to_method_content when /^(.*_)?element$/ # we need to affect only things on and below our callstack so surrounging directives will render (like if) @method_content_ref[event_def.name.to_s.sub(/^(.*_)?element$/, '\1stag').to_sym].clear # we will zero the stag reference which will clear it from the output @directive_call_stack.context[:tag].content = nil @render_accumulator.reset_event_level_content_array if event_def.options[:render] == :nothing # zero content if render :nothing @render_accumulator.replace_all_method_content_with_event_content # replace everything with event content end end # convert symbol name to instance var name, it appends a @ if not # already starting with it. Handles strings or symbols. # returns symbol of complete name :@name def name_to_instance_var_name(name) str_name = name.to_s str_name = '@'+str_name unless str_name[0] == '@' str_name.to_sym end # evaluate the attr_arg using the current attr_value, attributes, etc. # This is the method that takes the definition, determines the instance value # and stores it. def eval_attr_arg(args, attr_arg_def) name = attr_arg_def.name options = attr_arg_def.options proc = attr_arg_def.proc instance_var_name = name_to_instance_var_name(name) if(options and options[:varargs]) value = args.clone # value set to array of all remaining args args.clear else value = args.shift if options value = quote_if(value) if options[:quote] if merge_array = options[:append_element_attrs] merge_array = [merge_array] unless merge_array.is_a? Array merge_opts = {} merge_array.each do |merge_item| if merge_item == :common_html merge_opts.merge! common_html_options else merge_item_sym = merge_item.to_sym if (v = element_attrs[merge_item_sym]) : merge_opts[merge_item_sym] = v; end end end value = merge_hash_into_str(merge_opts, value) end end end unless proc.nil? if proc.arity < 1 # simply set value to block's return value = proc.call elsif proc.arity == 1 # |x| - pass value into block to allow it to manipulate value = proc.call(value) elsif proc.arity == 2 # |value, args| - pass value and remaining args array value = proc.call(value, args) elsif proc.arity == 3 # |value, args, instance| - pass value, remaining args, and directive instance value = proc.call(value, args, self) end end self.instance_variable_set(instance_var_name, value) # set @foo = value end # safely checks for existence of instance variable without throwing NameError # true if instance_variable exists def instance_variable_exists?(instance_variable_name) self.instance_variables.include?(instance_variable_name.to_s) end #inside characters, cdata, or comment you can call this to get the characters passed def data @directive_call_stack.context[:content_part] end #set the data that will be passed to characters, cdata, or comment directives def data=(data) @directive_call_stack.context[:content_part]=data end end end