246 lines
7.3 KiB
Ruby
246 lines
7.3 KiB
Ruby
|
# frozen_string_literal: true
|
||
|
|
||
|
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 reset
|
||
|
@glob_cache = {} if @glob_cache
|
||
|
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 type and path
|
||
|
def applies?(scope, path, type)
|
||
|
applies_type?(scope, type) && applies_path?(scope, path)
|
||
|
end
|
||
|
|
||
|
def applies_path?(scope, path)
|
||
|
return true if !scope.key?("path") || scope["path"].empty?
|
||
|
|
||
|
sanitized_path = Pathname.new(sanitize_path(path))
|
||
|
rel_scope_path = Pathname.new(scope["path"])
|
||
|
|
||
|
if scope["path"].to_s.include?("*")
|
||
|
glob_scope(sanitized_path, rel_scope_path)
|
||
|
else
|
||
|
path_is_subpath?(sanitized_path, strip_collections_dir(scope["path"]))
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def glob_scope(sanitized_path, rel_scope_path)
|
||
|
site_source = Pathname.new(@site.source)
|
||
|
abs_scope_path = site_source.join(rel_scope_path).to_s
|
||
|
|
||
|
glob_cache(abs_scope_path).each do |scope_path|
|
||
|
scope_path = Pathname.new(scope_path).relative_path_from(site_source).to_s
|
||
|
scope_path = strip_collections_dir(scope_path)
|
||
|
Jekyll.logger.debug "Globbed Scope Path:", scope_path
|
||
|
return true if path_is_subpath?(sanitized_path, scope_path)
|
||
|
end
|
||
|
false
|
||
|
end
|
||
|
|
||
|
def glob_cache(path)
|
||
|
@glob_cache ||= {}
|
||
|
@glob_cache[path] ||= Dir.glob(path)
|
||
|
end
|
||
|
|
||
|
def path_is_subpath?(path, parent_path)
|
||
|
path.ascend do |ascended_path|
|
||
|
return true if ascended_path.to_s == parent_path.to_s
|
||
|
end
|
||
|
|
||
|
false
|
||
|
end
|
||
|
|
||
|
def strip_collections_dir(path)
|
||
|
collections_dir = @site.config["collections_dir"]
|
||
|
slashed_coll_dir = collections_dir.empty? ? "/" : "#{collections_dir}/"
|
||
|
return path if collections_dir.empty? || !path.to_s.start_with?(slashed_coll_dir)
|
||
|
|
||
|
path.sub(slashed_coll_dir, "")
|
||
|
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)
|
||
|
@matched_set_cache ||= {}
|
||
|
@matched_set_cache[path] ||= {}
|
||
|
@matched_set_cache[path][type] ||= begin
|
||
|
valid_sets.select do |set|
|
||
|
!set.key?("scope") || applies?(set["scope"], path, type)
|
||
|
end
|
||
|
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
|
||
|
|
||
|
SANITIZATION_REGEX = %r!\A/|(?<=[^/])\z!.freeze
|
||
|
|
||
|
def sanitize_path(path)
|
||
|
if path.nil? || path.empty?
|
||
|
""
|
||
|
else
|
||
|
path.gsub(SANITIZATION_REGEX, "")
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
end
|