module MasterView class RenderLevel #contains render modes, each gen level attr_accessor :render_modes def initialize(render_modes = []) @render_modes = render_modes end def push(render_mode) @render_modes.push render_mode end end class RenderMode #contains tags and output, each output style attr_accessor :output, :tags, :mode_type def initialize(output = nil, mode_type = :normal) @output = output @tags = [] @mode_type = mode_type end def tag @tags.last end def directives @tags.last.directives end def render_directives(method_name, context=tag.create_context) dcs = DirectiveCallStack.new tag_name = tag.tag_name.downcase depth = tags.size-1 @tags.each do |tag| case depth when 0 dcs << tag.directives.determine_dcs(method_name) when 1 dcs << tag.directives.determine_dcs(determine_method(:child, tag_name, method_name)) dcs << tag.directives.determine_dcs(determine_method(:child, :any, method_name)) dcs << tag.directives.determine_dcs(determine_method(:descendant, tag_name, method_name)) dcs << tag.directives.determine_dcs(determine_method(:descendant, :any, method_name)) else dcs << tag.directives.determine_dcs(determine_method(:descendant, tag_name, method_name)) dcs << tag.directives.determine_dcs(determine_method(:descendant, :any, method_name)) end depth -= 1 end dcs.context = context dcs.render end def determine_method(modifier, tag_name, base) type = "#{modifier.to_s}_#{tag_name.to_s}_#{base.to_s}" end end class DirectiveCallStack attr_reader :directives_to_call attr_accessor :context def initialize @directives_to_call = [] end def <<(directive_calls) if directive_calls.is_a? DirectiveCallStack @directives_to_call << directive_calls.directives_to_call else @directives_to_call << directive_calls end @directives_to_call.flatten! self end def render return [] if @directives_to_call.empty? directive_proc = @directives_to_call.shift directive_proc.call(self) end end class DirectiveSet attr_accessor :directives def initialize @directives = [] end def <<(directive) @directives << directive @directives.flatten! self end def determine_dcs(method_name) method_name_sym = method_name.to_sym dcs = DirectiveCallStack.new @directives.each do |directive| if directive.respond_to? method_name_sym dcs << create_call_proc(directive, method_name_sym) end end dcs end def create_call_proc(directive, method_name_sym) lambda do |dcs| directive.save_directive_call_stack(dcs) if directive.respond_to? :save_directive_call_stack directive.send(method_name_sym, dcs) end end end class SimpleRenderHandler def description 'SimpleRenderHandler is the default renderer for nodes, it should be invoked as the last directive and will output node normally' end def stag(dcs) context = dcs.context ret = [] ret << "<#{context[:tag].tag_name}" context[:tag].attributes.sort.each do |name, value| ret << " #{name}=\"#{value}\"" end ret << '>' #must output as separate string so simplify_empty_elements can find it end def characters(dcs) context = dcs.context [] << context[:content_part] end def comment(dcs) context = dcs.context [] << '' end def cdata(dcs) context = dcs.context [] << '' end def etag(dcs) context = dcs.context [] << '" #must output self, :mode_type => @mode_type }.merge!(values) end def data [] << @stag << @content << @etag end end class MIOSerializer def initialize(options) raise RequiredArgumentMissingError.new("Required argument is missing, specify the MasterViewIO object in options[:output_mio_tree]") unless options[:output_mio_tree] @options = options @mio_tree = options[:output_mio_tree] end def serialize(render_mode, tag) data_to_write = tag.data.join @mio_tree.path(render_mode.output).write(data_to_write, @options) end end =begin # Serializer which simply serializes output to the console class ConsoleSerializer def serialize(render_mode, tag) puts "outputing mode=#{render_mode.mode_type} to file=#{render_mode.output}" puts tag.data.join puts '' true end end # Serializer which simply outputs each fragment to a hash with the key representing # the path and the value the string contents. # You may specify this serializer as an option to the parser (:serializer => HashSerializer.new(output_hash)). # It takes an empty hash as the single constructor parameter to which the contents will be output. class HashSerializer def initialize( output_hash ) @output_hash = output_hash end def serialize(render_mode, tag) Log.debug { "adding to hash - outputing mode=#{render_mode.mode_type} to file=#{render_mode.output}" } @output_hash[ render_mode.output ] = tag.data.join true end end =end class Renderer include DirectiveHelpers # Set of element names that can be simplified. Used in simplify_empty_elements. # Only simplify elements that are specified in the DTD as being empty, collapsing others can cause parsing # or rendering problems in browsers. # http://www.w3.org/TR/xhtml1/dtds.html#a_dtd_XHTML-1.0-Strict # base, meta, link, hr, br, param, img, area, input, col XHTMLEmptyElementNameSet = %w{ base meta link hr br param img area input col }.to_set attr_reader :directive_load_paths, :mv_ns, :keyword_expander, :default_extension attr_accessor :render_levels, :default_render_handler, :serializer, :template_pathname, :template_full_pathname attr_reader :directives_registry #:nodoc: def self.last_renderer; @@last_renderer; end def initialize( options = {} ) @@last_renderer = self; #save last renderer for convenient access @default_render_handler = SimpleRenderHandler.new @render_levels = [ RenderLevel.new( [RenderMode.new] ) ] serializer = options[:serializer] || DefaultSerializer self.serializer = serializer.is_a?(Class) ? serializer.new(options) : serializer #one can pass in Serializer class or an instance self.mv_ns = options[:namespace] || NamespacePrefix self.template_pathname = options[:template_pathname] self.template_full_pathname = IOMgr.template.path(self.template_pathname).full_pathname if self.template_pathname @default_extension = (options[:output_mio_tree]) ? options[:output_mio_tree].default_extension : IOMgr.erb.default_extension @keyword_expander = KeywordExpander.new @keyword_expander.set_template_pathname(self.template_pathname, @default_extension) #ISSUE: if :additional_directive_paths then do cleaning and validity checks here and now # (then we don't need to keep re-checking in DirectiveRegistry or other processing) # [DJL 04-Jul-2006] self.directive_load_paths = ( DefaultDirectiveLoadPaths << options[:additional_directive_paths] ).flatten end def mv_ns=(namespace_prefix) @mv_ns = namespace_prefix Log.debug { 'namespace_prefix set to '+namespace_prefix } end # Sets directive_load_paths, re-requiring all the new load paths, however any directives that were # already required (and loaded) will still be in memory because these are not reset. def directive_load_paths=( directive_paths ) @directives_registry = MasterView::DirectivesRegistry #ISSUE: can we optimize this if there aren't :additional_directive_paths? # Do we even need/want the notion of per-invocation :additional_directive_paths # given that the directives load path is configurable for the client app? # [DJL 04-Jul-2006] directives_registry.load_directives( directive_paths ) #?? if directive_load_paths != DefaultDirectiveLoadPaths ?? # is this a good idea? no clear that we need/want to modify mv_ns after app initialization # If so, this could be simplifed. Discuss with jeffb. # [DJL 04-Jul-2006] if @mv_ns != NamespacePrefix @directives_registry = directives_registry.clone end # only need to do the following if :additional_directive_paths or nonstd @mv_ns??? directives_registry.build_directive_maps( @mv_ns ) end def modes @render_levels.last.render_modes end def push_level(render_level) @render_levels.push render_level end def push_tag(tag_name, attributes) modes.each do |mode| attributes_copy = attributes.clone #these get changed in select_active_directives directives = select_active_directives(tag_name, attributes_copy, mode) parent = (mode.tags.empty?) ? nil : mode.tag mode.tags.push Tag.new(directives, tag_name, attributes_copy, mode.mode_type, parent) mode.tag.stag << mode.render_directives(:stag) end end def append_content(type, content) modes.each do |mode| if mode.tag mode.tag.content << mode.render_directives( type, mode.tag.create_context(:content_part => content) ) end end end #does not call any directives, direct output def append_raw(raw_output) modes.each do |mode| if mode.tag mode.tag.content << raw_output end end end def pop_level @render_levels.pop end def pop_tag need_to_pop_level = false modes.each do |mode| mode.tag.etag << mode.render_directives(:etag) content = [] content << mode.tag.stag << mode.tag.content << mode.tag.etag content = simplify_empty_elements content last_tag = mode.tags.pop if mode.tags.empty? unless mode.output.nil? @serializer.serialize(mode, last_tag) need_to_pop_level = true end else #add it to the parent mode.tag.content << content end end pop_level if need_to_pop_level end # Simplify (collapse) empty elements so that from rexml parsing ends up being . # Only simplify elements that are specified in the DTD as being empty, collapsing others can cause parsing # or rendering problems in browsers. Uses constant XHTMLEmptyElementNameSet to find elements that should be # collapsed. # http://www.w3.org/TR/xhtml1/dtds.html#a_dtd_XHTML-1.0-Strict # xhtml-1.0-Strict empty elements are: # base, meta, link, hr, br, param, img, area, input, col def simplify_empty_elements(content) #relies on the fact that > and ' && last == '' ret.pop #remove '' # adding in a space to make xhtml more compatible with html editors and older browsers else ret << item end unless item.nil? if !item.starts_with?(' yval end sorted_directives << @default_render_handler #insure this is last selected << sorted_directives end end class MasterViewListener include REXML::SAX2Listener include DirectiveHelpers def initialize( options = {} ) @renderer = Renderer.new(options) end def xmldecl(version, encoding, standalone) #todo end def start_document #todo end def doctype(name, pub, sys, long_name, uri) #todo end def start_element(uri, localname, qname, attributes) unescape_attributes!(attributes) push_levels(attributes) @renderer.push_tag(qname, attributes) end def characters(text) @renderer.append_content(:characters, text) end def comment(comment) @renderer.append_content(:comment, comment) end def cdata(content) @renderer.append_content(:cdata, content) end def end_element(uri, localname, qname) @renderer.pop_tag end def end_document #todo end def unescape_attributes!(attributes) attributes.each do |name, value| value.replace CGI::unescapeHTML(value) value.gsub!(''', '\'') end end def generate_replace(value) @renderer.append_raw ERB_EVAL+value+ERB_END end # handle a mv:gen_partial attribute, which calls generate and outputs a token # it takes an optional :dir => 'foo/bar' which is prepended to partial path, # otherwise it just uses what is in partial. # This creates a generate attribute value which will be used later. # Parameters # value = attribute value for gen_partial # attributes = all remaining attributes hash def handle_gen_partial(attributes) value = @renderer.keyword_expander.resolveAttrAndDelete(attributes, @renderer.mv_ns+'gen_partial') if value prepend_dir = find_string_val_in_string_hash(value, :dir) #only used for masterview partial = find_string_val_in_string_hash(value, :partial) return if partial.nil? path = render_partial_name_to_file_name(partial, @renderer.default_extension) path = File.join(prepend_dir, path) if prepend_dir generate_attribute = attributes[@renderer.mv_ns+'generate'] || '' # check if we need to add to existing generate generate_attribute = path + (generate_attribute.blank? ? '' : ', '+generate_attribute) attributes[@renderer.mv_ns+'generate'] = generate_attribute @renderer.append_raw( ERB_EVAL+'render( '+value+' )'+ERB_END ) end end def push_levels(attributes) handle_gen_partial(attributes) gen_replace = @renderer.keyword_expander.resolveAttrAndDelete(attributes, @renderer.mv_ns+'gen_replace') #get and delete from map generate_replace( gen_replace ) unless gen_replace.nil? gen = @renderer.keyword_expander.resolveAttrAndDelete(attributes, @renderer.mv_ns+'generate') #get and delete from map if gen omit_comment = OmitGeneratedComments || File.extname(gen) != MasterView::IOMgr.erb.default_extension attributes[@renderer.mv_ns+'insert_generated_comment'] = @renderer.template_full_pathname.to_s unless omit_comment #add the comment directive, so it will be written to each gen'd file render_level = nil gen_values = parse_eval_into_hash(gen, :normal) #Log.debug { 'generate_hash='+gen_values.inspect } gen_values.each do |key,value| mode_type = key.to_sym arr_values = (value.is_a?(Enumerable)) ? value : [value] #if not enumerable add it to array value.each do |path| path.strip! #Log.debug { ('pushing mode='+mode_type.to_s+' path='+path).indent(2*@renderer.render_levels.size) } render_level ||= RenderLevel.new render_level.push RenderMode.new(path, mode_type) end end @renderer.push_level(render_level) unless render_level.nil? end end end class Parser def self.parse_mio( template_mio, output_mio_tree, options = {}) options[:template_pathname]= template_mio.pathname options[:output_mio_tree] = output_mio_tree template = template_mio.read self.parse( template, options ) end # parse a MasterView template and render output. # template param is actual template source passed in as string or array. # options are the optional parameters which control the output (:output_mio_tree, :namespace, :serializer) def self.parse( template, options = DefaultParserOptions.clone) options[:listeners] ||= [MasterViewListener] options[:rescue_exceptions] = RescueExceptions unless options.include?(:rescue_exceptions) begin if options[:tidy] template = TidyHelper.tidy(template) elsif options[:escape_erb] template = EscapeErbHelper.escape_erb(template) end parser = REXML::Parsers::SAX2Parser.new( template ) options[:listeners].each do |listener| if listener.is_a? Class parser.listen( listener.new(options) ) else parser.listen(listener) end end parser.parse rescue Exception => e if options[:rescue_exceptions] Log.error { "Failure to parse template. Exception="+e } Log.debug { e.backtrace.join("\n") } else raise end end end end end