diff --git a/features/frontmatter_defaults.feature b/features/frontmatter_defaults.feature new file mode 100644 index 00000000..fa9e0a4d --- /dev/null +++ b/features/frontmatter_defaults.feature @@ -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:

just some post

" 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 |

{{page.custom}}

{{page.author}}
| + 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 "

some special data

Ben
" 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:

info on the webpage

" in "_site/2013/10/14/about.html" + And I should see "subfolder:

info on the special section

" 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 diff --git a/lib/jekyll.rb b/lib/jekyll.rb index d7a2c2ea..caa492fe 100644 --- a/lib/jekyll.rb +++ b/lib/jekyll.rb @@ -37,6 +37,7 @@ require 'jekyll/configuration' require 'jekyll/document' require 'jekyll/collection' require 'jekyll/plugin_manager' +require 'jekyll/frontmatter_defaults' require 'jekyll/site' require 'jekyll/convertible' require 'jekyll/url' diff --git a/lib/jekyll/configuration.rb b/lib/jekyll/configuration.rb index ffdb0a22..ca055329 100644 --- a/lib/jekyll/configuration.rb +++ b/lib/jekyll/configuration.rb @@ -46,6 +46,8 @@ module Jekyll 'excerpt_separator' => "\n\n", + 'defaults' => [], + 'maruku' => { 'use_tex' => false, 'use_divs' => false, @@ -251,6 +253,5 @@ module Jekyll config end - end end diff --git a/lib/jekyll/convertible.rb b/lib/jekyll/convertible.rb index 4818328a..cce7460b 100644 --- a/lib/jekyll/convertible.rb +++ b/lib/jekyll/convertible.rb @@ -14,6 +14,8 @@ require 'set' # self.output= # self.name # self.path +# self.type -> :page, :post or :draft + module Jekyll module Convertible # Returns the contents as a String. @@ -107,7 +109,19 @@ module Jekyll further_data = Hash[(attrs || self.class::ATTRIBUTES_FOR_LIQUID).map { |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 # Recursively render layouts diff --git a/lib/jekyll/frontmatter_defaults.rb b/lib/jekyll/frontmatter_defaults.rb new file mode 100644 index 00000000..2c5755b5 --- /dev/null +++ b/lib/jekyll/frontmatter_defaults.rb @@ -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 \ No newline at end of file diff --git a/lib/jekyll/page.rb b/lib/jekyll/page.rb index 5f768cc6..d045b7c2 100644 --- a/lib/jekyll/page.rb +++ b/lib/jekyll/page.rb @@ -28,8 +28,13 @@ module Jekyll @dir = dir @name = name + process(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 # The generated directory into which the page will be placed diff --git a/lib/jekyll/post.rb b/lib/jekyll/post.rb index 2308a33d..8ebe92b5 100644 --- a/lib/jekyll/post.rb +++ b/lib/jekyll/post.rb @@ -56,6 +56,10 @@ module Jekyll process(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') self.date = Time.parse(data["date"].to_s) end @@ -64,6 +68,14 @@ module Jekyll populate_tags end + def published? + if data.has_key?('published') && data['published'] == false + false + else + true + end + end + def populate_categories if categories.empty? self.categories = Utils.pluralized_array_from_hash(data, 'category', 'categories').map {|c| c.to_s.downcase} diff --git a/lib/jekyll/site.rb b/lib/jekyll/site.rb index 43618dc2..51d2be5a 100644 --- a/lib/jekyll/site.rb +++ b/lib/jekyll/site.rb @@ -412,6 +412,10 @@ module Jekyll end end + def frontmatter_defaults + @frontmatter_defaults ||= Configuration::FrontmatterDefaults.new(self) + end + private def has_relative_page? diff --git a/site/docs/configuration.md b/site/docs/configuration.md index 431177af..9051104a 100644 --- a/site/docs/configuration.md +++ b/site/docs/configuration.md @@ -121,6 +121,18 @@ class="flag">flags (specified on the command-line) that control them.

encoding: ENCODING

+ + +

Defaults

+

+ Set defaults for YAML frontmatter + variables. +

+ + +

see below

+ + @@ -264,6 +276,43 @@ before your site is served.

+## 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 Jekyll runs with the following configuration options by default. Unless diff --git a/site/docs/frontmatter.md b/site/docs/frontmatter.md index 19156e03..b9520ff4 100644 --- a/site/docs/frontmatter.md +++ b/site/docs/frontmatter.md @@ -179,3 +179,13 @@ These are available out-of-the-box to be used in the front-matter for a post. + +
+
ProTip™: Don't repeat yourself
+

+ If you don't want to repeat your frequently used front-matter variables over and over, + just define defaults + for them and only override them where necessary (or not at all). This works both for predefined + and custom variables. +

+