| module Jekyll |
| # 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 |
| |
| def update_deprecated_types(set) |
| return set unless set.key?("scope") && set["scope"].key?("type") |
| |
| set["scope"]["type"] = |
| case set["scope"]["type"] |
| when "page" |
| Deprecator.defaults_deprecate_type("page", "pages") |
| "pages" |
| when "post" |
| Deprecator.defaults_deprecate_type("post", "posts") |
| "posts" |
| when "draft" |
| Deprecator.defaults_deprecate_type("draft", "drafts") |
| "drafts" |
| else |
| set["scope"]["type"] |
| end |
| |
| set |
| end |
| |
| def ensure_time!(set) |
| return set unless set.key?("values") && set["values"].key?("date") |
| return set if set["values"]["date"].is_a?(Time) |
| set["values"]["date"] = Utils.parse_date( |
| set["values"]["date"], |
| "An invalid date format was found in a front-matter default set: #{set}" |
| ) |
| set |
| 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"].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 = Utils.deep_merge_hashes(defaults, set["values"]) |
| old_scope = set["scope"] |
| else |
| defaults = Utils.deep_merge_hashes(set["values"], 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.key?("path") || scope["path"].empty? |
| |
| scope_path = Pathname.new(scope["path"]) |
| Pathname.new(sanitize_path(path)).ascend do |ascended_path| |
| if ascended_path.to_s == scope_path.to_s |
| return true |
| end |
| end |
| end |
| |
| # Determines whether the scope applies to type. |
| # The scope applies to the type if: |
| # 1. no 'type' is specified |
| # 2. the 'type' in the scope is the same as the type asked about |
| # |
| # scope - the Hash defaults set being asked about application |
| # type - the type of the document being processed / asked about |
| # its defaults. |
| # |
| # Returns true if either of the above conditions are satisfied, |
| # otherwise returns false |
| def applies_type?(scope, type) |
| !scope.key?("type") || scope["type"].eql?(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["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 |
| # rubocop: disable PredicateName |
| 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.key?("type") |
| true |
| else |
| !old_scope.key? "type" |
| end |
| end |
| # rubocop: enable PredicateName |
| |
| # 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| |
| !set.key?("scope") || 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.map do |set| |
| if valid?(set) |
| ensure_time!(update_deprecated_types(set)) |
| else |
| Jekyll.logger.warn "Defaults:", "An invalid front-matter default set was found:" |
| Jekyll.logger.warn set.to_s |
| nil |
| end |
| end.compact |
| end |
| |
| # Sanitizes the given path by removing a leading and adding a trailing slash |
| def sanitize_path(path) |
| if path.nil? || path.empty? |
| "" |
| else |
| path.gsub(%r!\A/|(?<=[^/])\z!, "".freeze) |
| end |
| end |
| end |
| end |