require 'yaml' module Webby::Resources # The MetaFile class is used to read meta-data and content from files. The # meta-data is in a YAML block located at the top of the file. The content # is the remainder of the file (everything after the YAML block). # # The meta-data data must be found between two YAML block separators "---", # each on their own line. # # Example: # # --- # layout: blog # filter: markdown # tags: # - ruby # - web development # --- # This is a blog entry formatted using MarkDown and tagged as "ruby" and # "web development". The layout being used is the "blog" format. # class MetaFile class Error < StandardError; end META_SEP = %r/\A---\s*(?:\r\n|\n)?\z/ # :nodoc: ERR_MSG = "corrupt meta-data (perhaps there is an errant YAML marker '---' in the file)" # :nodoc: # call-seq: # MetaFile.read( filename ) => string # # Opens the file identified by _filename_ and returns the contents of the # file as a string. Any meta-data at the top of the file is skipped and # is not included in the returned string. If the file contains no # meta-data, then this method behaves the same as File#read. # def self.read( name ) ::File.open(name, 'r') {|fd| MetaFile.new(fd).read} end # call-seq: # MetaFile.meta_data( filename ) => object or nil # # Opens the file identified by _filename_ and returns the meta-data # located at the top of the file. If the file contains no meta-data, then # +nil+ is returned. # def self.meta_data( name ) ::File.open(name, 'r') {|fd| MetaFile.new(fd).meta_data} end # call-seq: # MetaFile.meta_data?( filename ) => true or false # # Opens the file identified by _filename_ and returns true if there is a # meta-data block at the top of the file, and returns false if there is # not a meta-data block at the top of the file. # def self.meta_data?( name ) ::File.open(name, 'r') {|fd| MetaFile.new(fd).meta_data?} end # Creates a new MetaFile parser that will read from the given _io_ stream. # def initialize( io ) raise ArgumentError, "expecting an IO stream" unless io.respond_to? :gets @io = io @meta_count = 0 end # Returns the entire contents of the IO stream exluding any meta-data # found at the beginning of the stream. # def read count = meta_end @io.seek 0 count.times {@io.gets} unless count.nil? @io.read end # Reads in each meta-data section and yields it to the given block. The # first meta-data section is yielded "as is", but subsequent meta-data # sections are merged with this first section and then yielded. This # allows the user to define common items in the first meta-data section # and only include items that are different in the subsequent sections. # # Example: # # --- # title: First Title # author: me # directory: foo/bar/baz # --- # title: Second Title # author: you # --- # title: Third Title # author: them # --- # # and parsing the meta-data above yields ... # # meta_file.each do |hash| # pp hash # end # # the following output # # { 'title' => 'First Title', # 'author' => 'me', # 'directory' => 'foo/bar/baz' } # # { 'title' => 'Second Title', # 'author' => 'you', # 'directory' => 'foo/bar/baz' } # # { 'title' => 'Third Title', # 'author' => 'them', # 'directory' => 'foo/bar/baz' } # # Even though the "directory" item only appears in the first meta-data # block, it is copied to all the subsequent blocks. # def each return unless meta_data? first, count = nil, 0 @io.seek 0 buffer = @io.gets while count < @meta_count while (line = @io.gets) !~ META_SEP buffer << line end h = YAML.load(buffer) raise Error, ERR_MSG unless h.instance_of?(Hash) if first then h = first.merge(h) else first = h.dup end buffer = line count += 1 yield h end rescue ArgumentError => err msg = ERR_MSG.dup << "\n\t-- " << err.message raise Error, msg end # Returns the meta-data defined at the top of the file. Returns +nil+ if # no meta-data is defined. The meta-data is returned as Ruby objects # # Meta-data is stored in YAML format between two YAML separators "---" on # their own lines. # def meta_data return if meta_end.nil? @io.seek 0 return YAML.load(@io) end # Returns true if the IO stream contains meta-data. Returns false if the # IO stream does not contain meta-data. # def meta_data? meta_end.nil? ? false : true end # Returns the number of meta-data blocks at the top of the file. # def meta_count meta_end @meta_count end # Returns the position in the IO stream where the meta-data ends and the # regular data begins. If there is no meta-data in the stream, returns +nil+. # def meta_end return @meta_end if defined? @meta_end @meta_end = nil @io.seek 0 line = @io.read(4) return unless META_SEP =~ line @io.seek 0 @io.gets pos, count = nil, 1 while line = @io.gets count += 1 if META_SEP =~ line pos = count @meta_count += 1 end end return if pos.nil? @meta_end = pos end end # class MetaFile end # module Webby::Resources # EOF