module Jekyll class Site attr_accessor :config, :layouts, :posts, :categories, :exclude, :source, :dest, :lsi, :pygments, :permalink_style, :tags # Initialize the site # +config+ is a Hash containing site configurations details # # Returns def initialize(config) self.config = config.clone self.source = config['source'] self.dest = config['destination'] self.lsi = config['lsi'] self.pygments = config['pygments'] self.permalink_style = config['permalink'].to_sym self.exclude = config['exclude'] || [] self.reset self.setup end def reset self.layouts = {} self.posts = [] self.categories = Hash.new { |hash, key| hash[key] = [] } self.tags = Hash.new { |hash, key| hash[key] = [] } end def setup # Check to see if LSI is enabled. require 'classifier' if self.lsi # Set the Markdown interpreter (and Maruku self.config, if necessary) case self.config['markdown'] when 'rdiscount' begin require 'rdiscount' def markdown(content) RDiscount.new(content).to_html end rescue LoadError puts 'You must have the rdiscount gem installed first' end when 'maruku' begin require 'maruku' def markdown(content) Maruku.new(content).to_html end if self.config['maruku']['use_divs'] require 'maruku/ext/div' puts 'Maruku: Using extended syntax for div elements.' end if self.config['maruku']['use_tex'] require 'maruku/ext/math' puts "Maruku: Using LaTeX extension. Images in `#{self.config['maruku']['png_dir']}`." # Switch off MathML output MaRuKu::Globals[:html_math_output_mathml] = false MaRuKu::Globals[:html_math_engine] = 'none' # Turn on math to PNG support with blahtex # Resulting PNGs stored in `images/latex` MaRuKu::Globals[:html_math_output_png] = true MaRuKu::Globals[:html_png_engine] = self.config['maruku']['png_engine'] MaRuKu::Globals[:html_png_dir] = self.config['maruku']['png_dir'] MaRuKu::Globals[:html_png_url] = self.config['maruku']['png_url'] end rescue LoadError puts "The maruku gem is required for markdown support!" end else raise "Invalid Markdown processor: '#{self.config['markdown']}' -- did you mean 'maruku' or 'rdiscount'?" end end def textile(content) RedCloth.new(content).to_html end # Do the actual work of processing the site and generating the # real deal. # # Returns nothing def process self.reset self.read_layouts self.transform_pages self.write_posts end # Read all the files in /_layouts into memory for later use. # # Returns nothing def read_layouts base = File.join(self.source, "_layouts") entries = [] Dir.chdir(base) { entries = filter_entries(Dir['*.*']) } entries.each do |f| name = f.split(".")[0..-2].join(".") self.layouts[name] = Layout.new(self, base, f) end rescue Errno::ENOENT => e # ignore missing layout dir end # Read all the files in /_posts and create a new Post object with each one. # # Returns nothing def read_posts(dir) base = File.join(self.source, dir, '_posts') entries = [] Dir.chdir(base) { entries = filter_entries(Dir['**/*']) } # first pass processes, but does not yet render post content entries.each do |f| if Post.valid?(f) post = Post.new(self, self.source, dir, f) if post.published self.posts << post post.categories.each { |c| self.categories[c] << post } post.tags.each { |c| self.tags[c] << post } end end end self.posts.sort! # second pass renders each post now that full site payload is available self.posts.each do |post| post.render(self.layouts, site_payload) end self.categories.values.map { |ps| ps.sort! { |a, b| b <=> a} } self.tags.values.map { |ps| ps.sort! { |a, b| b <=> a} } rescue Errno::ENOENT => e # ignore missing layout dir end # Write each post to //// # # Returns nothing def write_posts self.posts.each do |post| post.write(self.dest) end end # Copy all regular files from to / ignoring # any files/directories that are hidden or backup files (start # with "." or "#" or end with "~") or contain site content (start with "_") # unless they are "_posts" directories or web server files such as # '.htaccess' # The +dir+ String is a relative path used to call this method # recursively as it descends through directories # # Returns nothing def transform_pages(dir = '') base = File.join(self.source, dir) entries = filter_entries(Dir.entries(base)) directories = entries.select { |e| File.directory?(File.join(base, e)) } files = entries.reject { |e| File.directory?(File.join(base, e)) || File.symlink?(File.join(base, e)) } # we need to make sure to process _posts *first* otherwise they # might not be available yet to other templates as {{ site.posts }} if directories.include?('_posts') directories.delete('_posts') read_posts(dir) end [directories, files].each do |entries| entries.each do |f| if File.directory?(File.join(base, f)) next if self.dest.sub(/\/$/, '') == File.join(base, f) transform_pages(File.join(dir, f)) elsif Pager.pagination_enabled?(self.config, f) paginate_posts(f, dir) else first3 = File.open(File.join(self.source, dir, f)) { |fd| fd.read(3) } if first3 == "---" # file appears to have a YAML header so process it as a page page = Page.new(self, self.source, dir, f) page.render(self.layouts, site_payload) page.write(self.dest) else # otherwise copy the file without transforming it FileUtils.mkdir_p(File.join(self.dest, dir)) FileUtils.cp(File.join(self.source, dir, f), File.join(self.dest, dir, f)) end end end end end # Constructs a hash map of Posts indexed by the specified Post attribute # # Returns {post_attr => []} def post_attr_hash(post_attr) # Build a hash map based on the specified post attribute ( post attr => array of posts ) # then sort each array in reverse order hash = Hash.new { |hash, key| hash[key] = Array.new } self.posts.each { |p| p.send(post_attr.to_sym).each { |t| hash[t] << p } } hash.values.map { |sortme| sortme.sort! { |a, b| b <=> a} } return hash end # The Hash payload containing site-wide data # # Returns {"site" => {"time" =>