module MasterView # The Analyzer module provides parsing and analysis services for the # TemplateSpec class to determine the parts of a template document # and their up-to-date status # module Analyzer ExpandAlwaysElements = %w{ script textarea } module Common ImportToNonImportDir = { "import_render" => "gen_partial", "import" => "generate", } NonImportToImportDir = ImportToNonImportDir.invert def calc_hash(data) #MasterView::Log.warn { 'hash= '+data.hash.to_s+' data='+data } data.gsub( /\s+/, ' ' ).hash #collapse whitespace end def src_hash(data_orig) data = convert_import_to_non_import_dirs(data_orig) calc_hash(data) end # gsub any import directives with non-import directives and return, makes clone before gsub def convert_import_to_non_import_dirs(text) text_clone = text.clone mv_ns = DirectiveRegistry.current.mv_namespace_prefix ImportToNonImportDir.sort.reverse.each do |i,n| # reverse order to substitute longer items first text_clone.gsub! mv_ns+i, mv_ns+n # mv:import_render to mv:gen_partial and mv:import to mv:generate end text_clone end # gsub any non-import directives with import directives and return, makes clone before gsub def convert_non_import_to_import_dirs(text) text_clone = text.clone mv_ns = DirectiveRegistry.current.mv_namespace_prefix NonImportToImportDir.sort.reverse.each do |i,n| # reverse order to substitute longer items first text_clone.gsub! mv_ns+i, mv_ns+n # mv:gen_partial to mv:import_render and mv:generate to mv:import end text_clone end end class ContentEntry include Common attr_accessor :hash, :data def initialize(data) @data = data @hash = src_hash(data) end end class StackEntry attr_accessor :name, :buffer, :depth, :import, :parts, :page_attributes def initialize(name, depth, import, page_attributes) @name = name @depth = depth @import = import @buffer = [] @parts = 0 @page_attributes = page_attributes # directives that are unique to page and ignored during synchronization end def inc_parts @parts += 1 end end class ListEntry attr_accessor :name, :index, :import, :page_attributes attr_writer :hash_invalid def initialize(name, index, import, page_attributes) @name = name @index = index @import = import @page_attributes = page_attributes end def hash_invalid? @hash_invalid end end # builder class facilitates the hash_check and building process by monitoring if all parts of a # file have been requested, if not when a higher index part is requested all the other previous # parts will be sent along as well, this is necessary for cases where a file doesn't have as many # internal parts or none at all. This way the hash and all the parts of the file will still be there # if layout has two parts and only last part -1 is requested, this builder will give back part 0 and part 1 # if layout would have had three parts then it would have given back all three. # if a part does not exist then it will come back as empty string, same for an index out of bounds class Builder include Common def initialize(content_hash) @content_hash = content_hash @builder_hash = {} #will contain next index to use end def hash(name, index) src_hash(data(name, index)) end def data(name, index) content = '' content_parts = @content_hash[name] content_parts_length = (content_parts) ? content_parts.length : 0 next_index_to_use = (nind = @builder_hash[name]) ? nind : 0 next_index_to_use = 0 if (-1 == index) && (next_index_to_use+1 > content_parts_length) # allow -1 to rerequest part as needed (singlefile) return '' if content_parts.nil? || (next_index_to_use+1 > content_parts_length) highest_index = content_parts.length - 1 requested_index = (index == -1) ? highest_index : index content = content_parts[next_index_to_use..requested_index].collect do |ce| (ce.data.gsub( /\s/, '' ).empty?) ? '' : ce.data # if only white space then get rid of it end.join @builder_hash[name] = requested_index + 1 # store next index to use content end end class Listener include REXML::SAX2Listener include DirectiveHelpers include Common attr_accessor :list, :content def initialize( options = {} ) @depth = 0 @stack = [] @list = [] @prolog = [] # prolog if any @content = options[:content_hash] || {} @options = options @only_check_hash = options[:only_check_hash] || false @builder = Builder.new(@content) @default_extname = options[:default_extname] || ::MasterView::IOMgr.erb.default_extension @keyword_expander = KeywordExpander.new @keyword_expander.set_template_pathname( options[:template_pathname], @default_extname ) if options[:template_pathname] mv_ns = DirectiveRegistry.current.mv_namespace_prefix @mv_processing_directives = { # attribute_qname => directive_type_flags mv_ns+'generate' => { :partial => false, :import => false }, mv_ns+'gen_partial' => { :partial => true, :import => false }, mv_ns+'import' => { :partial => false, :import => true }, mv_ns+'import_render' => { :partial => true, :import => true }, } @mv_import_directive_mappings = { # mv:import and mv:import_render get mapped to generate directives during analysis mv_ns+'import_render' => mv_ns+'gen_partial', mv_ns+'import' => mv_ns+'generate', } Log.debug { "only_check_hash => true" } if @only_check_hash end def only_check_hash? @only_check_hash end def xmldecl(version, encoding, standalone) #todo end def start_document #todo end def doctype(name, pub_sys, long_name, uri) xml_doctype = [] xml_doctype << '' @prolog << doctype_str end def start_element(uri, localname, qname, attributes) unescape_attributes!(attributes) @depth += 1 # expand the target specification to the actual path for import/generate directives in case of rebuilding path = nil import = false page_attributes = { } @mv_processing_directives.each_pair { | mv_attr_name, directive_type_flags | if attributes.has_key?( mv_attr_name ) expanded_attr_value = @keyword_expander.expand_keywords(attributes[mv_attr_name]) attributes[mv_attr_name] = expanded_attr_value if directive_type_flags[:partial] partial = find_string_val_in_string_hash( expanded_attr_value, :partial ) path = render_partial_name_to_file_name(partial, @default_extname) page_attributes[partial] = expanded_attr_value # save all the page specific values of gen_partial and import_partial by path attributes[mv_attr_name] = ":partial => '#{partial}'" # exclude others for comparison will be put back else path = expanded_attr_value end import = directive_type_flags[:import] break end } if path store_last_buffer false @stack << StackEntry.new(path, @depth, import, page_attributes) end unless @stack.empty? @stack.last.buffer << "<#{qname}" sorted_attributes = attributes.sort do |a,b| # sort import and import_render like generate and gen_partial so hashs work a_working = a[0] b_working = b[0] working_cmp = [a_working, b_working].collect do |working| # mv:import and mv:import_render to generate directives working = @mv_import_directive_mappings.fetch(working, working) working end working_cmp[0] <=> working_cmp[1] end sorted_attributes.each do |name, value| @stack.last.buffer << " #{name}=\"#{value}\"" end @stack.last.buffer << '>' #must output as separate string so simplify_empty_elements can find it end end def characters(text) @stack.last.buffer << text unless @stack.empty? end def comment(comment) @stack.last.buffer << '' unless @stack.empty? end def cdata(content) (@stack.last.buffer << '') unless @stack.empty? end def end_element(uri, localname, qname) unless @stack.empty? if @stack.last.buffer.last == '>' && MasterView::TemplateProcessing::Renderer::XHTMLEmptyElementNameSet.include?(qname) #collapse @stack.last.buffer.pop @stack.last.buffer << '/>' else @stack.last.buffer << '" #must output