module Jekyll class Post include Comparable include Convertible class << self attr_accessor :lsi end MATCHER = /^(.+\/)*(\d+-\d+-\d+)-(.*)(\.[^.]+)$/ # Post name validator. Post filenames must be like: # 2008-11-05-my-awesome-post.textile # # Returns def self.valid?(name) name =~ MATCHER end attr_accessor :site, :date, :slug, :ext, :published, :data, :content, :output, :tags attr_writer :categories def categories @categories ||= [] end # Initialize this Post instance. # +site+ is the Site # +base+ is the String path to the dir containing the post file # +name+ is the String filename of the post file # +categories+ is an Array of Strings for the categories for this post # # Returns def initialize(site, source, dir, name) @site = site @base = File.join(source, dir, '_posts') @name = name self.categories = dir.split('/').reject { |x| x.empty? } self.process(name) self.read_yaml(@base, name) if self.data.has_key?('published') && self.data['published'] == false self.published = false else self.published = true end if self.data.has_key?("tag") self.tags = [self.data["tag"]] elsif self.data.has_key?("tags") self.tags = self.data['tags'] else self.tags = [] end if self.categories.empty? if self.data.has_key?('category') self.categories << self.data['category'] elsif self.data.has_key?('categories') # Look for categories in the YAML-header, either specified as # an array or a string. if self.data['categories'].kind_of? String self.categories = self.data['categories'].split else self.categories = self.data['categories'] end end end end # Spaceship is based on Post#date, slug # # Returns -1, 0, 1 def <=>(other) cmp = self.date <=> other.date if 0 == cmp cmp = self.slug <=> other.slug end return cmp end # Extract information from the post filename # +name+ is the String filename of the post file # # Returns nothing def process(name) m, cats, date, slug, ext = *name.match(MATCHER) self.date = Time.parse(date) self.slug = slug self.ext = ext end # The generated directory into which the post will be placed # upon generation. This is derived from the permalink or, if # permalink is absent, set to the default date # e.g. "/2008/11/05/" if the permalink style is :date, otherwise nothing # # Returns def dir File.dirname(url) end # The full path and filename of the post. # Defined in the YAML of the post body # (Optional) # # Returns def permalink self.data && self.data['permalink'] end def template case self.site.permalink_style when :pretty "/:categories/:year/:month/:day/:title/" when :none "/:categories/:title.html" when :date "/:categories/:year/:month/:day/:title.html" else self.site.permalink_style.to_s end end # The generated relative url of this post # e.g. /2008/11/05/my-awesome-post.html # # Returns def url return permalink if permalink @url ||= { "year" => date.strftime("%Y"), "month" => date.strftime("%m"), "day" => date.strftime("%d"), "title" => CGI.escape(slug), "categories" => categories.sort.join('/') }.inject(template) { |result, token| result.gsub(/:#{token.first}/, token.last) }.gsub(/\/\//, "/") end # The UID for this post (useful in feeds) # e.g. /2008/11/05/my-awesome-post # # Returns def id File.join(self.dir, self.slug) end # Calculate related posts. # # Returns [] def related_posts(posts) return [] unless posts.size > 1 if self.site.lsi self.class.lsi ||= begin puts "Running the classifier... this could take a while." lsi = Classifier::LSI.new posts.each { |x| $stdout.print(".");$stdout.flush;lsi.add_item(x) } puts "" lsi end related = self.class.lsi.find_related(self.content, 11) related - [self] else (posts - [self])[0..9] end end # Add any necessary layouts to this post # +layouts+ is a Hash of {"name" => "layout"} # +site_payload+ is the site payload hash # # Returns nothing def render(layouts, site_payload) # construct payload payload = { "site" => { "related_posts" => related_posts(site_payload["site"]["posts"]) }, "page" => self.to_liquid } payload = payload.deep_merge(site_payload) do_layout(payload, layouts) end # Write the generated post file to the destination directory. # +dest+ is the String path to the destination dir # # Returns nothing def write(dest) FileUtils.mkdir_p(File.join(dest, dir)) # The url needs to be unescaped in order to preserve the correct filename path = File.join(dest, CGI.unescape(self.url)) # Write the post's files out into a directory named after the post self.write_postfiles(path) if self.site.config['postfiles'] if template[/\.html$/].nil? FileUtils.mkdir_p(path) path = File.join(path, "index.html") end File.open(path, 'w') do |f| f.write(self.output) end end # Write the files that the post depends on into its directory # # Returns nothing def write_postfiles(dest) glob = File.join("./_postfiles", File.basename(@name, File.extname(@name)), "*") postfiles = Dir[glob] if postfiles.any? FileUtils.mkdir_p(dest) postfiles.each{|pf| FileUtils.cp(pf, dest) } end end # Convert this post into a Hash for use in Liquid templates. # # Returns def to_liquid { "title" => self.data["title"] || self.slug.split('-').select {|w| w.capitalize! || w }.join(' '), "url" => self.url, "date" => self.date, "id" => self.id, "categories" => self.categories, "next" => self.next, "previous" => self.previous, "tags" => self.tags, "content" => self.content }.deep_merge(self.data) end def inspect "" end def next pos = self.site.posts.index(self) if pos && pos < self.site.posts.length-1 self.site.posts[pos+1] else nil end end def previous pos = self.site.posts.index(self) if pos && pos > 0 self.site.posts[pos-1] else nil end end end end