require 'digest' module Plate class Page include Callbacks attr_accessor :body, :content, :file, :meta, :site, :layout def initialize(site, file = nil, load_on_initialize = true) self.site = site self.file = file self.meta = {} self.content = "" load! if load_on_initialize and file? end # Setup some shortcut getters for meta attributes %w( title description tags category ).each do |meta_attribute| class_eval <<-META def #{meta_attribute} # def title self.meta[:#{meta_attribute}] # self.meta[:title] end # end def #{meta_attribute}=(value) # def title=(value) self.meta[:#{meta_attribute}] = value # self.meta[:title] = value end # end META end # The name of the file, without any path data def basename File.basename(self.file) end alias_method :name, :basename # The directory this page is located in, relative to the site root. def directory return @directory if @directory base = Pathname.new(File.join(self.site.source, 'content')) current = Pathname.new(self.file) dirs = current.relative_path_from(base).to_s.split('/') if dirs.size > 1 dirs.pop @directory = "/#{dirs.join('/')}" else @directory = "/" end end def engines @engines ||= self.extensions.reverse.collect { |e| self.site.registered_page_engines[e.gsub(/\./, '').to_sym] }.reject { |e| !e } end def extensions @extensions ||= self.basename.scan(/\.[^.]+/) end # Does the file exist or not. def file? return false if self.file.nil? File.exists?(self.file) end # Returns just the file name, no extension. def file_name File.basename(self.file, '.*') end # The full file path of where this file will be written to. (Relative to site root) def file_path return @file_path if @file_path result = nil if self.meta.has_key?(:path) result = self.meta[:path] result = "/#{result}" unless result.start_with?('/') else result = directory result << '/' unless result =~ /\/$/ result << slug unless slug == 'index' # Remove any double slashes result.gsub!(/\/\//, '/') # Remove file extensions, and cleanup URL result = result.split('/').reject{ |segment| segment =~ /^\.+$/ }.join('/') # Add a trailing slash result << '/' unless result =~ /\/$/ # Tack on index.html for the folder result << 'index.html' end @file_path = result end def format_extension format = self.extensions.reverse.detect() { |e| !self.site.page_engine_extensions.include?(e) } format = ".html" if format.nil? format end # A unique ID for this page. def id @id ||= Digest::MD5.hexdigest(relative_file) end def inspect "#<#{self.class}:0x#{object_id.to_s(16)} name=#{name.to_s.inspect}>" end # Utility method to sanitize keywords output. Keywords are returned as an array. def keywords @keywords ||= (Array === self.meta[:keywords] ? self.meta[:keywords] : self.meta[:keywords].to_s.strip.split(',').collect(&:strip)) end # The layout to use when rendering this page. Returns nil if no default layout is available, # or the layout has specifically been turned off within the config. def layout return @layout if defined?(@layout) self.layout = self.meta[:layout] @layout end # Manually set the layout def layout=(value) if value == false @layout = nil elsif value @layout = self.site.find_layout(value) else @layout = self.site.default_layout end end # Has this page been loaded from file? def loaded? !!@loaded end # Read the file data for this page def load! return if @loaded raise FileNotFound unless file? read_file! read_metadata! @loaded = true end def path return '/' if self.file_path == '/index.html' @path ||= self.file_path.sub(/(.*?)\/index\.html$/i, '\1') end # The file's source path, relative to site root. def relative_file @relative_file ||= self.site.relative_path(self.file) end def reload! @loaded = false @content = nil @meta = {} @keywords = nil @rendered_content = nil @body = nil load! end # Returns the rendered body of this page, without the layout. # # The callbacks `before_render` and `after_render` are called here. To perform # custom actions before or after a page file is written to disk, use these callback # methods. # # See {Plate::Callbacks} for more information on setting up callbacks. def rendered_body return @body if @body result = "" around_callback :render do result = self.content view = View.new(self.site, self) self.engines.each do |engine| template = engine.new(self.file) { result } result = template.render(view, {}) end view = nil @body = result end @body end def rendered_content @rendered_content ||= self.apply_layout_to(rendered_body) end # Name of the file to be saved. Just takes the current file name and removes any extensions. def slug self.basename.to_s.downcase.split('.')[0].dasherize.parameterize end # The title from this page's meta data, turned into a parameter for use in a url. def title_for_url self.title.to_s.dasherize.parameterize end # Returns this page's content def to_s self.inspect end # The full URL of this page. Depends on the site's URL attribute and a config option of `:base_url` def url @url ||= "#{site.url}#{path}" end # Write the compiled page file to the destination. The content is written to disk # using the path designated in `file_path` and the content from `rendered_content`. # # The callbacks `before_write` and `after_write` are included here. To perform # custom actions before or after a page file is written to disk, use these callback # methods. # # See {Plate::Callbacks} for more information on setting up callbacks. def write! path = File.join(site.build_destination, file_path) FileUtils.mkdir_p(File.dirname(path)) around_callback :write do File.open(path, 'w') do |f| f.write(self.rendered_content) end end path end # Is this page equal to another page being sent? def ==(other) other = other.relative_file if other.respond_to?(:relative_file) self.id == other or self.relative_file == other end # Compare two posts, by date. def <=>(other) self.path <=> other.path end protected def apply_layout_to(content) return content unless Layout === self.layout self.layout.render(content, self) end # Reading page details # ##################################################################### # Read the file and store it in @content def read_file! self.content = file? ? File.read(self.file) : nil end # Reads all content from a page's meta data # # Meta data is stored in YAML format within the head of a page after the -- declaration like so: # # --- # title: "Hello" # description: "This is some meta data" # keywords: [ blah, blah ] # tags: [ blah, blah ] # category: Test # layout: default # # # Start of actual content def read_metadata! return unless self.content begin if matches = /^(---\n)(.*?)^\s*?$/m.match(self.content) if matches.size == 3 self.content = matches.post_match.strip self.meta = YAML.load(matches[2]) self.meta.symbolize_keys! end end rescue Exception => e self.meta = {} self.site.log(" ** Problem reading YAML for file #{relative_file} (#{e.message}). Meta data skipped") end end end end