require 'ostruct' require 'pathname' require 'stringio' require File.join( File.dirname(__FILE__), 'pathname_extensions' ) require File.join( File.dirname(__FILE__), 'mtime_tracking_hash' ) require File.join( File.dirname(__FILE__), '../facets/core/string/starts_with' ) require File.join( File.dirname(__FILE__), 'filter_helpers' ) module MasterView # IOManager which retrieves the proper MasterViewIOTree object for the type of object being requested # set and access MasterViewIOTree by using accessors # IOMgr.template = FileMIOTree() # ftree = IOMgr.template # IOMgr.erb = FileMIOTree() # etree = IOMgr.erb # Once the mioTree is obtained it may be used for obtaining mio objects for reading and writing class MIOTrees < OpenStruct end # MIOTreeInterface # path(path) # find(options, &block) options[:path] = 'foo' limits find to dir foo # cleanup_path_get_relative_pathname(path) # MIOInterface # pathname() # full_pathname() # for FileMIO returns full_pathname otherwise for all others identical to pathname # read(options={}) options[:disable_cache]=true bypasses cache filter, options[:disable_logging] = true turns off log of this event # write(content=nil, options={}, &block) options[:disable_logging]=true turns off log of this event options[:force] = true forces the write even if identical # exist?(path) # identical?(content) # mtime() # mixin which encapsulates the apply filter to new MIO objects and # the default filter block module MasterViewIOTreeMixin DefaultExtensionInstances = {} # apply filters to object, use block if provided otherwise use # default_mio_filter_block which is customized by options hash # options[:escape_erb] = true enables erb escaping # options[:tidy] = true enables tidy processing to cleanup bad xhtml # options[:caching] = true enables caching so that reads are cached # options[:logging] = true enables logging of reads and writes # options[:default_generate] = true enables creation of default generate directive if none found def apply_filters(mio, options, block) if block block.call(mio) else default_mio_filter_block(options).call(mio) end mio end def default_mio_filter_block(options) lambda do |mio| mio.extend EscapeErbMIOFilter if options[:escape_erb] mio.extend TidyMIOFilter if options[:tidy] mio.extend DefaultGenerateMIOFilter if options[:default_generate] mio.extend CachingMIOFilter if options[:caching] mio.extend ReadWriteLoggingMIOFilter if options[:logging] end end def default_extension DefaultExtensionInstances[object_id] end def default_extension=(default_extension) DefaultExtensionInstances[object_id] = default_extension end # clean up ../.. check if path starts with root path and if so get relative, otherwise return path, root_path is only # used in FileMIO, so all others just simply clean the path. FileMIO will override this with its own implementation. def cleanup_path_get_relative_pathname(path) pathname = Pathname.for_path(path) pathname = Pathname.for_path(pathname.to_s+self.default_extension) if pathname.extname.empty? && self.default_extension && !self.default_extension.empty? pathname end end class FileMIOTree < Pathname include MasterViewIOTreeMixin def initialize(root_path=Pathname.getwd, default_extension=nil, options = {}, &block) @options = options self.default_extension = default_extension super(Pathname.for_path(root_path).cleanpath) @new_mio_block = block end def path(path) raise InvalidPathError.new('Invalid path specified ('+path.to_s+'). Path is limited to directories under root_path ('+self.to_s+')') unless (self+path).expand_path.to_s.starts_with?(self.expand_path.to_s) #ensure sandbox mio = FileMIO.new(path, self + path) apply_filters(mio, @options, @new_mio_block) end # returns mio objects, if block_given then it yields passing the mio object to the block def find(options = {}, &block) path = options[:path] || '.' found = [] working_path = Pathname.safe_concat(self, path) working_path.find { |p| found << p unless p.directory? } found = found.select { |f| f.basename.fnmatch?(options[:pattern]) } if options[:pattern] found = found.collect{ |f| self.path(f.relative_path_from(self)) } # create mio objects found = found.sort { |a,b| a.pathname.to_s <=> b.pathname.to_s } found.each { |mio| yield mio } if block_given? found end # clean up ../.. check if path starts with root path and if so get relative, otherwise return path def cleanup_path_get_relative_pathname(path) pathname = Pathname.for_path(path) pathname = pathname.relative_path_from(self) if pathname.to_s.starts_with?(self.to_s) pathname = Pathname.for_path(pathname.to_s+self.default_extension) if pathname.extname.empty? && self.default_extension && !self.default_extension.empty? pathname end end # MIO implemented as simple hash of path to string content # This MIOTree does not keep track of mtimes and thus will always return new mtimes class StringHashMIOTree include MasterViewIOTreeMixin attr_reader :string_hash def initialize(string_hash = {}, default_extension=nil, options={}, &block) @string_hash = string_hash @options = options self.default_extension = default_extension @new_mio_block = block end def string_hash=(string_hash) @string_hash = string_hash end def path(path) clean_path = Pathname.for_path(path).cleanpath mio = StringMIO.new(clean_path, @string_hash) mio.pathname = clean_path apply_filters(mio, @options, @new_mio_block) end def find(options = {}) path = options[:path] || '' path = '' if path == '.' found = [] @string_hash.each { |p,v| found << p if p.starts_with?(path) } found = found.select { |p| Pathname.for_path(p).basename.fnmatch?(options[:pattern]) } if options[:pattern] found.sort! found = found.collect{ |p| self.path(p) } # create mio objects found.each { |mio| yield mio } if block_given? found end end class ActiveRecordMIOTree #todo include MasterViewIOTreeMixin def initialize self.default_extension = default_extension end def path(path) end def find(options, &block) end end # MIOTree which works like StringHashMIO but uses MTimeTrackingHash to track mod times class MTimeStringHashMIOTree < StringHashMIOTree def initialize(default_extension=nil, options={}, &block) super(MTimeTrackingHash.new, default_extension, options) end def path(path) clean_path = Pathname.for_path(path).cleanpath mio = MTimeStringMIO.new(clean_path, @string_hash) mio.pathname = clean_path apply_filters(mio, @options, @new_mio_block) end def string_hash=(mtime_tracking_hash) raise "mtime_tracking_hash passed in does not respond_to mtime method call" unless mtime_tracking_hash.respond_to?(:mtime) @string_hash = mtime_tracking_hash end end # mio which allows skipping the intermediate erb file generation in rails. # Current implementation inherits its functionality from MTimeStringHashMIOTree class RailsErbCacheMIOTree < MTimeStringHashMIOTree end class MIOBase attr_accessor :pathname def initialize(path) @pathname = Pathname.for_path(path) end # for mio objects other than FileMIO, full_pathname is same as pathname def full_pathname @pathname end end module MasterViewIOIdenticalMixin def identical?(compare_content) self.exist? && compare_content == self.read(:disable_logging => true ) # eliminate the noise, simply checking if different end end class FileMIO < Pathname include MasterViewIOIdenticalMixin attr_reader :pathname def initialize(path, full_path) super(full_path) @pathname = Pathname.for_path(path) end def full_pathname self end def read(options = {}) super() end def write(content = nil, options={}, &block) written = false if block_given? sio = ::StringIO.new yield sio content = sio.string end if options[:force] || !identical?(content) self.dirname.mkpath unless self.dirname.exist? self.open('w') do |io| io << content end written = true end written end end class StringMIO < MIOBase include MasterViewIOIdenticalMixin attr_accessor :exist attr_reader :mtime def initialize(path, string_hash, mtime = Time.new) super(path) @string_hash = string_hash @mtime = mtime end def read(options = {}) @string_hash[self.pathname.to_s] end def write(content = nil, options = {}, &block) written = false if block_given? sio = ::StringIO.new yield sio content = sio.string end if options[:force] || !self.identical?(content) @string_hash[self.pathname.to_s] = content @mtime = Time.now written = true end written end def exist? @string_hash.has_key?(self.pathname.to_s) end end # used by MTimeStringHashMIOTree to create MIO objects that check # their mod time using the MTimeTrackingHash class MTimeStringMIO < StringMIO def initialize(path, mtime_tracking_hash) super(path, mtime_tracking_hash, nil) end def mtime @string_hash.mtime(self.pathname.to_s) end end class ActiveRecordMIO < MIOBase include MasterViewIOIdenticalMixin def exist?; end def mtime; end end module CachingMIOFilter #todo def read(options={}) if options[:disable_cache] super else #caching on super #todo obtain from cache end end end module TidyMIOFilter def read(options={}) content = super ::MasterView::TidyHelper.tidy(content) end end module EscapeErbMIOFilter def read(options={}) content = super ::MasterView::EscapeErbHelper.escape_erb(content) end end # checks to see that mv:generate or mv:gen_partial has been used somewhere in file # and if not then it puts in a default mv:generate directive based on the following # logic. If a body tag exists then add the mv:generate directive to the body tag # and also add a mv:omit_tag="" directive. This is based on the assumption that we are # body will be defined in a layout somewhere so we are only interested in the # internal content of it (excluding the body tag itself). # If a body tag does not exist, then simply add the mv:generate tag to the root element. # # Examples: #
Hello world # becomes # Hello world # #