blob: 7cbfa358677a30a55c835a21b75a31db19f98d76 [file] [log] [blame]
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