module MasterView # TemplateSpec is a class which contains the information about how to build a # template. It contains the information on all the parts that make up the template # and the status of them (whether they are up to date or out of sync). class TemplateSpec attr_accessor :path, :status, :message, :gen_parts, :build_list class Status OK = 'OK' ImportsOutdated = 'Imports(s) outdated' Conflicts = 'Conflict(s)' InvalidXHTML = 'Invalid XHTML' end def initialize(path = nil) @path = path @status = '' @message = '' @gen_parts = [] @build_list = [] end # get short base name of path def basename File.basename @path end # get path relative to working dir def long_path IOMgr.template.path(@path).full_pathname.to_s end # scan the directory of templates, building template_specs and the content_hash def self.scan(options = {}, &block) content_hash = {} template_specs = {} IOMgr.template.find(:pattern => TemplateFilenamePattern) do |mio| template_specs[mio.pathname.to_s] = scan_template_mio(mio, content_hash) end IOMgr.template.find(:pattern => TemplateFilenamePattern) do |mio| path = mio.pathname.to_s if template_specs[path].status == Status::OK invalid_parts = template_mio_out_of_sync?(path, content_hash) template_spec = template_specs[path] template_spec.update_status_from_invalid_parts(invalid_parts) end yield template_specs[path], content_hash if block_given? end return template_specs, content_hash end def self.scan_template_mio(mio, content_hash = {} ) template = mio.read(:disable_cache => true) path = mio.pathname.to_s self.scan_template(template, path, content_hash) end def update_status_from_invalid_parts(invalid_parts) unless invalid_parts.empty? @status = Status::ImportsOutdated @message = "Outdated parts: "+invalid_parts.join(', ') end end # create a template_spec def self.scan_template(template, path, content_hash = {}) template_spec = TemplateSpec.new(path) listener = MasterView::Analyzer::Listener.new( :template_pathname => Pathname.for_path(path) ) begin MasterView::Parser.parse( template, :rescue_exceptions => false, :listeners => [listener]) template_spec.build_list = listener.list conflicts = content_hash.keys & listener.content.keys content_hash.merge! listener.content if conflicts.empty? template_spec.status = Status::OK else template_spec.status = Status::Conflicts template_spec.message = 'Duplicated generation of: ' + conflicts.sort.join(', ') end template_spec.gen_parts = listener.content.keys.sort rescue REXML::ParseException => e template_spec.status = Status::InvalidXHTML template_spec.message = e.to_s end template_spec end def self.template_mio_out_of_sync?(path, content_hash) mio = IOMgr.template.path(path) template = mio.read(:disable_cache => true) self.template_out_of_sync?(template, path, content_hash) end # check if the template is out of sync with the source content (check imports), return array of invalid def self.template_out_of_sync?(template, path, content_hash) invalid = [] listener = MasterView::Analyzer::Listener.new(:content_hash => content_hash, :template_pathname => Pathname.for_path(path), :only_check_hash => true) Parser.parse( template, :rescue_exceptions => false, :listeners => [listener] ) invalid_list_items = listener.list.find_all { |li| li.hash_invalid? } invalid_with_dups = invalid_list_items.collect { |li| li.name } invalid = invalid_with_dups.uniq.sort invalid end # rebuild template updating all imports, returns the string contents, # if options[:write_to_file] = true then it will write the contents to file if different and return true, if identical then returns false # otherwise this method returns the content of the template # raise error for any other problems. options[:backup] = true to make backup before rebuilding, uses MasterView::IOMgr.backup to determine # path for where to store backup files. If MasterView::IOMgr.backup is nil, then no backup is created. def rebuild_template(content_hash, options = {} ) out = [] builder = MasterView::Analyzer::Builder.new(content_hash) @build_list.each do |li| #Log.debug { li.inspect } con = builder.data(li.name, li.index) if li.import con = con.gsub NamespacePrefix+'generate', NamespacePrefix+'import' con.gsub! NamespacePrefix+'gen_partial', NamespacePrefix+'import_render' end out << con end template = out.join if options[:write_to_file] backup = !options[:backup].nil? ? options[:backup] : !IOMgr.backup.nil? orig = IOMgr.template.path(self.path).read(:disable_cache => true) file_written = false unless template == orig self.backup_file if backup IOMgr.template.path(self.path).write(template, :force => true) #force write in case tidy had cleaned up file_written = true end return file_written end template end # create backup file by appending secs since epoch to filename def backup_file(options={} ) return unless IOMgr.backup contents_to_backup = IOMgr.template.path(self.path).read backup_path = self.path+'.'+Time.new.to_i.to_s Log.debug { 'creating backup file '+backup_path } IOMgr.backup.path(backup_path).write(contents_to_backup) end # create empty shell file consisting of layout and a comment for where to insert new content, return contents def self.create_empty_shell(template_spec_to_copy, content_hash, content_to_insert, options = {} ) from_spec = template_spec_to_copy content_hash['empty_shell_contents'] = [] content_hash['empty_shell_contents'] << MasterView::Analyzer::ContentEntry.new(content_to_insert) to_spec = TemplateSpec.new to_spec.build_list = [] li = from_spec.build_list.first li.import = true to_spec.build_list << li to_spec.build_list << MasterView::Analyzer::ListEntry.new('empty_shell_contents', -1, false) li = from_spec.build_list.last li.import = true to_spec.build_list << li to_spec.rebuild_template(content_hash) end class CreateShellERBValues attr_reader :controller_name, :action_name, :controller_file_name, :controller_view_dir_name def initialize(controller_name, action_name) @controller_name = Inflector.underscore(Inflector.singularize(controller_name)) @action_name = Inflector.underscore(Inflector.singularize(action_name)) @controller_file_name = @controller_name @controller_view_dir_name = @controller_file_name end def get_binding binding end end # create empty shell file consisting of layout and a comment for where to insert new content. Use action_to_create # to infer the destination name controller_action.html and to customize the inserted place holder. Pass the # empty insert erb (rhtml) content in which will be rendered with appropriate controller and action values. # Valid options: # :write_to_file => true (write to file and return filename, if false then simply return generated contents) # :template_source => source_for_template (use this source instead of reading from file) # :content_hash => use this content_hash, otherwise it will scan the masterview template directory to create content_hash def self.create_empty_shell_for_action(path_to_copy_shell_from, action_to_create, empty_insert_erb, options={} ) path = IOMgr.template.cleanup_path_get_relative_pathname(path_to_copy_shell_from).to_s controller_name = Pathname.for_path(path).dirname.to_s extension = IOMgr.template.default_extension erb_values = CreateShellERBValues.new(controller_name, action_to_create) template = ERB.new(empty_insert_erb) content_to_insert = template.result(erb_values.get_binding).strip #clear off surrounding whitespace that makes it difficult to debug basename_with_extension = erb_values.action_name basename_with_extension += extension if extension dst_path = (Pathname.for_path(path).dirname+(basename_with_extension)).to_s src = (tsrc = options[:template_source]) ? tsrc : IOMgr.template.path(path).read template_specs = {} content_hash = options[:content_hash] template_specs, content_hash = TemplateSpec.scan unless content_hash template_spec_to_copy = template_specs[path] || TemplateSpec.scan_template(src, path) result = TemplateSpec.create_empty_shell( template_spec_to_copy, content_hash, content_to_insert) if options[:write_to_file] template_mio = IOMgr.template.path(dst_path) raise 'Template '+dst_path+' already exists, operation aborted.' if template_mio.exist? template_mio.write(result) return dst_path end result end end end