diff --git a/features/collections.feature b/features/collections.feature
new file mode 100644
index 000000000..451c84d87
--- /dev/null
+++ b/features/collections.feature
@@ -0,0 +1,38 @@
+Feature: Collections
+ As a hacker who likes to structure content
+ I want to be able to create collections of similar information
+ And render them
+
+ Scenario: Unrendered collection
+ Given I have an "index.html" page that contains "Collections: {{ site.methods }}"
+ And I have fixture collections
+ And I have a configuration file with "collections" set to "['methods']"
+ When I run jekyll
+ Then the _site directory should exist
+ And I should see "Collections: Use `{{ page.title }}` to build a full configuration for use w/Jekyll.\n\nWhatever: {{ page.whatever }}\n`{{ page.title }}` is used to make sure your path is in your source.\nRun your generators! {{ page.layout }}\nCreate dat site.\nRun your generators! {{ page.layout }}" in "_site/index.html"
+
+ Scenario: Rendered collection
+ Given I have an "index.html" page that contains "Collections: {{ site.collections }}"
+ And I have fixture collections
+ And I have a configuration file with:
+ | key | value |
+ | collections | ['methods'] |
+ | render | ['methods'] |
+ When I run jekyll
+ Then the _site directory should exist
+ And I should see "Collections: methods" in "_site/index.html"
+ And I should see "
Whatever: foo.bar
" in "_site/methods/configuration.html"
+
+ Scenario: Rendered document in a layout
+ Given I have an "index.html" page that contains "Collections: {{ site.collections }}"
+ And I have a default layout that contains "
Tom Preston-Werner
{{content}}"
+ And I have fixture collections
+ And I have a configuration file with:
+ | key | value |
+ | collections | ['methods'] |
+ | render | ['methods'] |
+ When I run jekyll
+ Then the _site directory should exist
+ And I should see "Collections: methods" in "_site/index.html"
+ And I should see "
Run your generators! default
" in "_site/methods/site/generate.html"
+ And I should see "
Tom Preston-Werner
" in "_site/methods/site/generate.html"
\ No newline at end of file
diff --git a/features/step_definitions/jekyll_steps.rb b/features/step_definitions/jekyll_steps.rb
index 93de5c108..45210630e 100644
--- a/features/step_definitions/jekyll_steps.rb
+++ b/features/step_definitions/jekyll_steps.rb
@@ -16,15 +16,14 @@ def file_content_from_hash(input_hash)
EOF
end
-
Before do
FileUtils.mkdir_p(TEST_DIR) unless File.exist?(TEST_DIR)
Dir.chdir(TEST_DIR)
end
After do
- FileUtils.rm_rf(TEST_DIR) if File.exist?(TEST_DIR)
- FileUtils.rm(JEKYLL_COMMAND_OUTPUT_FILE)
+ FileUtils.rm_rf(TEST_DIR) if File.exists?(TEST_DIR)
+ FileUtils.rm(JEKYLL_COMMAND_OUTPUT_FILE) if File.exists?(JEKYLL_COMMAND_OUTPUT_FILE)
end
World(Test::Unit::Assertions)
@@ -130,6 +129,16 @@ Given /^I have a configuration file with "([^\"]*)" set to:$/ do |key, table|
end
end
+Given /^I have fixture collections$/ do
+ FileUtils.cp_r File.join(JEKYLL_SOURCE_DIR, "test", "source", "_methods"), source_dir
+end
+
+##################
+#
+# Changing stuff
+#
+##################
+
When /^I run jekyll(?: with "(.+)")?$/ do |opt|
run_jekyll_build(opt)
end
diff --git a/features/support/env.rb b/features/support/env.rb
index ad3bb38a8..5f6752aa2 100644
--- a/features/support/env.rb
+++ b/features/support/env.rb
@@ -6,10 +6,15 @@ require 'rr'
require 'test/unit'
require 'time'
+JEKYLL_SOURCE_DIR = File.dirname(File.dirname(File.dirname(__FILE__)))
TEST_DIR = File.expand_path(File.join('..', '..', 'tmp', 'jekyll'), File.dirname(__FILE__))
JEKYLL_PATH = File.join(File.dirname(__FILE__), '..', '..', 'bin', 'jekyll')
JEKYLL_COMMAND_OUTPUT_FILE = File.join(File.dirname(TEST_DIR), 'jekyll_output.txt')
+def source_dir(*files)
+ File.join(TEST_DIR, *files)
+end
+
def jekyll_output_file
JEKYLL_COMMAND_OUTPUT_FILE
end
@@ -19,7 +24,7 @@ def jekyll_run_output
end
def run_jekyll(args, output_file)
- command = "#{JEKYLL_PATH} #{args} > #{jekyll_output_file} 2>&1"
+ command = "#{JEKYLL_PATH} #{args} --trace > #{jekyll_output_file} 2>&1"
system command
end
diff --git a/lib/jekyll.rb b/lib/jekyll.rb
index e2dd369c4..d7a2c2eab 100644
--- a/lib/jekyll.rb
+++ b/lib/jekyll.rb
@@ -34,6 +34,8 @@ require 'jekyll/utils'
require 'jekyll/stevenson'
require 'jekyll/deprecator'
require 'jekyll/configuration'
+require 'jekyll/document'
+require 'jekyll/collection'
require 'jekyll/plugin_manager'
require 'jekyll/site'
require 'jekyll/convertible'
@@ -51,6 +53,7 @@ require 'jekyll/cleaner'
require 'jekyll/entry_filter'
require 'jekyll/layout_reader'
require 'jekyll/publisher'
+require 'jekyll/renderer'
# extensions
require 'jekyll/plugin'
diff --git a/lib/jekyll/cleaner.rb b/lib/jekyll/cleaner.rb
index e3a89b4be..583fc8446 100644
--- a/lib/jekyll/cleaner.rb
+++ b/lib/jekyll/cleaner.rb
@@ -4,6 +4,8 @@ module Jekyll
class Site
# Handles the cleanup of a site's destination before it is built.
class Cleaner
+ attr_reader :site
+
def initialize(site)
@site = site
end
@@ -27,7 +29,7 @@ module Jekyll
# Returns a Set with the file paths
def existing_files
files = Set.new
- Dir.glob(File.join(@site.dest, "**", "*"), File::FNM_DOTMATCH) do |file|
+ Dir.glob(File.join(site.dest, "**", "*"), File::FNM_DOTMATCH) do |file|
files << file unless file =~ /\/\.{1,2}$/ || file =~ keep_file_regex
end
files
@@ -38,7 +40,7 @@ module Jekyll
# Returns a Set with the file paths
def new_files
files = Set.new
- @site.each_site_file { |item| files << item.destination(@site.dest) }
+ site.each_site_file { |item| files << item.destination(site.dest) }
files
end
@@ -64,7 +66,7 @@ module Jekyll
#
# Returns the regular expression
def keep_file_regex
- or_list = @site.keep_files.join("|")
+ or_list = site.keep_files.join("|")
pattern = "\/(#{or_list.gsub(".", "\.")})"
Regexp.new pattern
end
diff --git a/lib/jekyll/collection.rb b/lib/jekyll/collection.rb
new file mode 100644
index 000000000..ad0e5fa54
--- /dev/null
+++ b/lib/jekyll/collection.rb
@@ -0,0 +1,121 @@
+module Jekyll
+ class Collection
+ attr_reader :site, :label
+
+ # Create a new Collection.
+ #
+ # site - the site to which this collection belongs.
+ # label - the name of the collection
+ #
+ # Returns nothing.
+ def initialize(site, label)
+ @site = site
+ @label = sanitize_label(label)
+ end
+
+ # Fetch the Documents in this collection.
+ # Defaults to an empty array if no documents have been read in.
+ #
+ # Returns an array of Jekyll::Document objects.
+ def docs
+ @docs ||= []
+ end
+
+ # Read the allowed documents into the collection's array of docs.
+ #
+ # Returns the sorted array of docs.
+ def read
+ filtered_entries.each do |file_path|
+ doc = Jekyll::Document.new(Jekyll.sanitized_path(directory, file_path), { site: site, collection: self })
+ doc.read
+ docs << doc
+ end
+ docs.sort!
+ end
+
+ # All the entries in this collection.
+ #
+ # Returns an Array of file paths to the documents in this collection
+ # relative to the collection's directory
+ def entries
+ return Array.new unless exists?
+ Dir.glob(File.join(directory, "**", "*.*")).map do |entry|
+ entry[File.join(directory, "")] = ''; entry
+ end
+ end
+
+ # Filtered version of the entries in this collection.
+ # See `Jekyll::EntryFilter#filter` for more information.
+ #
+ # Returns a list of filtered entry paths.
+ def filtered_entries
+ return Array.new unless exists?
+ Dir.chdir(directory) do
+ entry_filter.filter(entries)
+ end
+ end
+
+ # The directory for this Collection, relative to the site source.
+ #
+ # Returns a String containing the directory name where the collection
+ # is stored on the filesystem.
+ def relative_directory
+ "_#{label}"
+ end
+
+ # The full path to the directory containing the
+ #
+ # Returns a String containing th directory name where the collection
+ # is stored on the filesystem.
+ def directory
+ Jekyll.sanitized_path(site.source, relative_directory)
+ end
+
+ # Checks whether the directory "exists" for this collection.
+ # The directory must exist on the filesystem and must not be a symlink
+ # if in safe mode.
+ #
+ # Returns false if the directory doesn't exist or if it's a symlink
+ # and we're in safe mode.
+ def exists?
+ File.directory?(directory) && !(File.symlink?(directory) && site.safe)
+ end
+
+ # The entry filter for this collection.
+ # Creates an instance of Jekyll::EntryFilter.
+ #
+ # Returns the instance of Jekyll::EntryFilter for this collection.
+ def entry_filter
+ @entry_filter ||= Jekyll::EntryFilter.new(site, relative_directory)
+ end
+
+ # An inspect string.
+ #
+ # Returns the inspect string
+ def inspect
+ "#"
+ end
+
+ # Produce a sanitized label name
+ # Label names may not contain anything but alphanumeric characters,
+ # underscores, and hyphens.
+ #
+ # label - the possibly-unsafe label
+ #
+ # Returns a sanitized version of the label.
+ def sanitize_label(label)
+ label.gsub(/[^a-z0-9_\-]/i, '')
+ end
+
+ # Produce a representation of this Collection for use in Liquid.
+ # Exposes two attributes:
+ # - label
+ # - docs
+ #
+ # Returns a representation of this collection for use in Liquid.
+ def to_liquid
+ docs
+ end
+
+ end
+end
diff --git a/lib/jekyll/configuration.rb b/lib/jekyll/configuration.rb
index 93dc51930..ffdb0a229 100644
--- a/lib/jekyll/configuration.rb
+++ b/lib/jekyll/configuration.rb
@@ -13,6 +13,7 @@ module Jekyll
'data_source' => '_data',
'keep_files' => ['.git','.svn'],
'gems' => [],
+ 'collections' => nil,
'timezone' => nil, # use the local timezone
diff --git a/lib/jekyll/document.rb b/lib/jekyll/document.rb
new file mode 100644
index 000000000..ed2005117
--- /dev/null
+++ b/lib/jekyll/document.rb
@@ -0,0 +1,228 @@
+module Jekyll
+ class Document
+ include Comparable
+
+ attr_reader :path, :site
+ attr_accessor :content, :collection, :output
+
+ # Create a new Document.
+ #
+ # site - the Jekyll::Site instance to which this Document belongs
+ # path - the path to the file
+ #
+ # Returns nothing.
+ def initialize(path, relations)
+ @site = relations[:site]
+ @path = path
+ @collection = relations[:collection]
+ end
+
+ # Fetch the Document's data.
+ #
+ # Returns a Hash containing the data. An empty hash is returned if
+ # no data was read.
+ def data
+ @data ||= Hash.new
+ end
+
+ # The path to the document, relative to the site source.
+ #
+ # Returns a String path which represents the relative path
+ # from the site source to this document
+ def relative_path
+ Pathname.new(path).relative_path_from(Pathname.new(site.source)).to_s
+ end
+
+ # The base filename of the document.
+ #
+ # suffix - (optional) the suffix to be removed from the end of the filename
+ #
+ # Returns the base filename of the document.
+ def basename(suffix = "")
+ File.basename(path, suffix)
+ end
+
+ # The extension name of the document.
+ #
+ # Returns the extension name of the document.
+ def extname
+ File.extname(path)
+ end
+
+ # Produces a "cleaned" relative path.
+ # The "cleaned" relative path is the relative path without the extname
+ # and with the collection's directory removed as well.
+ # This method is useful when building the URL of the document.
+ #
+ # Examples:
+ # When relative_path is "_methods/site/generate.md":
+ # cleaned_relative_path
+ # # => "/site/generate"
+ #
+ # Returns the cleaned relative path of the document.
+ def cleaned_relative_path
+ relative_path[0 .. -extname.length - 1].sub(collection.relative_directory, "")
+ end
+
+ # Determine whether the document is a YAML file.
+ #
+ # Returns true if the extname is either .yml or .yaml, false otherwise.
+ def yaml_file?
+ %w[.yaml .yml].include?(extname)
+ end
+
+ # Determine whether the document is an asset file.
+ # Asset files include CoffeeScript files and Sass/SCSS files.
+ #
+ # Returns true if the extname belongs to the set of extensions
+ # that asset files use.
+ def asset_file?
+ %w[.sass .scss .coffee].include?(extname)
+ end
+
+ # Determine whether the file should be rendered with Liquid.
+ #
+ # Returns false if the document is either an asset file or a yaml file,
+ # true otherwise.
+ def render_with_liquid?
+ !(asset_file? || yaml_file?)
+ end
+
+ # The URL template where the document would be accessible.
+ #
+ # Returns the URL template for the document.
+ def url_template
+ "/:collection/:path:output_ext"
+ end
+
+ # Construct a Hash of key-value pairs which contain a mapping between
+ # a key in the URL template and the corresponding value for this document.
+ #
+ # Returns the Hash of key-value pairs for replacement in the URL.
+ def url_placeholders
+ {
+ collection: collection.label,
+ path: cleaned_relative_path,
+ output_ext: Jekyll::Renderer.new(site, self).output_ext
+ }
+ end
+
+ # The permalink for this Document.
+ # Permalink is set via the data Hash.
+ #
+ # Returns the permalink or nil if no permalink was set in the data.
+ def permalink
+ data && data['permalink']
+ end
+
+ # The computed URL for the document. See `Jekyll::URL#to_s` for more details.
+ #
+ # Returns the computed URL for the document.
+ def url
+ @url ||= URL.new({
+ template: url_template,
+ placeholders: url_placeholders,
+ permalink: permalink
+ }).to_s
+ end
+
+ # The full path to the output file.
+ #
+ # base_directory - the base path of the output directory
+ #
+ # Returns the full path to the output file of this document.
+ def destination(base_directory)
+ path = Jekyll.sanitized_path(base_directory, url)
+ path = File.join(path, "index.html") if url =~ /\/$/
+ path
+ end
+
+ # Write the generated Document file to the destination directory.
+ #
+ # dest - The String path to the destination dir.
+ #
+ # Returns nothing.
+ def write(dest)
+ path = destination(dest)
+ FileUtils.mkdir_p(File.dirname(path))
+ File.open(path, 'wb') do |f|
+ f.write(output)
+ end
+ end
+
+ # Returns merged option hash for File.read of self.site (if exists)
+ # and a given param
+ #
+ # opts - override options
+ #
+ # Return the file read options hash.
+ def merged_file_read_opts(opts)
+ site ? site.file_read_opts.merge(opts) : opts
+ end
+
+ # Whether the file is published or not, as indicated in YAML front-matter
+ #
+ # Returns true if the 'published' key is specified in the YAML front-matter and not `false`.
+ def published?
+ !(data.has_key?('published') && data['published'] == false)
+ end
+
+ # Read in the file and assign the content and data based on the file contents.
+ #
+ # Returns nothing.
+ def read(opts = {})
+ if yaml_file?
+ @data = SafeYAML.load_file(path)
+ else
+ begin
+ @content = File.read(path, merged_file_read_opts(opts))
+ if content =~ /\A(---\s*\n.*?\n?)^(---\s*$\n?)/m
+ @content = $POSTMATCH
+ @data = SafeYAML.load($1)
+ end
+ rescue SyntaxError => e
+ puts "YAML Exception reading #{path}: #{e.message}"
+ rescue Exception => e
+ puts "Error reading file #{path}: #{e.message}"
+ end
+ end
+ end
+
+ # Create a Liquid-understandable version of this Document.
+ #
+ # Returns a Hash representing this Document's data.
+ def to_liquid
+ Utils.deep_merge_hashes data, {
+ "content" => content,
+ "path" => path,
+ "relative_path" => relative_path,
+ "url" => url
+ }
+ end
+
+ # The inspect string for this document.
+ # Includes the relative path and the collection label.
+ #
+ # Returns the inspect string for this document.
+ def inspect
+ "#"
+ end
+
+ # The string representation for this document.
+ #
+ # Returns the content of the document
+ def to_s
+ output || content
+ end
+
+ # Compare this document against another document.
+ # Comparison is a comparison between the 2 paths of the documents.
+ #
+ # Returns -1, 0, +1 or nil depending on whether this doc's path is less than,
+ # equal or greater than the other doc's path. See String#<=> for more details.
+ def <=>(anotherDocument)
+ path <=> anotherDocument.path
+ end
+
+ end
+end
diff --git a/lib/jekyll/entry_filter.rb b/lib/jekyll/entry_filter.rb
index 11db9e11c..0b00218b8 100644
--- a/lib/jekyll/entry_filter.rb
+++ b/lib/jekyll/entry_filter.rb
@@ -1,5 +1,7 @@
module Jekyll
class EntryFilter
+ SPECIAL_LEADING_CHARACTERS = ['.', '_', '#'].freeze
+
attr_reader :site
def initialize(site, base_directory = nil)
@@ -35,7 +37,8 @@ module Jekyll
end
def special?(entry)
- ['.', '_', '#'].include?(entry[0..0])
+ SPECIAL_LEADING_CHARACTERS.include?(entry[0..0]) ||
+ SPECIAL_LEADING_CHARACTERS.include?(File.basename(entry)[0..0])
end
def backup?(entry)
diff --git a/lib/jekyll/renderer.rb b/lib/jekyll/renderer.rb
new file mode 100644
index 000000000..e3d233df3
--- /dev/null
+++ b/lib/jekyll/renderer.rb
@@ -0,0 +1,132 @@
+module Jekyll
+ class Renderer
+
+ attr_reader :document, :site
+
+ def initialize(site, document)
+ @site = site
+ @document = document
+ end
+
+ # Determine which converters to use based on this document's
+ # extension.
+ #
+ # Returns an array of Converter instances.
+ def converters
+ @converters ||= site.converters.select { |c| c.matches(document.extname) }
+ end
+
+ # Determine the extname the outputted file should have
+ #
+ # Returns the output extname including the leading period.
+ def output_ext
+ converters.first.output_ext(document.extname)
+ end
+
+ ######################
+ ## DAT RENDER THO
+ ######################
+
+ def run
+ payload = Utils.deep_merge_hashes({
+ "page" => document.to_liquid
+ }, site.site_payload)
+
+ info = {
+ filters: [Jekyll::Filters],
+ registers: { :site => site, :page => payload['page'] }
+ }
+
+ # render and transform content (this becomes the final content of the object)
+ payload["highlighter_prefix"] = converters.first.highlighter_prefix
+ payload["highlighter_suffix"] = converters.first.highlighter_suffix
+
+ output = document.content
+
+ if document.render_with_liquid?
+ output = render_liquid(output, payload, info)
+ end
+
+ place_in_layouts(
+ convert(output),
+ payload,
+ info
+ )
+ end
+
+ # Convert the given content using the converters which match this renderer's document.
+ #
+ # content - the raw, unconverted content
+ #
+ # Returns the converted content.
+ def convert(content)
+ converters.reduce(content) do |output, converter|
+ begin
+ converter.convert output
+ rescue => e
+ Jekyll.logger.error "Conversion error:", "#{converter.class} encountered an error converting '#{document.relative_path}'."
+ raise e
+ end
+ end
+ end
+
+ # Render the given content with the payload and info
+ #
+ # content -
+ # payload -
+ # info -
+ # path - (optional) the path to the file, for use in ex
+ #
+ # Returns the content, rendered by Liquid.
+ def render_liquid(content, payload, info, path = nil)
+ Liquid::Template.parse(content).render!(payload, info)
+ rescue Tags::IncludeTagError => e
+ Jekyll.logger.error "Liquid Exception:", "#{e.message} in #{e.path}, included in #{path || document.relative_path}"
+ raise e
+ rescue Exception => e
+ Jekyll.logger.error "Liquid Exception:", "#{e.message} in #{path || document.relative_path}"
+ raise e
+ end
+
+ # Render layouts and place given content inside.
+ #
+ # content - the content to be placed in the layout
+ #
+ #
+ # Returns the content placed in the Liquid-rendered layouts
+ def place_in_layouts(content, payload, info)
+ output = content.dup
+ layout = site.layouts[document.data["layout"]]
+ used = Set.new([layout])
+
+ while layout
+ payload = Utils.deep_merge_hashes(
+ payload,
+ {
+ "content" => output,
+ "page" => document.to_liquid,
+ "layout" => layout.data
+ }
+ )
+
+ output = render_liquid(
+ layout.content,
+ payload,
+ info,
+ File.join(site.config['layouts'], layout.name)
+ )
+
+ if layout = site.layouts[layout.data["layout"]]
+ if used.include?(layout)
+ layout = nil # avoid recursive chain
+ else
+ used << layout
+ end
+ end
+ end
+
+ output
+ end
+
+ end
+end
diff --git a/lib/jekyll/site.rb b/lib/jekyll/site.rb
index 14224898d..92e5e3594 100644
--- a/lib/jekyll/site.rb
+++ b/lib/jekyll/site.rb
@@ -4,7 +4,7 @@ module Jekyll
:exclude, :include, :source, :dest, :lsi, :highlighter,
:permalink_style, :time, :future, :unpublished, :safe, :plugins, :limit_posts,
:show_drafts, :keep_files, :baseurl, :data, :file_read_opts, :gems,
- :plugin_manager
+ :plugin_manager, :collections
attr_accessor :converters, :generators
@@ -14,12 +14,13 @@ module Jekyll
def initialize(config)
self.config = config.clone
- %w[safe lsi highlighter baseurl exclude include future unpublished show_drafts limit_posts keep_files gems].each do |opt|
+ %w[safe lsi highlighter baseurl exclude include future unpublished
+ show_drafts limit_posts keep_files gems].each do |opt|
self.send("#{opt}=", config[opt])
end
- self.source = File.expand_path(config['source'])
- self.dest = File.expand_path(config['destination'])
+ self.source = File.expand_path(config['source'])
+ self.dest = File.expand_path(config['destination'])
self.permalink_style = config['permalink'].to_sym
self.plugin_manager = Jekyll::PluginManager.new(self)
@@ -83,6 +84,26 @@ module Jekyll
end
end
+ # The list of collections and their corresponding Jekyll::Collection instances.
+ # If config['collections'] is set, a new instance is created for each item in the collection.
+ # If config['collections'] is not set, a new hash is returned.
+ #
+ # Returns a Hash containing collection name-to-instance pairs.
+ def collections
+ @collections ||= if config['collections']
+ Hash[config['collections'].map { |coll| [coll, Jekyll::Collection.new(self, coll)] } ]
+ else
+ Hash.new
+ end
+ end
+
+ # The list of collections to render.
+ #
+ # The array of collection labels to render.
+ def to_render
+ @to_render ||= (config['render'] || Array.new)
+ end
+
# Read Site data from disk and load it into internal data structures.
#
# Returns nothing.
@@ -90,6 +111,7 @@ module Jekyll
self.layouts = LayoutReader.new(self).read
read_directories
read_data(config['data_source'])
+ read_collections
end
# Recursively traverse directories to find posts, pages and static files
@@ -166,19 +188,25 @@ module Jekyll
#
# Returns nothing
def read_data(dir)
- base = File.join(source, dir)
- return unless File.directory?(base) && (!safe || !File.symlink?(base))
-
- entries = Dir.chdir(base) { Dir['*.{yaml,yml}'] }
- entries.delete_if { |e| File.directory?(File.join(base, e)) }
-
- entries.each do |entry|
- path = File.join(source, dir, entry)
- next if File.symlink?(path) && safe
-
- key = sanitize_filename(File.basename(entry, '.*'))
- self.data[key] = SafeYAML.load_file(path)
+ unless dir.to_s.eql?("_data")
+ Jekyll.logger.error "Error:", "Data source directories other than '_data' have been removed.\n" +
+ "Please move your YAML files to `_data` and remove the `data_source` key from your `_config.yml`."
end
+
+ collections['data'] = Jekyll::Collection.new(self, "data")
+ collections['data'].read
+
+ collections['data'].docs.each do |doc|
+ key = sanitize_filename(doc.basename(".*"))
+ self.data[key] = doc.data
+ end
+ end
+
+ # Read in all collections specified in the configuration
+ #
+ # Returns nothing.
+ def read_collections
+ collections.each { |_, collection| collection.read }
end
# Run each of the Generators.
@@ -196,6 +224,12 @@ module Jekyll
def render
relative_permalinks_deprecation_method
+ to_render.each do |label|
+ collections[label].docs.each do |document|
+ document.output = Jekyll::Renderer.new(self, document).run
+ end
+ end
+
payload = site_payload
[posts, pages].flatten.each do |page_or_post|
page_or_post.render(layouts, payload)
@@ -271,7 +305,8 @@ module Jekyll
# See Site#post_attr_hash for type info.
def site_payload
{"jekyll" => { "version" => Jekyll::VERSION },
- "site" => config.merge({
+ "site" => Utils.deep_merge_hashes(config,
+ Utils.deep_merge_hashes(collections, {
"time" => time,
"posts" => posts.sort { |a, b| b <=> a },
"pages" => pages,
@@ -279,7 +314,9 @@ module Jekyll
"html_pages" => pages.reject { |page| !page.html? },
"categories" => post_attr_hash('categories'),
"tags" => post_attr_hash('tags'),
- "data" => site_data})}
+ "data" => site_data
+ }))
+ }
end
# Filter out any files/directories that are hidden or backup files (start
@@ -357,8 +394,18 @@ module Jekyll
end
end
+ def documents
+ collections.reduce(Set.new) do |docs, (label, coll)|
+ if to_render.include?(label)
+ docs.merge(coll.docs)
+ else
+ docs
+ end
+ end
+ end
+
def each_site_file
- %w(posts pages static_files).each do |type|
+ %w(posts pages static_files documents).each do |type|
send(type).each do |item|
yield item
end
diff --git a/lib/jekyll/url.rb b/lib/jekyll/url.rb
index 66b4412d6..8cd47242d 100644
--- a/lib/jekyll/url.rb
+++ b/lib/jekyll/url.rb
@@ -24,9 +24,9 @@ module Jekyll
# template. Instead, the given permalink will be
# used as URL.
def initialize(options)
- @template = options[:template]
+ @template = options[:template]
@placeholders = options[:placeholders] || {}
- @permalink = options[:permalink]
+ @permalink = options[:permalink]
if (@template || @permalink).nil?
raise ArgumentError, "One of :template or :permalink must be supplied."
diff --git a/script/console b/script/console
new file mode 100755
index 000000000..34ad6e8a3
--- /dev/null
+++ b/script/console
@@ -0,0 +1,38 @@
+#!/usr/bin/env ruby
+
+require 'pry'
+$LOAD_PATH.unshift File.join(File.dirname(__FILE__), *%w{ .. lib })
+require 'jekyll'
+
+TEST_DIR = File.expand_path(File.join(File.dirname(__FILE__), *%w{ .. test }))
+
+def fixture_site(overrides = {})
+ Jekyll::Site.new(site_configuration(overrides))
+end
+
+def build_configs(overrides, base_hash = Jekyll::Configuration::DEFAULTS)
+ Jekyll::Utils.deep_merge_hashes(base_hash, overrides)
+end
+
+def site_configuration(overrides = {})
+ build_configs({
+ "source" => source_dir,
+ "destination" => dest_dir
+ }, build_configs(overrides))
+end
+
+def dest_dir(*subdirs)
+ test_dir('dest', *subdirs)
+end
+
+def source_dir(*subdirs)
+ test_dir('source', *subdirs)
+end
+
+def test_dir(*subdirs)
+ File.join(TEST_DIR, *subdirs)
+end
+
+module Jekyll
+ binding.pry
+end
diff --git a/site/_data/docs.yml b/site/_data/docs.yml
index 1c7906364..7d0a83f3f 100644
--- a/site/_data/docs.yml
+++ b/site/_data/docs.yml
@@ -14,6 +14,7 @@
- drafts
- pages
- variables
+ - collections
- datafiles
- assets
- migrations
diff --git a/site/docs/collections.md b/site/docs/collections.md
new file mode 100644
index 000000000..23ac190a8
--- /dev/null
+++ b/site/docs/collections.md
@@ -0,0 +1,126 @@
+---
+layout: docs
+title: Collections
+prev_section: variables
+next_section: datafiles
+permalink: /docs/collections/
+---
+
+
+ This is an experimental feature and that the API may likely change until the feature stabilizes.
+
+
+
+Put some things in a folder and add the folder to your config. It's simple...
+
+Not everything is a post or a page. Maybe you want to document the various methods in your open source project, members of a team, or talks at a conference. Collections allow you to define a new type of document that behave like Pages or Posts do normally, but also have their own unique properties and namespace.
+
+## Using Collections
+
+### Step 1: Tell Jekyll to read in your collection
+
+Add the following to your site's `_config.yml` file, replacing `my_collection` with the name of your collection:
+
+{% highlight yaml %}
+collections:
+- my_collection
+{% endhighlight %}
+
+### Step 2: Add your content
+
+Create a corresponding folder (e.g. `/_my_collection`) and add documents.
+YAML front-matter is read in as data if it exists, if not, then everything is just stuck in the Document's `content` attribute.
+
+Note: the folder must be named identical to the collection you defined in you config.yml file, with the addition of the preceding `_` character.
+
+### Step 3: Optionally render your collection's documents into independent files
+
+If you'd like Jekyll to create a public-facing, rendered version of each document in your collection, add your collection name to the `render` config key in your `_config.yml`:
+
+{% highlight yaml %}
+render:
+- my_collection
+{% endhighlight %}
+
+This will produce a file for each document in the collection.
+For example, if you have `_my_collection/some_subdir/some_doc.md`,
+it will be rendered using Liquid and the Markdown converter of your
+choice and written out to `/my_collection/some_subdir/some_doc.html`.
+
+## Liquid Attributes
+
+### Collections
+
+Each collection is accessible via the `site` Liquid variable. For example, if you want to access the `albums` collection found in `_albums`, you'd use `site.albums`. Each collection is itself an array of documents (e.g. `site.albums` is an array of documents, much like `site.pages` and `site.posts`). See below for how to access attributes of those documents.
+
+### Documents
+
+In addition to any YAML front-matter provided in the document's corresponding file, each document has the following attributes:
+
+
+
+
+
+
Variable
+
Description
+
+
+
+
+
+
content
+
+
+
+ The content of the document. If no YAML front-matter is provided,
+ this is the entirety of the file contents. If YAML front-matter
+ is used, then this is all the contents of the file after the terminating
+ `---` of the front-matter.
+
+
+
+
+
+
path
+
+
+
+ The full path to the document's source file.
+
+
+
+
+
+
relative_path
+
+
+
+ The path to the document's source file relative to the site source.
+
+
+
+
+
+
url
+
+
+
+ The URL of the rendered collection. The file is only written to the
+ destination when the name of the collection to which it belongs is
+ included in the render key in the site's configuration file.
+
+
+
+
+
+
diff --git a/site/docs/datafiles.md b/site/docs/datafiles.md
index 55af6ec43..1dbe40972 100644
--- a/site/docs/datafiles.md
+++ b/site/docs/datafiles.md
@@ -1,7 +1,7 @@
---
layout: docs
title: Data Files
-prev_section: variables
+prev_section: collections
next_section: assets
permalink: /docs/datafiles/
---
diff --git a/site/docs/variables.md b/site/docs/variables.md
index 2c6d38492..d6c7390b4 100644
--- a/site/docs/variables.md
+++ b/site/docs/variables.md
@@ -2,7 +2,7 @@
layout: docs
title: Variables
prev_section: pages
-next_section: datafiles
+next_section: collections
permalink: /docs/variables/
---
diff --git a/test/source/_methods/_do_not_read_me.md b/test/source/_methods/_do_not_read_me.md
new file mode 100644
index 000000000..1b5ad07d0
--- /dev/null
+++ b/test/source/_methods/_do_not_read_me.md
@@ -0,0 +1,5 @@
+---
+title: The unreadable wonder
+---
+
+Don't read me, you fool! FILTER ME
diff --git a/test/source/_methods/configuration.md b/test/source/_methods/configuration.md
new file mode 100644
index 000000000..fd17980b4
--- /dev/null
+++ b/test/source/_methods/configuration.md
@@ -0,0 +1,8 @@
+---
+title: "Jekyll.configuration"
+whatever: foo.bar
+---
+
+Use `{{ page.title }}` to build a full configuration for use w/Jekyll.
+
+Whatever: {{ page.whatever }}
diff --git a/test/source/_methods/sanitized_path.md b/test/source/_methods/sanitized_path.md
new file mode 100644
index 000000000..8b4d767a8
--- /dev/null
+++ b/test/source/_methods/sanitized_path.md
@@ -0,0 +1,5 @@
+---
+title: "Jekyll.sanitized_path"
+---
+
+`{{ page.title }}` is used to make sure your path is in your source.
diff --git a/test/source/_methods/site/_dont_include_me_either.md b/test/source/_methods/site/_dont_include_me_either.md
new file mode 100644
index 000000000..66079613e
--- /dev/null
+++ b/test/source/_methods/site/_dont_include_me_either.md
@@ -0,0 +1,5 @@
+---
+title: Don't Include Me Either
+---
+
+Don't include me either. FILTER ME PLZ
diff --git a/test/source/_methods/site/generate.md b/test/source/_methods/site/generate.md
new file mode 100644
index 000000000..1cab376ee
--- /dev/null
+++ b/test/source/_methods/site/generate.md
@@ -0,0 +1,6 @@
+---
+title: "Site#generate"
+layout: default
+---
+
+Run your generators! {{ page.layout }}
diff --git a/test/source/_methods/site/initialize.md b/test/source/_methods/site/initialize.md
new file mode 100644
index 000000000..9c23b967f
--- /dev/null
+++ b/test/source/_methods/site/initialize.md
@@ -0,0 +1,5 @@
+---
+title: "Site#initialize"
+---
+
+Create dat site.
diff --git a/test/source/_methods/um_hi.md b/test/source/_methods/um_hi.md
new file mode 120000
index 000000000..9ebb53251
--- /dev/null
+++ b/test/source/_methods/um_hi.md
@@ -0,0 +1 @@
+./site/generate.md
\ No newline at end of file
diff --git a/test/test_collections.rb b/test/test_collections.rb
new file mode 100644
index 000000000..8b4c2d230
--- /dev/null
+++ b/test/test_collections.rb
@@ -0,0 +1,129 @@
+require 'helper'
+
+class TestCollections < Test::Unit::TestCase
+
+ def fixture_site(overrides = {})
+ Jekyll::Site.new(Jekyll.configuration(
+ overrides.merge({
+ "source" => source_dir,
+ "destination" => dest_dir
+ })
+ ))
+ end
+
+ context "an evil collection" do
+ setup do
+ @collection = Jekyll::Collection.new(fixture_site, "../../etc/password")
+ end
+
+ should "sanitize the label name" do
+ assert_equal @collection.label, "etcpassword"
+ end
+
+ should "have a sanitized relative path name" do
+ assert_equal @collection.relative_directory, "_etcpassword"
+ end
+
+ should "have a sanitized full path" do
+ assert_equal @collection.directory, source_dir("_etcpassword")
+ end
+ end
+
+ context "a simple collection" do
+ setup do
+ @collection = Jekyll::Collection.new(fixture_site, "methods")
+ end
+
+ should "sanitize the label name" do
+ assert_equal @collection.label, "methods"
+ end
+
+ should "contain no docs when initialized" do
+ assert_empty @collection.docs
+ end
+
+ should "know its relative directory" do
+ assert_equal @collection.relative_directory, "_methods"
+ end
+
+ should "know the full path to itself on the filesystem" do
+ assert_equal @collection.directory, source_dir("_methods")
+ end
+ end
+
+ context "with no collections specified" do
+ setup do
+ @site = fixture_site
+ @site.process
+ end
+
+ should "not contain any collections other than the default ones" do
+ collections = @site.collections.dup
+ assert collections.delete("data").is_a?(Jekyll::Collection)
+ assert_equal Hash.new, collections
+ end
+ end
+
+ context "with a collection" do
+ setup do
+ @site = fixture_site({
+ "collections" => ["methods"]
+ })
+ @site.process
+ @collection = @site.collections["methods"]
+ end
+
+ should "create a Hash on Site with the label mapped to the instance of the Collection" do
+ assert @site.collections.is_a?(Hash)
+ assert_not_nil @site.collections["methods"]
+ assert @site.collections["methods"].is_a? Jekyll::Collection
+ end
+
+ should "collects docs in an array on the Collection object" do
+ assert @site.collections["methods"].docs.is_a? Array
+ @site.collections["methods"].docs.each do |doc|
+ assert doc.is_a? Jekyll::Document
+ assert_include %w[
+ _methods/configuration.md
+ _methods/sanitized_path.md
+ _methods/site/generate.md
+ _methods/site/initialize.md
+ _methods/um_hi.md
+ ], doc.relative_path
+ end
+ end
+
+ should "not include files which start with an underscore in the base collection directory" do
+ assert_not_include @collection.filtered_entries, "_do_not_read_me.md"
+ end
+
+ should "not include files which start with an underscore in a subdirectory" do
+ assert_not_include @collection.filtered_entries, "site/_dont_include_me_either.md"
+ end
+
+ should "not include the underscored files in the list of docs" do
+ assert_not_include @collection.docs.map(&:relative_path), "_methods/_do_not_read_me.md"
+ assert_not_include @collection.docs.map(&:relative_path), "_methods/site/_dont_include_me_either.md"
+ end
+ end
+
+ context "in safe mode" do
+ setup do
+ @site = fixture_site({
+ "collections" => ["methods"],
+ "safe" => true
+ })
+ @site.process
+ @collection = @site.collections["methods"]
+ end
+
+ should "not allow symlinks" do
+ assert_not_include @collection.filtered_entries, "um_hi.md"
+ end
+
+ should "not include the symlinked file in the list of docs" do
+ assert_not_include @collection.docs.map(&:relative_path), "_methods/um_hi.md"
+ end
+ end
+
+end
diff --git a/test/test_document.rb b/test/test_document.rb
new file mode 100644
index 000000000..794c2117a
--- /dev/null
+++ b/test/test_document.rb
@@ -0,0 +1,48 @@
+require 'helper'
+
+class TestDocument < Test::Unit::TestCase
+
+ context "a document in a collection" do
+ setup do
+ @site = Site.new(Jekyll.configuration({
+ "collections" => ["methods"],
+ "source" => source_dir,
+ "destination" => dest_dir
+ }))
+ @site.process
+ @document = @site.collections["methods"].docs.first
+ end
+
+ should "know its relative path" do
+ assert_equal "_methods/configuration.md", @document.relative_path
+ end
+
+ should "knows its extname" do
+ assert_equal ".md", @document.extname
+ end
+
+ should "know its basename" do
+ assert_equal "configuration.md", @document.basename
+ end
+
+ should "allow the suffix to be specified for the basename" do
+ assert_equal "configuration", @document.basename(".*")
+ end
+
+ should "know whether its a yaml file" do
+ assert_equal false, @document.yaml_file?
+ end
+
+ should "know its data" do
+ assert_equal({
+ "title" => "Jekyll.configuration",
+ "whatever" => "foo.bar"
+ }, @document.data)
+ end
+
+ end
+
+ context " a document part of a rendered collection" do
+ end
+
+end