Merge pull request #2205 from jekyll/maul-esel-frontmatter-defaults

This commit is contained in:
Parker Moore 2014-04-21 23:01:05 -04:00
commit 9db5a1a6ce
10 changed files with 325 additions and 2 deletions

View File

@ -0,0 +1,79 @@
Feature: frontmatter defaults
Scenario: Use default for frontmatter variables internally
Given I have a _layouts directory
And I have a pretty layout that contains "THIS IS THE LAYOUT: {{content}}"
And I have a _posts directory
And I have the following post:
| title | date | content |
| default layout | 2013-09-11 | just some post |
And I have an "index.html" page with title "some title" that contains "just some page"
And I have a configuration file with "defaults" set to "[{scope: {path: ""}, values: {layout: "pretty"}}]"
When I run jekyll
Then the _site directory should exist
And I should see "THIS IS THE LAYOUT: <p>just some post</p>" in "_site/2013/09/11/default-layout.html"
And I should see "THIS IS THE LAYOUT: just some page" in "_site/index.html"
Scenario: Use default for frontmatter variables in Liquid
Given I have a _posts directory
And I have the following post:
| title | date | content |
| default data | 2013-09-11 | <p>{{page.custom}}</p><div>{{page.author}}</div> |
And I have an "index.html" page that contains "just {{page.custom}} by {{page.author}}"
And I have a configuration file with "defaults" set to "[{scope: {path: ""}, values: {custom: "some special data", author: "Ben"}}]"
When I run jekyll
Then the _site directory should exist
And I should see "<p>some special data</p><div>Ben</div>" in "_site/2013/09/11/default-data.html"
And I should see "just some special data by Ben" in "_site/index.html"
Scenario: Override frontmatter defaults by path
Given I have a _layouts directory
And I have a root layout that contains "root: {{ content }}"
And I have a subfolder layout that contains "subfolder: {{ content }}"
And I have a _posts directory
And I have the following post:
| title | date | content |
| about | 2013-10-14 | info on {{page.description}} |
And I have a special/_posts directory
And I have the following post in "special":
| title | date | path | content |
| about | 2013-10-14 | local | info on {{page.description}} |
And I have an "index.html" page with title "overview" that contains "Overview for {{page.description}}"
And I have an "special/index.html" page with title "section overview" that contains "Overview for {{page.description}}"
And I have a configuration file with "defaults" set to "[{scope: {path: "special"}, values: {layout: "subfolder", description: "the special section"}}, {scope: {path: ""}, values: {layout: "root", description: "the webpage"}}]"
When I run jekyll
Then the _site directory should exist
And I should see "root: <p>info on the webpage</p>" in "_site/2013/10/14/about.html"
And I should see "subfolder: <p>info on the special section</p>" in "_site/special/2013/10/14/about.html"
And I should see "root: Overview for the webpage" in "_site/index.html"
And I should see "subfolder: Overview for the special section" in "_site/special/index.html"
Scenario: Override frontmatter defaults by type
Given I have a _posts directory
And I have the following post:
| title | date | content |
| this is a post | 2013-10-14 | blabla |
And I have an "index.html" page that contains "interesting stuff"
And I have a configuration file with "defaults" set to "[{scope: {path: "", type: "post"}, values: {permalink: "/post.html"}}, {scope: {path: "", type: "page"}, values: {permalink: "/page.html"}}, {scope: {path: ""}, values: {permalink: "/perma.html"}}]"
When I run jekyll
Then I should see "blabla" in "_site/post.html"
And I should see "interesting stuff" in "_site/page.html"
But the "_site/perma.html" file should not exist
Scenario: Actual frontmatter overrides defaults
Given I have a _posts directory
And I have the following post:
| title | date | permalink | author | content |
| override | 2013-10-14 | /frontmatter.html | some guy | a blog by {{page.author}} |
And I have an "index.html" page with permalink "override.html" that contains "nothing"
And I have a configuration file with "defaults" set to "[{scope: {path: ""}, values: {permalink: "/perma.html", author: "Chris"}}]"
When I run jekyll
Then I should see "a blog by some guy" in "_site/frontmatter.html"
And I should see "nothing" in "_site/override.html"
But the "_site/perma.html" file should not exist

View File

@ -37,6 +37,7 @@ require 'jekyll/configuration'
require 'jekyll/document' require 'jekyll/document'
require 'jekyll/collection' require 'jekyll/collection'
require 'jekyll/plugin_manager' require 'jekyll/plugin_manager'
require 'jekyll/frontmatter_defaults'
require 'jekyll/site' require 'jekyll/site'
require 'jekyll/convertible' require 'jekyll/convertible'
require 'jekyll/url' require 'jekyll/url'

