module Plate require 'tilt' # This class contains everything you'll want to know about a site. It contains all data # about the site, including blog posts, content pages, static files, assets and anything else. class Site include Callbacks # [Array] An array of all dynamic assets for this site attr_accessor :assets # [String] The directory where the built site will be created for this site. attr_accessor :build_destination # [String] The file path of the cache directory for this site. attr_accessor :cache_location # [String] The destination directory file path for this site attr_accessor :destination # [Array] Any posts marked as drafts for this site attr_accessor :drafts # [Array] An array of all layout classes used for this site attr_accessor :layouts # [Object] The logger instance for this site. attr_accessor :logger # [Hash] A hash of all default meta data options for this site. attr_accessor :metadata # [Hash] A hash of configuration options for this site attr_accessor :options # [Array] An array of all non-blog post pages for this site attr_accessor :pages # [Array] An array of all view partials available in this site. attr_accessor :partials # [PostCollection] All blog posts for this site attr_accessor :posts # [String] The source directory file path for building this site attr_accessor :source def initialize(source, destination, options = {}) # Setup source and destination for the site files self.source = source self.destination = destination # By default, the build goes into the destination folder. # Override this to output to a different folder by default self.build_destination = destination # Sanitize options self.options = Hash === options ? options.clone : {} self.options.symbolize_keys! clear end %w( assets drafts layouts pages posts ).each do |group| class_eval "def #{group}; load!; @#{group}; end" end def all_files @all_files ||= self.assets + self.layouts + self.pages + self.posts.to_a + self.drafts end # All extensions that are registered, as strings. def asset_engine_extensions @asset_engine_extensions ||= self.registered_asset_engines.keys.collect { |e| ".#{e}" } end # Alphabetical list of all blog post categories used. def categories @categories ||= self.posts.collect(&:category).uniq.sort end # Clear out all data related to this site. Prepare for a reload, or first time load. def clear @loaded = false @tags_counts = nil @default_layout = nil self.assets = [] self.layouts = [] self.pages = [] self.posts = PostCollection.new self.partials = [] self.drafts = [] @metadata = {} end # The default blog post category def default_category options[:default_category] || 'Posts' end # The default layout for all pages that do not specifically name their own def default_layout return nil if self.layouts.size == 0 return @default_layout if @default_layout layout ||= self.layouts.reject { |l| !l.default? } layout = self.layouts if layout.size == 0 if Array === layout and layout.size > 0 layout = layout[0] end @default_layout = layout end # Find a page, asset or layout by source relative file path def find(search_path) self.all_files.find { |file| file == search_path } end # Find all registered files by the given file extension def find_by_extension(extension) extension = extension.to_s.downcase.gsub(/^\./, '') self.all_files.select { |file| file.extension.to_s.downcase.gsub(/^\./, '') == extension } end # Find all pages and posts with this layout def find_by_layout(layout_name) result = [] result += self.pages.find_all { |page| page.layout == layout_name } result += self.posts.find_all { |post| post.layout == layout_name } result end # Find a specific layout by its file name. Any extensions are removed. def find_layout(layout_name) search_name = layout_name.to_s.downcase.strip.split('.')[0] matches = self.layouts.reject { |l| l.name != search_name } matches.empty? ? self.default_layout : matches[0] end def inspect "#<#{self.class}:0x#{object_id.to_s(16)} source=#{source.to_s.inspect}>" end # Load all data from the various source directories. def load! return if @loaded log("Loading site from source [#{source}]") run_callback :before_load self.load_pages! self.load_layouts! self.load_posts! run_callback :after_load @loaded = true end # Returns true if the site has been loaded from the source directories. def loaded? !!@loaded end # Write to the log if enable_logging is enabled def log(message, style = :indent) logger.send(:log, message, style) if logger and logger.respond_to?(:log) end # Access to read all meta data for this site. Meta data can be set on the # site instance by passing in the `metadata` hash, or it can be pulled # from the config file under the heading of `meta`. # # All keys within the hash can be accessed directly by calling the key # as a method name: # # @example # # Set the meta data hash # @site.meta = { :title => 'My Site Title' } # # # Does the title key exist? # @site.meta.title? # => true # # # Return the title value # @site.meta.title # => 'My Site 'Title' def meta @meta ||= HashProxy.new(self.metadata) end # Set the meta data hash object for this site. def meta=(hash) # Reset the meta hash proxy @meta = nil self.metadata = hash end def page_engine_extensions @page_engine_extensions ||= self.registered_page_engines.keys.collect { |e| ".#{e}" } end def relative_path(file_or_directory) file_or_directory.to_s.gsub(/^#{Regexp.quote(source)}(.*)$/, '\1') end def reload! clear load! end # Returns the asset engines that are available for use. def registered_asset_engines Plate.asset_engines end # Returns the engines available for use in page and layout formatting. def registered_page_engines Plate.template_engines end # All tags used on this site def tags @tags ||= self.posts.tag_list end def to_url(str) result = str.to_s.strip.downcase result = result.gsub(/[^-a-z0-9~\s\.:;+=_]/, '') result = result.gsub(/[\.:;=+-]+/, '') result = result.gsub(/[\s]/, '-') result end alias_method :sanitize_slug, :to_url # The base URL for this site. The url can be set using the config option named `:base_url`. # # The base URL will not have any trailing slashes. def url return '' unless self.options[:base_url] @url ||= self.options[:base_url].to_s.gsub(/(.*?)\/?$/, '\1') end protected # Load all layouts from layouts/ def load_layouts!(verbose = true) @layouts = [] Dir.glob(File.join(source, "layouts/**/*")).each do |file| # If this 'file' is a directory, just skip it. We only care about files. unless File.directory?(file) @layouts << Layout.new(self, file) end end log("#{@layouts.size} layouts loaded") if verbose @layouts end def load_pages!(verbose = true) @assets = [] @pages = [] @partials = [] # Load all pages, static pages and assets from content/ Dir.glob(File.join(source, "content/**/*")).each do |file| # Detect if this is a partial or not partial = File.basename(file).start_with?('_') # If this 'file' is a directory, just skip it. We only care about files. unless File.directory?(file) # Check for assets that need to be compiled. Currently only looks to see if the file # ends in .coffee, .scss or .sass. # # Assets that start with _ are assumed to be partials and are not loaded. if asset_engine_extensions.include?(File.extname(file)) unless partial @assets << Asset.new(self, file) end else if partial @partials << Partial.new(self, file) else # Check for YAML meta header. If it starts with ---, then process it as a page intro = File.open(file) { |f| f.read(3) } # If file contents start with ---, then it is something we should process as a page. if intro == "---" @pages << Page.new(self, file) else @pages << StaticPage.new(self, file) end end end end end if verbose log("#{@assets.size} assets loaded") log("#{@pages.size} pages and other files loaded") log("#{@partials.size} partials loaded") end @pages end # Load blog posts from posts/ def load_posts!(verbose = true) @drafts = [] @posts = PostCollection.new # Load up all published posts from posts/ directory Dir.glob(File.join(source, "posts/**/*")).each do |file| # If this 'file' is a directory, just skip it. We only care about files. unless File.directory?(file) # Check for YAML meta header. If it starts with ---, then process it as a page intro = File.open(file) { |f| f.read(3) } # If file contents start with ---, then it is something we should process as a page. if intro == "---" @posts.add(Post.new(self, file)) end end end # Load up any drafts, and publish as needed Dir.glob(File.join(source, "drafts/**/*")).each do |file| # If this 'file' is a directory, just skip it. We only care about files. unless File.directory?(file) # Check for YAML meta header. If it starts with ---, then process it as a page intro = File.open(file) { |f| f.read(3) } # If file contents start with ---, then it is something we should process as a page. if intro == "---" draft = Draft.new(self, file) if draft.publish? new_file = draft.publish! @posts.add(Post.new(self, new_file)) else @drafts << draft end end end end @posts.sort! log("#{@posts.size} posts loaded") if verbose @posts end end end