Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor front matter handling and extract behavior into loaders #778

Merged
merged 4 commits into from
Feb 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bridgetown-builder/lib/bridgetown-builder/dsl/resources.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ def resource
end

def add_resource(collection_name, path, &block) # rubocop:todo Metrics/AbcSize
data = Bridgetown::Utils::RubyFrontMatter.new(scope: self).tap do |fm|
data = Bridgetown::FrontMatter::RubyFrontMatter.new(scope: self).tap do |fm|
fm.define_singleton_method(:___) do |hsh|
hsh.each do |k, v|
fm.set k, v
Expand Down
13 changes: 11 additions & 2 deletions bridgetown-core/lib/bridgetown-core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,7 @@ module Bridgetown
autoload :EntryFilter, "bridgetown-core/entry_filter"
# TODO: we have too many errors! This is silly
autoload :Errors, "bridgetown-core/errors"
autoload :FrontmatterDefaults, "bridgetown-core/frontmatter_defaults"
autoload :FrontMatterImporter, "bridgetown-core/concerns/front_matter_importer"
autoload :FrontMatter, "bridgetown-core/front_matter"
autoload :GeneratedPage, "bridgetown-core/generated_page"
autoload :Hooks, "bridgetown-core/hooks"
autoload :Layout, "bridgetown-core/layout"
Expand All @@ -110,6 +109,16 @@ module Bridgetown
autoload :Watcher, "bridgetown-core/watcher"
autoload :YAMLParser, "bridgetown-core/yaml_parser"

FrontmatterDefaults = ActiveSupport::Deprecation::DeprecatedConstantProxy.new(
"FrontmatterDefaults",
"Bridgetown::FrontMatter::Defaults"
)

FrontMatterImporter = ActiveSupport::Deprecation::DeprecatedConstantProxy.new(
"FrontMatterImporter",
"Bridgetown::FrontMatter::Importer"
)
Comment on lines +112 to +120
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment: I kept these to honor the move fast but try not to break things principle.

question: These don't use the Bridgetown::Deprecator. Do we want to refactor to use it?


# extensions
require "bridgetown-core/commands/registrations"
require "bridgetown-core/plugin"
Expand Down
3 changes: 1 addition & 2 deletions bridgetown-core/lib/bridgetown-core/collection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,7 @@ def read

next if File.basename(file_path).starts_with?("_")

if label == "data" || Utils.has_yaml_header?(full_path) ||
Utils.has_rbfm_header?(full_path)
if label == "data" || FrontMatter::Loaders.front_matter?(full_path)
read_resource(full_path)
else
read_static_file(file_path, full_path)
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,13 @@ def defaults_reader
@defaults_reader ||= Bridgetown::DefaultsReader.new(self)
end

# Returns the current instance of {FrontmatterDefaults} or
# creates a new instance {FrontmatterDefaults} if it doesn't already exist.
# Returns the current instance of {FrontMatter::Defaults} or
# creates a new instance {FrontMatter::Defaults} if it doesn't already exist.
#
# @return [FrontmatterDefaults]
# Returns an instance of {FrontmatterDefaults}
# @return [FrontMatter::Defaults]
# Returns an instance of {FrontMatter::Defaults}
def frontmatter_defaults
@frontmatter_defaults ||= Bridgetown::FrontmatterDefaults.new(self)
@frontmatter_defaults ||= Bridgetown::FrontMatter::Defaults.new(self)
end

# Prefix a path or paths with the {#root_dir} directory.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

module Bridgetown
class Configuration
class ConfigurationDSL < Bridgetown::Utils::RubyFrontMatter
class ConfigurationDSL < Bridgetown::FrontMatter::RubyFrontMatter
attr_reader :context

# @yieldself [ConfigurationDSL]
Expand Down
11 changes: 11 additions & 0 deletions bridgetown-core/lib/bridgetown-core/front_matter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true

module Bridgetown
module FrontMatter
autoload :Defaults, "bridgetown-core/front_matter/defaults"
autoload :Importer, "bridgetown-core/front_matter/importer"
autoload :Loaders, "bridgetown-core/front_matter/loaders"
autoload :RubyDSL, "bridgetown-core/front_matter/ruby"
autoload :RubyFrontMatter, "bridgetown-core/front_matter/ruby"
end
end
225 changes: 225 additions & 0 deletions bridgetown-core/lib/bridgetown-core/front_matter/defaults.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
# frozen_string_literal: true

module Bridgetown
module FrontMatter
# This class handles custom defaults for front matter settings.
# It is exposed via the frontmatter_defaults method on the site class.
class Defaults
# @return [Bridgetown::Site]
attr_reader :site

def initialize(site)
@site = site
@defaults_cache = {}
end

