Use custom classifications like tags & categories #670

Closed
wants to merge 3 commits into
from
View
@@ -79,6 +79,19 @@ Feature: Site data
Then the _site directory should exist
And I should see "Yuengling" in "_site/index.html"
+ Scenario: Use the custom site.projects classification variable
+ Given I have a _posts directory
+ And I have an "index.html" page that contains "{% for post in site.projects.bartender %} {{ post.content }} {% endfor %}"
+ And I have a configuration file with "classifications" set to:
+ | value |
+ | projects |
+ And I have the following posts:
+ | title | date | project | content |
+ | Beer List | 2009-03-26 | bartender | Our most famous beverages |
+ When I run jekyll build
+ Then the _site directory should exist
+ And I should see "Our most famous beverages" in "_site/index.html"
+
Scenario: Order Posts by name when on the same date
Given I have a _posts directory
And I have an "index.html" page that contains "{% for post in site.posts %}{{ post.title }}:{{ post.previous.title}},{{ post.next.title}} {% endfor %}"
View
@@ -29,6 +29,7 @@ Gem::Specification.new do |s|
s.extra_rdoc_files = %w[README.markdown LICENSE]
s.add_runtime_dependency('liquid', "~> 2.5.5")
+ s.add_runtime_dependency('activesupport', "~> 3.2.13")
s.add_runtime_dependency('classifier', "~> 1.3")
s.add_runtime_dependency('listen', "~> 2.5")
s.add_runtime_dependency('kramdown', "~> 1.3")
@@ -55,7 +56,6 @@ Gem::Specification.new do |s|
s.add_development_dependency('simplecov-gem-adapter', "~> 1.0.1")
s.add_development_dependency('coveralls', "~> 0.7.0")
s.add_development_dependency('mime-types', "~> 1.5")
- s.add_development_dependency('activesupport', '~> 3.2.13')
s.add_development_dependency('jekyll_test_plugin')
s.add_development_dependency('jekyll_test_plugin_malicious')
s.add_development_dependency('rouge', '~> 1.3')
View
@@ -27,6 +27,7 @@ def require_all(path)
require 'kramdown'
require 'colorator'
require 'toml'
+require 'active_support/inflector'
# internal requires
require 'jekyll/version'
@@ -226,6 +226,8 @@ def write(dest)
def [](property)
if self.class::ATTRIBUTES_FOR_LIQUID.include?(property)
send(property)
+ elsif self.respond_to?(:classifications) && self.classifications.keys.include?(property)
+ self.classifications[property]
else
data[property]
end
View
@@ -12,11 +12,10 @@ class Post
dir
date
id
- categories
next
previous
- tags
path
+ classifications
]
# Attributes for Liquid templates
@@ -35,7 +34,7 @@ def self.valid?(name)
attr_accessor :site
attr_accessor :data, :extracted_excerpt, :content, :output, :ext
- attr_accessor :date, :slug, :tags, :categories
+ attr_accessor :date, :slug, :classifications
attr_reader :name
@@ -52,7 +51,8 @@ def initialize(site, source, dir, name)
@base = containing_dir(source, dir)
@name = name
- self.categories = dir.downcase.split('/').reject { |x| x.empty? }
+ self.classifications = {'categories' => dir.downcase.split('/').reject { |x| x.empty? }}
+
process(name)
read_yaml(@base, name)
@@ -64,8 +64,7 @@ def initialize(site, source, dir, name)
self.date = Time.parse(data["date"].to_s)
end
- populate_categories
- populate_tags
+ populate_classifications
end
def published?
@@ -76,15 +75,37 @@ def published?
end
end
- def populate_categories
- if categories.empty?
- self.categories = Utils.pluralized_array_from_hash(data, 'category', 'categories').map {|c| c.to_s.downcase}
- end
- categories.flatten!
+ # Delegate accessors to @classifications for backward compatibility
+ def categories=(c)
+ @classifications ||= {}
+ @classifications['categories'] = c
+ end
+
+ def categories
+ @classifications ||= {}
+ @classifications['categories']
+ end
+
+ def tags=(t)
+ @classifications ||= {}
+ @classifications['tags'] = t
+ end
+
+ def tags
+ @classifications ||= {}
+ @classifications['tags']
end
- def populate_tags
- self.tags = Utils.pluralized_array_from_hash(data, "tag", "tags").flatten
+ def populate_classifications
+ @site.classifications.keys.each do |classification|
+ value = Utils.pluralized_array_from_hash(data, ActiveSupport::Inflector.singularize(classification), classification)
+
+ if classification == 'categories'
+ value = self.categories.empty? ? value.map { |c| c.to_s.downcase } : self.categories
+ end
+
+ classifications[classification] = value.flatten
+ end
end
# Get the full path to the directory containing the post files
@@ -278,6 +299,13 @@ def destination(dest)
path
end
+ # Convert this post into a Hash for use in Liquid templates.
+ #
+ # Returns the representative Hash.
+ def to_liquid(attrs = nil)
+ Utils.deep_merge_hashes(super(attrs), @classifications)
+ end
+
# Returns the shorthand String identifier of this Post.
def inspect
"<Post: #{id}>"
View
@@ -111,6 +111,27 @@ def collection_names
end
end
+ # The list of classifications and their corresponding Jekyll::Post instances.
+ #
+ # Returns a Hash containing classifications name-to-posts pairs.
+ def classifications
+ Hash[classification_names.map { |classification| [classification, post_attr_hash(classification)]} ]
+ end
+
+ # The list of classification names.
+ #
+ # Returns an array of classification names from the configuration
+ # or ["categories", "tags"] if the `classifications` key is not set.
+ def classification_names
+ if !@classifications
+ # Include categories and tags for backward compatibility
+ @classifications = %w(categories tags)
+ # Sanitize classifications
+ @classifications |= (config['classifications'] || []).reject { |c| c !~ /\A[a-z][a-zA-Z_-]*\z/ }
+ end
+ @classifications
+ end
+
# Read Site data from disk and load it into internal data structures.
#
# Returns nothing.
@@ -295,7 +316,13 @@ 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 { |h, key| h[key] = [] }
- posts.each { |p| p.send(post_attr.to_sym).each { |t| hash[t] << p } }
+ posts.each do |p|
+ if p.classifications.keys.include?(post_attr) && !p.respond_to?(post_attr.to_sym)
+ p.classifications[post_attr].each { |t| hash[t] << p }
+ else
+ p.send(post_attr.to_sym).each { |t| hash[t] << p }
+ end
+ end
hash.values.each { |posts| posts.sort!.reverse! }
hash
end
@@ -319,35 +346,38 @@ def site_data
# The Hash payload containing site-wide data.
#
# Returns the Hash: { "site" => data } where data is a Hash with keys:
- # "time" - The Time as specified in the configuration or the
- # current time if none was specified.
- # "posts" - The Array of Posts, sorted chronologically by post date
- # and then title.
- # "pages" - The Array of all Pages.
- # "html_pages" - The Array of HTML Pages.
- # "categories" - The Hash of category values and Posts.
- # See Site#post_attr_hash for type info.
- # "tags" - The Hash of tag values and Posts.
- # See Site#post_attr_hash for type info.
+ # "time" - The Time as specified in the configuration or the
+ # current time if none was specified.
+ # "posts" - The Array of Posts, sorted chronologically by post date
+ # and then title.
+ # "pages" - The Array of all Pages.
+ # "html_pages" - The Array of HTML Pages.
+ # "classifications" - The Hash of classification names and Hash of classification values and Posts.
+ # "categories" - The Hash of category values and Posts.
+ # See Site#post_attr_hash for type info.
+ # "tags" - The Hash of tag values and Posts.
+ # See Site#post_attr_hash for type info.
+ # For each custom classification, the Hash will also contain a key:
+ # "#{classification}" - The Hash of custom classification values and Posts.
def site_payload
{
"jekyll" => {
"version" => Jekyll::VERSION,
"environment" => Jekyll.env
},
"site" => Utils.deep_merge_hashes(config,
- Utils.deep_merge_hashes(Hash[collections.map{|label, coll| [label, coll.docs]}], {
- "time" => time,
- "posts" => posts.sort { |a, b| b <=> a },
- "pages" => pages,
- "static_files" => static_files.sort { |a, b| a.relative_path <=> b.relative_path },
- "html_pages" => pages.reject { |page| !page.html? },
- "categories" => post_attr_hash('categories'),
- "tags" => post_attr_hash('tags'),
- "collections" => collections,
- "documents" => documents,
- "data" => site_data
- }))
+ Utils.deep_merge_hashes(Hash[collections.map{|label, coll| [label, coll.docs]}],
+ Utils.deep_merge_hashes(classifications, {
+ "time" => time,
+ "posts" => posts.sort { |a, b| b <=> a },
+ "pages" => pages,
+ "static_files" => static_files.sort { |a, b| a.relative_path <=> b.relative_path },
+ "html_pages" => pages.reject { |page| !page.html? },
+ "collections" => collections,
+ "classifications" => classifications,
+ "documents" => documents,
+ "data" => site_data
+ })))
}
end
@@ -0,0 +1,7 @@
+---
+title: Some Tags
+projects:
+- bartender
+---
+
+Our most famous beverages
@@ -14,7 +14,7 @@ class TestGeneratedSite < Test::Unit::TestCase
end
should "ensure post count is as expected" do
- assert_equal 42, @site.posts.size
+ assert_equal 43, @site.posts.size
end
should "insert site.posts into the index" do
View
@@ -15,7 +15,7 @@ def do_render(post)
context "A Post" do
setup do
clear_dest
- stub(Jekyll).configuration { Jekyll::Configuration::DEFAULTS }
+ stub(Jekyll).configuration { Jekyll::Configuration::DEFAULTS.merge({'classifications' => %w(projects)}) }
@site = Site.new(Jekyll.configuration)
end
@@ -531,6 +531,11 @@ def do_render(post)
assert_equal "Empty YAML.", post.content
end
+ should "recognize custom project classification in yaml" do
+ post = setup_post("2013-05-28-classifications.md")
+ assert post.classifications['projects'].include?('bartender')
+ end
+
context "rendering" do
setup do
clear_dest
View
@@ -45,7 +45,7 @@ class TestSite < Test::Unit::TestCase
context "creating sites" do
setup do
stub(Jekyll).configuration do
- Jekyll::Configuration::DEFAULTS.merge({'source' => source_dir, 'destination' => dest_dir})
+ Jekyll::Configuration::DEFAULTS.merge({'source' => source_dir, 'destination' => dest_dir, 'classifications' => %w(projects)})
end
@site = Site.new(Jekyll.configuration)
@num_invalid_posts = 2
@@ -77,6 +77,7 @@ def generate(site)
before_layouts = @site.layouts.length
before_categories = @site.categories.length
before_tags = @site.tags.length
+ before_projects = @site.classifications['projects'].length
before_pages = @site.pages.length
before_static_files = @site.static_files.length
before_time = @site.time
@@ -86,6 +87,7 @@ def generate(site)
assert_equal before_layouts, @site.layouts.length
assert_equal before_categories, @site.categories.length
assert_equal before_tags, @site.tags.length
+ assert_equal before_projects, @site.classifications['projects'].length
assert_equal before_pages, @site.pages.length
assert_equal before_static_files, @site.static_files.length
assert before_time <= @site.time
@@ -213,10 +215,13 @@ def generate(site)
posts = Dir[source_dir("**", "_posts", "**", "*")]
posts.delete_if { |post| File.directory?(post) && !Post.valid?(post) }
categories = %w(2013 bar baz category foo z_category publish_test win).sort
+ projects = %w(bartender)
assert_equal posts.size - @num_invalid_posts, @site.posts.size
assert_equal categories, @site.categories.keys.sort
+ assert_equal projects, @site.classifications['projects'].keys.sort
assert_equal 5, @site.categories['foo'].size
+ assert_equal 1, @site.classifications['projects']['bartender'].size
end
context 'error handling' do