View File

@ -46,6 +46,8 @@ module Jekyll
'excerpt_separator' => "\n\n", 'excerpt_separator' => "\n\n",
'defaults' => [],
'maruku' => { 'maruku' => {
'use_tex' => false, 'use_tex' => false,
'use_divs' => false, 'use_divs' => false,
@ -251,6 +253,5 @@ module Jekyll
config config
end end
end end
end end

View File

@ -14,6 +14,8 @@ require 'set'
# self.output= # self.output=
# self.name # self.name
# self.path # self.path
# self.type -> :page, :post or :draft
module Jekyll module Jekyll
module Convertible module Convertible
# Returns the contents as a String. # Returns the contents as a String.
@ -107,7 +109,19 @@ module Jekyll
further_data = Hash[(attrs || self.class::ATTRIBUTES_FOR_LIQUID).map { |attribute| further_data = Hash[(attrs || self.class::ATTRIBUTES_FOR_LIQUID).map { |attribute|
[attribute, send(attribute)] [attribute, send(attribute)]
}] }]
Utils.deep_merge_hashes(data, further_data)
defaults = site.frontmatter_defaults.all(relative_path, type)
Utils.deep_merge_hashes defaults, Utils.deep_merge_hashes(data, further_data)
end
def type
if is_a?(Post)
:post
elsif is_a?(Page)
:page
elsif is_a?(Draft)
:draft
end
end end
# Recursively render layouts # Recursively render layouts

View File

@ -0,0 +1,148 @@
module Jekyll
class Configuration
# This class handles custom defaults for YAML frontmatter settings.
# These are set in _config.yml and apply both to internal use (e.g. layout)
# and the data available to liquid.
#
# It is exposed via the frontmatter_defaults method on the site class.
class FrontmatterDefaults
# Initializes a new instance.
def initialize(site)
@site = site
end
# Finds a default value for a given setting, filtered by path and type
#
# path - the path (relative to the source) of the page, post or :draft the default is used in
# type - a symbol indicating whether a :page, a :post or a :draft calls this method
#
# Returns the default value or nil if none was found
def find(path, type, setting)
value = nil
old_scope = nil
matching_sets(path, type).each do |set|
if set['values'].has_key?(setting) && has_precedence?(old_scope, set['scope'])
value = set['values'][setting]
old_scope = set['scope']
end
end
value
end
# Collects a hash with all default values for a page or post
#
# path - the relative path of the page or post
# type - a symbol indicating the type (:post, :page or :draft)
#
# Returns a hash with all default values (an empty hash if there are none)
def all(path, type)
defaults = {}
old_scope = nil
matching_sets(path, type).each do |set|
if has_precedence?(old_scope, set['scope'])
defaults.merge! set['values']
old_scope = set['scope']
else
defaults = set['values'].merge(defaults)
end
end
defaults
end
private
# Checks if a given default setting scope matches the given path and type
#
# scope - the hash indicating the scope, as defined in _config.yml
# path - the path to check for
# type - the type (:post, :page or :draft) to check for
#
# Returns true if the scope applies to the given path and type
def applies?(scope, path, type)
applies_path?(scope, path) && applies_type?(scope, type)
end
def applies_path?(scope, path)
return true if scope['path'].empty?
scope_path = Pathname.new(scope['path'])
Pathname.new(sanitize_path(path)).ascend do |path|
if path == scope_path
return true
end
end
end
def applies_type?(scope, type)
!scope.has_key?('type') || scope['type'] == type.to_s
end
# Checks if a given set of default values is valid
#
# set - the default value hash, as defined in _config.yml
#
# Returns true if the set is valid and can be used in this class
def valid?(set)
set.is_a?(Hash) && set['scope'].is_a?(Hash) && set['scope']['path'].is_a?(String) && set['values'].is_a?(Hash)
end
# Determines if a new scope has precedence over an old one
#
# old_scope - the old scope hash, or nil if there's none
# new_scope - the new scope hash
#
# Returns true if the new scope has precedence over the older
def has_precedence?(old_scope, new_scope)
return true if old_scope.nil?
new_path = sanitize_path(new_scope['path'])
old_path = sanitize_path(old_scope['path'])
if new_path.length != old_path.length
new_path.length >= old_path.length
elsif new_scope.has_key? 'type'
true
else
!old_scope.has_key? 'type'
end
end
# Collects a list of sets that match the given path and type
#
# Returns an array of hashes
def matching_sets(path, type)
valid_sets.select do |set|
applies?(set['scope'], path, type)
end
end
# Returns a list of valid sets
#
# This is not cached to allow plugins to modify the configuration
# and have their changes take effect
#
# Returns an array of hashes
def valid_sets
sets = @site.config['defaults']
return [] unless sets.is_a?(Array)
sets.select do |set|
unless valid?(set)
Jekyll.logger.warn "Default:", "An invalid default set was found"
end
valid?(set)
end
end
# Sanitizes the given path by removing a leading and addding a trailing slash
def sanitize_path(path)
if path.nil? || path.empty?
""
else
path.gsub(/\A\//, '').gsub(/([^\/])\z/, '\1/')
end
end
end
end
end

View File

@ -28,8 +28,13 @@ module Jekyll
@dir = dir @dir = dir
@name = name @name = name
process(name) process(name)
read_yaml(File.join(base, dir), name) read_yaml(File.join(base, dir), name)
data.default_proc = proc do |hash, key|
site.frontmatter_defaults.find(File.join(dir, name), type, key)
end
end end
# The generated directory into which the page will be placed # The generated directory into which the page will be placed

View File

@ -56,6 +56,10 @@ module Jekyll
process(name) process(name)
read_yaml(@base, name) read_yaml(@base, name)
data.default_proc = proc do |hash, key|
site.frontmatter_defaults.find(File.join(dir, name), type, key)
end
if data.has_key?('date') if data.has_key?('date')
self.date = Time.parse(data["date"].to_s) self.date = Time.parse(data["date"].to_s)
end end
@ -64,6 +68,14 @@ module Jekyll
populate_tags populate_tags
end end
def published?
if data.has_key?('published') && data['published'] == false
false
else
true
end
end
def populate_categories def populate_categories
if categories.empty? if categories.empty?
self.categories = Utils.pluralized_array_from_hash(data, 'category', 'categories').map {|c| c.to_s.downcase} self.categories = Utils.pluralized_array_from_hash(data, 'category', 'categories').map {|c| c.to_s.downcase}

View File

@ -412,6 +412,10 @@ module Jekyll
end end
end end
def frontmatter_defaults
@frontmatter_defaults ||= Configuration::FrontmatterDefaults.new(self)
end
private private
def has_relative_page? def has_relative_page?

View File

@ -121,6 +121,18 @@ class="flag">flags</code> (specified on the command-line) that control them.
<p><code class="option">encoding: ENCODING</code></p> <p><code class="option">encoding: ENCODING</code></p>
</td> </td>
</tr> </tr>
<tr>
<td>
<p class='name'><strong>Defaults</strong></p>
<p class='description'>
Set defaults for <a href="../frontmatter/" title="YAML frontmatter">YAML frontmatter</a>
variables.
</p>
</td>
<td class='align-center'>
<p>see <a href="#frontmatter_defaults" title="details">below</a></p>
</td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>
@ -264,6 +276,43 @@ before your site is served.
</p> </p>
</div> </div>
## Frontmatter defaults
You can set default values for your [YAML frontmatter](../frontmatter/) variables
in your configuration. This way, you can for example set default layouts or define
defaults for your custom variables. Of course, any variable actually specified in
the front matter overrides the defaults.
All defaults go under the `defaults` key, which holds a list of scope-values combinations ("default sets").
The `scope` key defines for which files the defaults apply, limiting them by their `path` and
optionally by their `type` (`page`, `post` or `draft`). The `values` key holds the actual list of defaults.
For example:
{% highlight yaml %}
defaults:
-
scope:
path: "" # empty string for all files
values:
layout: "my-site"
-
scope:
path: "about/blog"
type: "post"
values:
layout: "meta-blog" # overrides previous default layout
author: "Dr. Hyde"
{% endhighlight %}
With these defaults, all pages and posts would default to the `my-site` layout except for the posts under `about/blog`,
who would default to the `meta-blog` layout and also have the `page.author` [liquid variable](../variables/) set to `Dr. Hyde` by default.
### Precedence
You can have multiple sets of frontmatter defaults that specify defaults for the same setting. In this case, for each page or post,
the default set with the more specific scope takes precedence. This way, you can specify defaults for a path like `/site/blog` that would
override any defaults for `/site`. Also, if the paths are equal, a scope with a specified type is more specific. If two sets are equally
specific, the bottom-most takes precedence.
## Default Configuration ## Default Configuration
Jekyll runs with the following configuration options by default. Unless Jekyll runs with the following configuration options by default. Unless

View File

@ -179,3 +179,13 @@ These are available out-of-the-box to be used in the front-matter for a post.
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="note">
<h5>ProTip™: Don't repeat yourself</h5>
<p>
If you don't want to repeat your frequently used front-matter variables over and over,
just define <a href="../configuration/#frontmatter_defaults" title="frontmatter defaults">defaults</a>
for them and only override them where necessary (or not at all). This works both for predefined
and custom variables.
</p>
</div>