module Jekyll class Post include Comparable include Convertible class << self attr_accessor :lsi end # Valid post name regex. MATCHER = /^(.+\/)*(\d+-\d+-\d+)-(.*)(\.[^.]+)$/ # Attributes for Liquid templates ATTRIBUTES_FOR_LIQUID = %w[ title url date id categories next previous tags content excerpt path ] # Post name validator. Post filenames must be like: # 2008-11-05-my-awesome-post.textile # # Returns true if valid, false if not. def self.valid?(name) name =~ MATCHER end attr_accessor :site attr_accessor :data, :extracted_excerpt, :content, :output, :ext attr_accessor :date, :slug, :published, :tags, :categories attr_reader :name # Initialize this Post instance. # # site - The Site. # base - The String path to the dir containing the post file. # name - The String filename of the post file. # # Returns the new Post. def initialize(site, source, dir, name) @site = site @dir = dir @base = self.containing_dir(source, dir) @name = name self.categories = dir.downcase.split('/').reject { |x| x.empty? } self.process(name) self.read_yaml(@base, name) if self.data.has_key?('date') self.date = Time.parse(self.data["date"].to_s) end self.published = self.published? self.populate_categories self.populate_tags end def published? if self.data.has_key?('published') && self.data['published'] == false false else true end end def populate_categories if self.categories.empty? self.categories = self.data.pluralized_array('category', 'categories').map {|c| c.to_s.downcase} end self.categories.flatten! end def populate_tags self.tags = self.data.pluralized_array("tag", "tags").flatten end # Get the full path to the directory containing the post files def containing_dir(source, dir) return File.join(source, dir, '_posts') end # Read the YAML frontmatter. # # base - The String path to the dir containing the file. # name - The String filename of the file. # # Returns nothing. def read_yaml(base, name) super(base, name) self.extracted_excerpt = self.extract_excerpt end # The post excerpt. This is either a custom excerpt # set in YAML front matter or the result of extract_excerpt. # # Returns excerpt string. def excerpt if self.data.has_key? 'excerpt' self.data['excerpt'] else self.extracted_excerpt end end # Public: the Post title, from the YAML Front-Matter or from the slug # # Returns the post title def title self.data["title"] || self.slug.split('-').select {|w| w.capitalize! || w }.join(' ') end # Public: the path to the post relative to the site source, # from the YAML Front-Matter or from a combination of # the directory it's in, "_posts", and the name of the # post file # # Returns the path to the file relative to the site source def path self.data['path'] || File.join(@dir, '_posts', @name).sub(/\A\//, '') end # Compares Post objects. First compares the Post date. If the dates are # equal, it compares the Post slugs. # # other - The other Post we are comparing to. # # 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 - 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 rescue ArgumentError raise FatalException.new("Post #{name} does not have a valid date.") end # Transform the contents and excerpt based on the content type. # # Returns nothing. def transform super self.extracted_excerpt = converter.convert(self.extracted_excerpt) 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 the String directory. def dir File.dirname(url) end # The full path and filename of the post. Defined in the YAML of the post # body (optional). # # Returns the String permalink. 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" when :ordinal "/:categories/:year/:y_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 the String URL. def url return @url if @url url = if permalink permalink else { "year" => date.strftime("%Y"), "month" => date.strftime("%m"), "day" => date.strftime("%d"), "title" => CGI.escape(slug), "i_day" => date.strftime("%d").to_i.to_s, "i_month" => date.strftime("%m").to_i.to_s, "categories" => categories.map { |c| URI.escape(c.to_s) }.join('/'), "short_month" => date.strftime("%b"), "y_day" => date.strftime("%j"), "output_ext" => self.output_ext }.inject(template) { |result, token| result.gsub(/:#{Regexp.escape token.first}/, token.last) }.gsub(/\/\//, "/") end # sanitize url @url = url.split('/').reject{ |part| part =~ /^\.+$/ }.join('/') @url += "/" if url =~ /\/$/ @url.gsub!(/\A([^\/])/, '/\1') @url end # The UID for this post (useful in feeds). # e.g. /2008/11/05/my-awesome-post # # Returns the String UID. def id File.join(self.dir, self.slug) end # Calculate related posts. # # Returns an Array of related Posts. def related_posts(posts) return [] unless posts.size > 1 if self.site.lsi build_index related = self.class.lsi.find_related(self.content, 11) related - [self] else (posts - [self])[0..9] end end def build_index self.class.lsi ||= begin puts "Starting the classifier..." lsi = Classifier::LSI.new(:auto_rebuild => false) $stdout.print(" Populating LSI... "); $stdout.flush self.site.posts.each { |x| $stdout.print("."); $stdout.flush; lsi.add_item(x) } $stdout.print("\n Rebuilding LSI index... ") lsi.build_index puts "" lsi end end # Add any necessary layouts to this post. # # layouts - A Hash of {"name" => "layout"}. # site_payload - 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 }.deep_merge(site_payload) do_layout(payload, layouts) end # Obtain destination path. # # dest - The String path to the destination dir. # # Returns destination file path String. def destination(dest) # The url needs to be unescaped in order to preserve the correct filename path = File.join(dest, CGI.unescape(self.url)) path = File.join(path, "index.html") if template[/\.html$/].nil? path end # Convert this post into a Hash for use in Liquid templates. # # Returns the representative Hash. def to_liquid further_data = Hash[ATTRIBUTES_FOR_LIQUID.map { |attribute| [attribute, send(attribute)] }] data.deep_merge(further_data) end # Returns the shorthand String identifier of this Post. 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 protected # Internal: Extract excerpt from the content # # By default excerpt is your first paragraph of a post: everything before # the first two new lines: # # --- # title: Example # --- # # First paragraph with [link][1]. # # Second paragraph. # # [1]: http://example.com/ # # This is fairly good option for Markdown and Textile files. But might cause # problems for HTML posts (which is quite unusual for Jekyll). If default # excerpt delimiter is not good for you, you might want to set your own via # configuration option `excerpt_separator`. For example, following is a good # alternative for HTML posts: # # # file: _config.yml # excerpt_separator: "" # # Notice that all markdown-style link references will be appended to the # excerpt. So the example post above will have this excerpt source: # # First paragraph with [link][1]. # # [1]: http://example.com/ # # Excerpts are rendered same time as content is rendered. # # Returns excerpt String def extract_excerpt separator = self.site.config['excerpt_separator'] head, _, tail = self.content.partition(separator) "" << head << "\n\n" << tail.scan(/^\[[^\]]+\]:.+$/).join("\n") end end end