def reset
@glob_cache = {}
@defaults_cache = {}
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

# Collects a hash with all default values for a resource
#
# @param path [String] the relative path of the resource
# @param collection_name [Symbol] :posts, :pages, etc.
#
# @return [Hash] all default values (an empty hash if there are none)
def all(path, collection_name)
if @defaults_cache.key?([path, collection_name])
return @defaults_cache[[path, collection_name]]
end

defaults = {}
merge_data_cascade_for_path(path, defaults)

old_scope = nil
matching_sets(path, collection_name).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_cache[[path, collection_name]] = defaults
end

private

def merge_data_cascade_for_path(path, merged_data)
absolute_path = site.in_source_dir(path)
site.defaults_reader.path_defaults
.select { |k, _v| absolute_path.include? k }
.sort_by { |k, _v| k.length }
.each do |defaults|
merged_data.merge!(defaults[1])
end
end

# Checks if a given default setting scope matches the given path and collection
#
# scope - the hash indicating the scope, as defined in bridgetown.config.yml
# path - the path to check for
# collection - the collection (:posts or :pages) to check for
#
# Returns true if the scope applies to the given collection and path
def applies?(scope, path, collection)
applies_collection?(scope, collection) && applies_path?(scope, path)
end

def applies_path?(scope, path)
rel_scope_path = scope["path"]
return true if !rel_scope_path.is_a?(String) || rel_scope_path.empty?

sanitized_path = strip_collections_dir(sanitize_path(path))

if rel_scope_path.include?("*")
glob_scope(sanitized_path, rel_scope_path)
else
path_is_subpath?(sanitized_path, strip_collections_dir(rel_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)
Bridgetown.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.start_with?(parent_path)
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 collection.
# The scope applies to the collection if:
# 1. no 'collection' is specified
# 2. the 'collection' in the scope is the same as the collection asked about
#
# @param scope [Hash] the defaults set being asked about
# @param collection [Symbol] the collection of the resource being processed
#
# @return [Boolean] whether either of the above conditions are satisfied
def applies_collection?(scope, collection)
!scope.key?("collection") || scope["collection"].eql?(collection.to_s)
end

# Checks if a given set of default values is valid
#
# @param set [Hash] the default value hash as defined in bridgetown.config.yml
#
# @return [Boolean] if the set is valid and can be used
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 Naming/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?("collection")
true
else
!old_scope.key? "collection"
end
end
# rubocop: enable Naming/PredicateName

# Collects a list of sets that match the given path and collection
#
# @return [Array<Hash>]
def matching_sets(path, collection)
@matched_set_cache ||= {}
@matched_set_cache[path] ||= {}
@matched_set_cache[path][collection] ||= valid_sets.select do |set|
!set.key?("scope") || applies?(set["scope"], path, collection)
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
#
# @return [Array<Hash>]
def valid_sets
sets = site.config["defaults"]
return [] unless sets.is_a?(Array)

sets.filter_map do |set|
if valid?(set)
massage_scope!(set)
# TODO: is this trip really necessary?
ensure_time!(set)
else
Bridgetown.logger.warn "Defaults:", "An invalid front-matter default set was found:"
Bridgetown.logger.warn set.to_s
nil
end
end
end

# Set path to blank if not specified and alias older type to collection
def massage_scope!(set)
set["scope"] ||= {}
set["scope"]["path"] ||= ""
return unless set["scope"]["type"] && !set["scope"]["collection"]

set["scope"]["collection"] = set["scope"]["type"]
end

SANITIZATION_REGEX = %r!\A/|(?<=[^/])\z!.freeze

# 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(SANITIZATION_REGEX, "")
end
end
end
end
end
34 changes: 34 additions & 0 deletions bridgetown-core/lib/bridgetown-core/front_matter/importer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# frozen_string_literal: true

module Bridgetown
module FrontMatter
module Importer
# Requires klass#content and klass#front_matter_line_count accessors
def self.included(klass)
klass.include Bridgetown::FrontMatter::RubyDSL
end

def read_front_matter(file_path)
file_contents = File.read(
file_path, **Bridgetown::Utils.merged_file_read_opts(Bridgetown::Current.site, {})
)
fm_result = nil
Loaders.for(self).each do |loader|
fm_result = loader.read(file_contents, file_path: file_path) and break
end

if fm_result
self.content = fm_result.content
self.front_matter_line_count = fm_result.line_count
fm_result.front_matter
elsif is_a?(Layout)
self.content = file_contents
{}
else
yaml_data = YAMLParser.load_file(file_path)
(yaml_data.is_a?(Array) ? { rows: yaml_data } : yaml_data)
end
end
end
end
end
Loading
Loading