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 # scan the directory of templates, building template_specs and the content_hash def self.scan(options = {}, &block) content_hash = {} template_specs = {} files = [] Find.find(File.join('app/views', TemplateSrcRelativePath)){ |f| files << f } files.sort! files.each do |path| if !File.directory?(path) && File.fnmatch?(TemplateFilenamePattern, path) template_specs[path] = scan_template_file(path, content_hash) end end files.each do |path| if !File.directory?(path) && File.fnmatch?(TemplateFilenamePattern, path) if template_specs[path].status == Status::OK invalid_parts = template_file_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 end end return template_specs, content_hash end def self.scan_template_file(path, content_hash = {} ) template = File.new(path) 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 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_file_out_of_sync?(path, content_hash) self.template_out_of_sync?(File.new(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, content_hash) invalid = [] listener = MasterView::Analyzer::Listener.new(:content_hash => content_hash, :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 DirectoryForRebuildBackups to determine # path for where to store backup files. If DirectoryForRebuildBackups 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_render', NamespacePrefix+'import_render' end out << con end template = out.join if options[:write_to_file] backup = !options[:backup].nil? ? options[:backup] : DirectoryForRebuildBackups orig = File.readlines(self.path).join file_written = false unless template == orig self.backup_file if backup File.open(self.path, 'w') do |io| io << template end 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 DirectoryForRebuildBackups FileUtils.makedirs(DirectoryForRebuildBackups) unless File.exist?(DirectoryForRebuildBackups) #ensure path exists dst = File.join(DirectoryForRebuildBackups, File.basename(self.path)+'.'+Time.new.to_i.to_s) Log.debug { 'creating backup file '+dst } FileUtils.cp self.path, dst 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={} ) short_name = File.basename(path_to_copy_shell_from) controller_name = nil extension = nil short_name.scan( /^([^_.]+)_?([^.]*)(\.?\w*)$/ ) do |c, a, e| controller_name = c extension = e end extension = '.html' if extension.empty? short_name = File.basename(short_name, extension)+extension #ensure short_name has 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 src_file = File.join('app/views', MasterView::TemplateSrcRelativePath, short_name) dst_file = File.join('app/views', MasterView::TemplateSrcRelativePath, erb_values.controller_file_name+'_'+erb_values.action_name+extension) src = (tsrc = options[:template_source]) ? tsrc : File.readlines(src_file).join template_specs = {} content_hash = options[:content_hash] template_specs, content_hash = TemplateSpec.scan unless content_hash template_spec_to_copy = template_specs[src_file] || TemplateSpec.scan_template(src, src_file) result = MasterView::TemplateSpec.create_empty_shell( template_spec_to_copy, content_hash, content_to_insert) if options[:write_to_file] raise 'File '+dst_file+' already exists, operation aborted.' if File.exist? dst_file File.open(dst_file, 'w') do |io| io << result end return dst_file end result end end end