diff --git a/bridgetown-builder/lib/bridgetown-builder/dsl/resources.rb b/bridgetown-builder/lib/bridgetown-builder/dsl/resources.rb index 004ee0922..04826cd64 100644 --- a/bridgetown-builder/lib/bridgetown-builder/dsl/resources.rb +++ b/bridgetown-builder/lib/bridgetown-builder/dsl/resources.rb @@ -9,7 +9,7 @@ def resource end def add_resource(collection_name, path, &block) # rubocop:todo Metrics/AbcSize - data = Bridgetown::FrontMatter::RubyFrontMatter.new(scope: self).tap do |fm| + data = Bridgetown::Utils::RubyFrontMatter.new(scope: self).tap do |fm| fm.define_singleton_method(:___) do |hsh| hsh.each do |k, v| fm.set k, v diff --git a/bridgetown-core/lib/bridgetown-core.rb b/bridgetown-core/lib/bridgetown-core.rb index 6b4ec3d22..130fde455 100644 --- a/bridgetown-core/lib/bridgetown-core.rb +++ b/bridgetown-core/lib/bridgetown-core.rb @@ -82,7 +82,8 @@ module Bridgetown autoload :EntryFilter, "bridgetown-core/entry_filter" # TODO: we have too many errors! This is silly autoload :Errors, "bridgetown-core/errors" - autoload :FrontMatter, "bridgetown-core/front_matter" + autoload :FrontmatterDefaults, "bridgetown-core/frontmatter_defaults" + autoload :FrontMatterImporter, "bridgetown-core/concerns/front_matter_importer" autoload :GeneratedPage, "bridgetown-core/generated_page" autoload :Hooks, "bridgetown-core/hooks" autoload :Layout, "bridgetown-core/layout" @@ -109,16 +110,6 @@ 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" - ) - # extensions require "bridgetown-core/commands/registrations" require "bridgetown-core/plugin" diff --git a/bridgetown-core/lib/bridgetown-core/collection.rb b/bridgetown-core/lib/bridgetown-core/collection.rb index fb7910a42..e9eb89d88 100644 --- a/bridgetown-core/lib/bridgetown-core/collection.rb +++ b/bridgetown-core/lib/bridgetown-core/collection.rb @@ -77,7 +77,8 @@ def read next if File.basename(file_path).starts_with?("_") - if label == "data" || FrontMatter::Loaders.front_matter?(full_path) + if label == "data" || Utils.has_yaml_header?(full_path) || + Utils.has_rbfm_header?(full_path) read_resource(full_path) else read_static_file(file_path, full_path) diff --git a/bridgetown-core/lib/bridgetown-core/concerns/front_matter_importer.rb b/bridgetown-core/lib/bridgetown-core/concerns/front_matter_importer.rb new file mode 100644 index 000000000..6bc103edc --- /dev/null +++ b/bridgetown-core/lib/bridgetown-core/concerns/front_matter_importer.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module Bridgetown + module FrontMatterImporter + # Requires klass#content and klass#front_matter_line_count accessors + def self.included(klass) + klass.include Bridgetown::Utils::RubyFrontMatterDSL + end + + YAML_HEADER = %r!\A---\s*\n!.freeze + YAML_BLOCK = %r!\A(---\s*\n.*?\n?)^((---|\.\.\.)\s*$\n?)!m.freeze + RUBY_HEADER = %r!\A[~`#-]{3,}(?:ruby|<%|{%)\s*\n!.freeze + RUBY_BLOCK = + %r!#{RUBY_HEADER.source}(.*?\n?)^((?:%>|%})?[~`#-]{3,}\s*$\n?)!m.freeze + + def read_front_matter(file_path) # rubocop:todo Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength + file_contents = File.read( + file_path, **Bridgetown::Utils.merged_file_read_opts(Bridgetown::Current.site, {}) + ) + yaml_content = file_contents.match(YAML_BLOCK) + if !yaml_content && Bridgetown::Current.site.config.should_execute_inline_ruby? + ruby_content = file_contents.match(RUBY_BLOCK) + end + + if yaml_content + self.content = yaml_content.post_match + self.front_matter_line_count = yaml_content[1].lines.size - 1 + YAMLParser.load(yaml_content[1]) + elsif ruby_content + # rbfm header + content underneath + self.content = ruby_content.post_match + self.front_matter_line_count = ruby_content[1].lines.size + process_ruby_data(ruby_content[1], file_path, 2) + elsif Bridgetown::Utils.has_rbfm_header?(file_path) + process_ruby_data(File.read(file_path).lines[1..].join("\n"), file_path, 2) + 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 + + def process_ruby_data(rubycode, file_path, starting_line) + ruby_data = instance_eval(rubycode, file_path.to_s, starting_line) + ruby_data.is_a?(Array) ? { rows: ruby_data } : ruby_data.to_h + rescue StandardError => e + raise "Ruby code isn't returning an array, or object which responds to `to_h' (#{e.message})" + end + end +end diff --git a/bridgetown-core/lib/bridgetown-core/concerns/site/configurable.rb b/bridgetown-core/lib/bridgetown-core/concerns/site/configurable.rb index bec6c4838..6d7ca86b7 100644 --- a/bridgetown-core/lib/bridgetown-core/concerns/site/configurable.rb +++ b/bridgetown-core/lib/bridgetown-core/concerns/site/configurable.rb @@ -55,13 +55,13 @@ def defaults_reader @defaults_reader ||= Bridgetown::DefaultsReader.new(self) end - # Returns the current instance of {FrontMatter::Defaults} or - # creates a new instance {FrontMatter::Defaults} if it doesn't already exist. + # Returns the current instance of {FrontmatterDefaults} or + # creates a new instance {FrontmatterDefaults} if it doesn't already exist. # - # @return [FrontMatter::Defaults] - # Returns an instance of {FrontMatter::Defaults} + # @return [FrontmatterDefaults] + # Returns an instance of {FrontmatterDefaults} def frontmatter_defaults - @frontmatter_defaults ||= Bridgetown::FrontMatter::Defaults.new(self) + @frontmatter_defaults ||= Bridgetown::FrontmatterDefaults.new(self) end # Prefix a path or paths with the {#root_dir} directory. diff --git a/bridgetown-core/lib/bridgetown-core/configuration/configuration_dsl.rb b/bridgetown-core/lib/bridgetown-core/configuration/configuration_dsl.rb index 437160313..fcd38191f 100644 --- a/bridgetown-core/lib/bridgetown-core/configuration/configuration_dsl.rb +++ b/bridgetown-core/lib/bridgetown-core/configuration/configuration_dsl.rb @@ -2,7 +2,7 @@ module Bridgetown class Configuration - class ConfigurationDSL < Bridgetown::FrontMatter::RubyFrontMatter + class ConfigurationDSL < Bridgetown::Utils::RubyFrontMatter attr_reader :context # @yieldself [ConfigurationDSL] diff --git a/bridgetown-core/lib/bridgetown-core/front_matter.rb b/bridgetown-core/lib/bridgetown-core/front_matter.rb deleted file mode 100644 index 032595b91..000000000 --- a/bridgetown-core/lib/bridgetown-core/front_matter.rb +++ /dev/null @@ -1,11 +0,0 @@ -# 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 diff --git a/bridgetown-core/lib/bridgetown-core/front_matter/defaults.rb b/bridgetown-core/lib/bridgetown-core/front_matter/defaults.rb deleted file mode 100644 index 9c1a5b137..000000000 --- a/bridgetown-core/lib/bridgetown-core/front_matter/defaults.rb +++ /dev/null @@ -1,225 +0,0 @@ -# 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] - 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] - 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 diff --git a/bridgetown-core/lib/bridgetown-core/front_matter/importer.rb b/bridgetown-core/lib/bridgetown-core/front_matter/importer.rb deleted file mode 100644 index c59d00b39..000000000 --- a/bridgetown-core/lib/bridgetown-core/front_matter/importer.rb +++ /dev/null @@ -1,34 +0,0 @@ -# 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 diff --git a/bridgetown-core/lib/bridgetown-core/front_matter/loaders.rb b/bridgetown-core/lib/bridgetown-core/front_matter/loaders.rb deleted file mode 100644 index 6297a2695..000000000 --- a/bridgetown-core/lib/bridgetown-core/front_matter/loaders.rb +++ /dev/null @@ -1,44 +0,0 @@ -# frozen_string_literal: true - -module Bridgetown - module FrontMatter - module Loaders - autoload :Base, "bridgetown-core/front_matter/loaders/base" - autoload :Ruby, "bridgetown-core/front_matter/loaders/ruby" - autoload :YAML, "bridgetown-core/front_matter/loaders/yaml" - - Result = Struct.new(:content, :front_matter, :line_count, keyword_init: true) - - # Constructs a list of possible loaders for a {Model::RepoOrigin} or {Layout} - # - # @param origin_or_layout [Bridgetown::Model::RepoOrigin, Bridgetown::Layout] - # @return [Array] - def self.for(origin_or_layout) - registry.map { |loader_class| loader_class.new(origin_or_layout) } - end - - # Determines whether a given file has front matter - # - # @param path [Pathname, String] the path to the file - # @return [Boolean] true if the file has front matter, false otherwise - def self.front_matter?(file) - registry.any? { |loader_class| loader_class.header?(file) } - end - - # Registers a new type of front matter loader - # - # @param loader_class [Loader::Base] - # @return [void] - def self.register(loader_class) - registry.push(loader_class) - end - - private_class_method def self.registry - @registry ||= [] - end - - register YAML - register Ruby - end - end -end diff --git a/bridgetown-core/lib/bridgetown-core/front_matter/loaders/base.rb b/bridgetown-core/lib/bridgetown-core/front_matter/loaders/base.rb deleted file mode 100644 index bb6717a5b..000000000 --- a/bridgetown-core/lib/bridgetown-core/front_matter/loaders/base.rb +++ /dev/null @@ -1,29 +0,0 @@ -# frozen_string_literal: true - -module Bridgetown - module FrontMatter - module Loaders - # An abstract base class for processing front matter - class Base - # @param origin_or_layout [Bridgetown::Model::RepoOrigin, Bridgetown::Layout] - def initialize(origin_or_layout) - @origin_or_layout = origin_or_layout - end - - # Reads the contents of a file, returning a possible {Result} - # - # @param file_contents [String] the contents of the file being processed - # @param file_path [String] the path to the file being processed - # @return [Result, nil] - def read(file_contents, file_path:) # rubocop:disable Lint/UnusedMethodArgument - raise "Implement #read in a subclass of Bridgetown::FrontMatter::Loaders::Base" - end - - private - - # @return [Bridgetown::Model::RepoOrigin, Bridgetown::Layout] - attr_reader :origin_or_layout - end - end - end -end diff --git a/bridgetown-core/lib/bridgetown-core/front_matter/loaders/ruby.rb b/bridgetown-core/lib/bridgetown-core/front_matter/loaders/ruby.rb deleted file mode 100644 index 2a2cb3b95..000000000 --- a/bridgetown-core/lib/bridgetown-core/front_matter/loaders/ruby.rb +++ /dev/null @@ -1,113 +0,0 @@ -# frozen_string_literal: true - -module Bridgetown - module FrontMatter - module Loaders - # Reads Ruby front matter delineated by fenced code blocks or ERB/Serbea indicators. - # - # For example, all of these resources load the hash `{published: false, - # title: "My post"}` as their front matter. - # - # ~~~ - # ```ruby - # { - # published: false, - # title: My post - # } - # ``` - # ~~~ - # - # ~~~~ - # ~~~ruby - # { - # published: false, - # title: My post - # } - # ~~~ - # ~~~~ - # - # ~~~ - # ###ruby - # { - # published: false, - # title: My post - # } - # ### - # ~~~ - # - # ~~~ - # ---ruby - # { - # published: false, - # title: My post - # } - # --- - # ~~~ - # - # ~~~~ - # ~~~<% - # { - # published: false, - # title: My post - # } - # %>~~~ - # ~~~~ - # - # ~~~~ - # ~~~{% - # { - # published: false, - # title: My post - # } - # %}~~~ - # ~~~~ - class Ruby < Base - HEADER = %r!\A[~`#-]{3,}(?:ruby|<%|{%)\s*\n!.freeze - BLOCK = %r!#{HEADER.source}(.*?\n?)^((?:%>|%})?[~`#-]{3,}\s*$\n?)!m.freeze - - # Determines whether a given file has Ruby front matter - # - # @param file [Pathname, String] the path to the file - # @return [Boolean] true if the file has Ruby front matter, false otherwise - def self.header?(file) - File.open(file, "rb", &:gets)&.match?(HEADER) || false - end - - # @see {Base#read} - def read(file_contents, file_path:) - if (ruby_content = file_contents.match(BLOCK)) && should_execute_inline_ruby? - Result.new( - content: ruby_content.post_match, - front_matter: process_ruby_data(ruby_content[1], file_path, 2), - line_count: ruby_content[1].lines.size - ) - elsif self.class.header?(file_path) - Result.new( - front_matter: process_ruby_data( - File.read(file_path).lines[1..].join("\n"), - file_path, - 2 - ), - line_count: 0 - ) - end - end - - private - - def process_ruby_data(rubycode, file_path, starting_line) - Bridgetown::Utils::RubyExec.process_ruby_data( - @origin_or_layout, - rubycode, - file_path, - starting_line - ) - end - - def should_execute_inline_ruby? - Bridgetown::Current.site.config.should_execute_inline_ruby? - end - end - end - end -end diff --git a/bridgetown-core/lib/bridgetown-core/front_matter/loaders/yaml.rb b/bridgetown-core/lib/bridgetown-core/front_matter/loaders/yaml.rb deleted file mode 100644 index d206579fe..000000000 --- a/bridgetown-core/lib/bridgetown-core/front_matter/loaders/yaml.rb +++ /dev/null @@ -1,42 +0,0 @@ -# frozen_string_literal: true - -module Bridgetown - module FrontMatter - module Loaders - # Reads YAML-formatted front matter delineated by triple hyphens - # - # As an example, this resource loads to the hash `{"published": false, - # "title": "My post"}`. - # - # ~~~ - # --- - # published: false - # title: My post - # --- - # ~~~ - class YAML < Base - HEADER = %r!\A---\s*\n!.freeze - BLOCK = %r!\A(---\s*\n.*?\n?)^((---|\.\.\.)\s*$\n?)!m.freeze - - # Determines whether a given file has YAML front matter - # - # @param file [String] the path to the file - # @return [Boolean] true if the file has YAML front matter, false otherwise - def self.header?(file) - File.open(file, "rb", &:gets)&.match?(HEADER) || false - end - - # @see {Base#read} - def read(file_contents, **) - yaml_content = file_contents.match(BLOCK) or return - - Result.new( - content: yaml_content.post_match, - front_matter: YAMLParser.load(yaml_content[1]), - line_count: yaml_content[1].lines.size - 1 - ) - end - end - end - end -end diff --git a/bridgetown-core/lib/bridgetown-core/frontmatter_defaults.rb b/bridgetown-core/lib/bridgetown-core/frontmatter_defaults.rb new file mode 100644 index 000000000..305767eff --- /dev/null +++ b/bridgetown-core/lib/bridgetown-core/frontmatter_defaults.rb @@ -0,0 +1,223 @@ +# frozen_string_literal: true + +module Bridgetown + # This class handles custom defaults for front matter settings. + # It is exposed via the frontmatter_defaults method on the site class. + class FrontmatterDefaults + # @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] + 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] + 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 diff --git a/bridgetown-core/lib/bridgetown-core/layout.rb b/bridgetown-core/lib/bridgetown-core/layout.rb index cbf1a4882..65efb3b6d 100644 --- a/bridgetown-core/lib/bridgetown-core/layout.rb +++ b/bridgetown-core/lib/bridgetown-core/layout.rb @@ -2,7 +2,7 @@ module Bridgetown class Layout - include FrontMatter::Importer + include FrontMatterImporter include LiquidRenderable # Gets the Site object. diff --git a/bridgetown-core/lib/bridgetown-core/model/repo_origin.rb b/bridgetown-core/lib/bridgetown-core/model/repo_origin.rb index 061c1aaf7..f6e6336ac 100644 --- a/bridgetown-core/lib/bridgetown-core/model/repo_origin.rb +++ b/bridgetown-core/lib/bridgetown-core/model/repo_origin.rb @@ -3,7 +3,13 @@ module Bridgetown module Model class RepoOrigin < Origin - include Bridgetown::FrontMatter::Importer + include Bridgetown::FrontMatterImporter + include Bridgetown::Utils::RubyFrontMatterDSL + + YAML_FRONT_MATTER_REGEXP = %r!\A(---\s*\n.*?\n?)^((---|\.\.\.)\s*$\n?)!m.freeze + RUBY_FRONT_MATTER_HEADER = %r!\A[~`#-]{3,}(?:ruby|<%|{%)\s*\n!.freeze + RUBY_FRONT_MATTER_REGEXP = + %r!#{RUBY_FRONT_MATTER_HEADER.source}(.*?\n?)^((?:%>|%})?[~`#-]{3,}\s*$\n?)!m.freeze # @return [String] attr_accessor :content @@ -123,12 +129,7 @@ def read_file_data # rubocop:todo Metrics/MethodLength, Metrics/CyclomaticComple encoding: site.config["encoding"]).map(&:to_hash), } when ".rb" - Bridgetown::Utils::RubyExec.process_ruby_data( - self, - File.read(original_path), - original_path, - 1 - ) + process_ruby_data(File.read(original_path), original_path, 1) when ".json" json_data = JSON.parse(File.read(original_path)) json_data.is_a?(Array) ? { rows: json_data } : json_data diff --git a/bridgetown-core/lib/bridgetown-core/reader.rb b/bridgetown-core/lib/bridgetown-core/reader.rb index 9c186e8a2..71e1d2323 100644 --- a/bridgetown-core/lib/bridgetown-core/reader.rb +++ b/bridgetown-core/lib/bridgetown-core/reader.rb @@ -69,7 +69,7 @@ def read_directories(dir = "") file_path = @site.in_source_dir(base, entry) if File.directory?(file_path) entries_dirs << entry - elsif FrontMatter::Loaders.front_matter?(file_path) + elsif Utils.has_yaml_header?(file_path) || Utils.has_rbfm_header?(file_path) entries_pages << entry else entries_static_files << entry diff --git a/bridgetown-core/lib/bridgetown-core/readers/plugin_content_reader.rb b/bridgetown-core/lib/bridgetown-core/readers/plugin_content_reader.rb index 16ff10f32..f66d8596e 100644 --- a/bridgetown-core/lib/bridgetown-core/readers/plugin_content_reader.rb +++ b/bridgetown-core/lib/bridgetown-core/readers/plugin_content_reader.rb @@ -31,7 +31,7 @@ def read_content_file(path) dir = File.dirname(path.sub("#{content_dir}/", "")) name = File.basename(path) - @content_files << if FrontMatter::Loaders.front_matter?(path) + @content_files << if Utils.has_yaml_header?(path) || Utils.has_rbfm_header?(path) site.collections.pages.read_resource(path, manifest: manifest) else Bridgetown::StaticFile.new(site, content_dir, "/#{dir}", name) diff --git a/bridgetown-core/lib/bridgetown-core/utils.rb b/bridgetown-core/lib/bridgetown-core/utils.rb index 1ee8216f7..80e61390b 100644 --- a/bridgetown-core/lib/bridgetown-core/utils.rb +++ b/bridgetown-core/lib/bridgetown-core/utils.rb @@ -9,18 +9,10 @@ module Utils # rubocop:todo Metrics/ModuleLength autoload :LoadersManager, "bridgetown-core/utils/loaders_manager" autoload :RequireGems, "bridgetown-core/utils/require_gems" autoload :RubyExec, "bridgetown-core/utils/ruby_exec" + autoload :RubyFrontMatter, "bridgetown-core/utils/ruby_front_matter" + autoload :RubyFrontMatterDSL, "bridgetown-core/utils/ruby_front_matter" autoload :SmartyPantsConverter, "bridgetown-core/utils/smarty_pants_converter" - RubyFrontMatter = ActiveSupport::Deprecation::DeprecatedConstantProxy.new( - "RubyFrontMatter", - "Bridgetown::FrontMatter::RubyFrontMatter" - ) - - RubyFrontMatterDSL = ActiveSupport::Deprecation::DeprecatedConstantProxy.new( - "RubyFrontMatterDSL", - "Bridgetown::FrontMatter::RubyDSL" - ) - # Constants for use in #slugify SLUGIFY_MODES = %w(raw default pretty simple ascii latin).freeze SLUGIFY_RAW_REGEXP = Regexp.new("\\s+").freeze @@ -133,19 +125,11 @@ def parse_date(input, msg = "Input could not be parsed.") # @return [Boolean] if the YAML front matter is present. # rubocop: disable Naming/PredicateName def has_yaml_header?(file) - Bridgetown::Deprecator.deprecation_message( - "Bridgetown::Utils.has_yaml_header? is deprecated, use " \ - "Bridgetown::FrontMatter::Loaders::YAML.header? instead" - ) - FrontMatter::Loaders::YAML.header?(file) + File.open(file, "rb", &:gets)&.match?(Bridgetown::FrontMatterImporter::YAML_HEADER) || false end def has_rbfm_header?(file) - Bridgetown::Deprecator.deprecation_message( - "Bridgetown::Utils.has_rbfm_header? is deprecated, use " \ - "Bridgetown::FrontMatter::Loaders::Ruby.header? instead" - ) - FrontMatter::Loaders::Ruby.header?(file) + File.open(file, "rb", &:gets)&.match?(Bridgetown::FrontMatterImporter::RUBY_HEADER) || false end # Determine whether the given content string contains Liquid Tags or Vaiables diff --git a/bridgetown-core/lib/bridgetown-core/utils/ruby_exec.rb b/bridgetown-core/lib/bridgetown-core/utils/ruby_exec.rb index 6ccd1c963..196ed4233 100644 --- a/bridgetown-core/lib/bridgetown-core/utils/ruby_exec.rb +++ b/bridgetown-core/lib/bridgetown-core/utils/ruby_exec.rb @@ -3,23 +3,6 @@ module Bridgetown module Utils module RubyExec - # @param context [Layout, Model::RepoOrigin] the execution context (i.e. - # `self` for the Ruby code) - # @param ruby_code [String] the Ruby code to execute - # @param file_path [String] the absolute path to the file - # @param starting_line [Integer] the number to list as the starting line - # for compilation errors - # @return [Hash] - def self.process_ruby_data(context, ruby_code, file_path, starting_line) - ruby_data = context.instance_eval(ruby_code, file_path.to_s, starting_line) - ruby_data.is_a?(Array) ? { rows: ruby_data } : ruby_data.to_h - rescue StandardError => e - raise( - "Ruby code isn't returning an array, or object which " \ - "responds to `to_h' (#{e.message})" - ) - end - def self.search_data_for_ruby_code(convertible) return if convertible.data.empty? diff --git a/bridgetown-core/lib/bridgetown-core/front_matter/ruby.rb b/bridgetown-core/lib/bridgetown-core/utils/ruby_front_matter.rb similarity index 96% rename from bridgetown-core/lib/bridgetown-core/front_matter/ruby.rb rename to bridgetown-core/lib/bridgetown-core/utils/ruby_front_matter.rb index 7346ac805..25cdc444d 100644 --- a/bridgetown-core/lib/bridgetown-core/front_matter/ruby.rb +++ b/bridgetown-core/lib/bridgetown-core/utils/ruby_front_matter.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true module Bridgetown - module FrontMatter - module RubyDSL + module Utils + module RubyFrontMatterDSL def front_matter(scope: nil, &block) RubyFrontMatter.new(scope: scope).tap { |fm| fm.instance_exec(&block) } end diff --git a/bridgetown-core/test/front_matter/loaders/test_ruby.rb b/bridgetown-core/test/front_matter/loaders/test_ruby.rb deleted file mode 100644 index ff657e725..000000000 --- a/bridgetown-core/test/front_matter/loaders/test_ruby.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -require "helper" - -module Bridgetown - module FrontMatter - module Loaders - class TestRuby < BridgetownUnitTest - context "The `FrontMatter::Loaders::Ruby.header?` method" do - should "accept files with Ruby front matter" do - file = source_dir("_posts", "2023-06-30-ruby-front-matter.md") - - assert_equal "```ruby", File.open(file, "rb") { |f| f.read(7) } - assert Ruby.header?(file) - end - end - end - end - end -end diff --git a/bridgetown-core/test/front_matter/loaders/test_yaml.rb b/bridgetown-core/test/front_matter/loaders/test_yaml.rb deleted file mode 100644 index 803007621..000000000 --- a/bridgetown-core/test/front_matter/loaders/test_yaml.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -require "helper" - -module Bridgetown - module FrontMatter - module Loaders - class TestYaml < BridgetownUnitTest - context "The `FrontMatter::Loaders::YAML.header?` method" do - should "accept files with YAML front matter" do - file = source_dir("_posts", "2008-10-18-foo-bar.markdown") - - assert_equal "---\n", File.open(file, "rb") { |f| f.read(4) } - assert YAML.header?(file) - end - - should "accept files with extraneous spaces after YAML front matter" do - file = source_dir("_posts", "2015-12-27-extra-spaces.markdown") - - assert_equal "--- \n", File.open(file, "rb") { |f| f.read(6) } - assert YAML.header?(file) - end - - should "reject pgp files and the like which resemble front matter" do - file = source_dir("pgp.key") - - assert_equal "-----B", File.open(file, "rb") { |f| f.read(6) } - refute YAML.header?(file) - end - end - end - end - end -end diff --git a/bridgetown-core/test/source/src/_posts/2023-06-30-ruby-front-matter.md b/bridgetown-core/test/source/src/_posts/2023-06-30-ruby-front-matter.md deleted file mode 100644 index ebbe489ba..000000000 --- a/bridgetown-core/test/source/src/_posts/2023-06-30-ruby-front-matter.md +++ /dev/null @@ -1,7 +0,0 @@ -```ruby -front_matter do - title "Boo" -end -``` - -This is a post with Ruby front matter. diff --git a/bridgetown-core/test/test_generated_site.rb b/bridgetown-core/test/test_generated_site.rb index c36142c7e..d4aa597f6 100644 --- a/bridgetown-core/test/test_generated_site.rb +++ b/bridgetown-core/test/test_generated_site.rb @@ -16,7 +16,7 @@ class TestGeneratedSite < BridgetownUnitTest end should "ensure post count is as expected" do - assert_equal 52, @site.collections.posts.resources.size + assert_equal 51, @site.collections.posts.resources.size end should "insert site.posts into the index" do diff --git a/bridgetown-core/test/test_ruby_helpers.rb b/bridgetown-core/test/test_ruby_helpers.rb index 0973d2ff7..2ab045c3e 100644 --- a/bridgetown-core/test/test_ruby_helpers.rb +++ b/bridgetown-core/test/test_ruby_helpers.rb @@ -26,7 +26,7 @@ def setup end should "return accept objects which respond to url" do - assert_equal "Label", @helpers.link_to("Label", @site.collections.posts.resources.first) + assert_equal "Label", @helpers.link_to("Label", @site.collections.posts.resources.first) end should "pass through relative/absolute URLs" do diff --git a/bridgetown-core/test/test_utils.rb b/bridgetown-core/test/test_utils.rb index 3f5c05fbc..065b8d8a2 100644 --- a/bridgetown-core/test/test_utils.rb +++ b/bridgetown-core/test/test_utils.rb @@ -387,26 +387,20 @@ class TestUtils < BridgetownUnitTest end context "The `Utils.has_yaml_header?` method" do - should "outputs a deprecation message" do + should "accept files with YAML front matter" do file = source_dir("_posts", "2008-10-18-foo-bar.markdown") - - output = capture_output do - Utils.has_yaml_header?(file) - end - - assert_match ".has_yaml_header? is deprecated", output - end - end - - context "The `Utils.has_rbfm_header?` method" do - should "outputs a deprecation message" do - file = source_dir("_posts", "2008-10-18-foo-bar.markdown") - - output = capture_output do - Utils.has_rbfm_header?(file) - end - - assert_match ".has_rbfm_header? is deprecated", output + assert_equal "---\n", File.open(file, "rb") { |f| f.read(4) } + assert Utils.has_yaml_header?(file) + end + should "accept files with extraneous spaces after YAML front matter" do + file = source_dir("_posts", "2015-12-27-extra-spaces.markdown") + assert_equal "--- \n", File.open(file, "rb") { |f| f.read(6) } + assert Utils.has_yaml_header?(file) + end + should "reject pgp files and the like which resemble front matter" do + file = source_dir("pgp.key") + assert_equal "-----B", File.open(file, "rb") { |f| f.read(6) } + refute Utils.has_yaml_header?(file) end end diff --git a/bridgetown-routes/lib/bridgetown-routes/code_blocks.rb b/bridgetown-routes/lib/bridgetown-routes/code_blocks.rb index e02210fc0..3eb57cfdd 100644 --- a/bridgetown-routes/lib/bridgetown-routes/code_blocks.rb +++ b/bridgetown-routes/lib/bridgetown-routes/code_blocks.rb @@ -30,7 +30,7 @@ def eval_route_file(file, file_slug, app) # rubocop:disable Lint/UnusedMethodArg code = File.read(file) code_postmatch = nil - ruby_content = code.match(Bridgetown::FrontMatter::Loaders::Ruby::BLOCK) + ruby_content = code.match(Bridgetown::FrontMatterImporter::RUBY_BLOCK) if ruby_content code = ruby_content[1] code_postmatch = ruby_content.post_match diff --git a/bridgetown-website/src/_docs/front-matter.md b/bridgetown-website/src/_docs/front-matter.md index 20ab0ec54..50b3e33c0 100644 --- a/bridgetown-website/src/_docs/front-matter.md +++ b/bridgetown-website/src/_docs/front-matter.md @@ -314,7 +314,3 @@ As you can see, literally any valid Ruby code has the potential to be transforme {%@ Note type: "warning" do %} For security reasons, please _do not allow_ untrusted content into your repository to be executed in an unsafe environment (aka outside of a Docker container or similar). Just like with custom plugins, a malicious content contributor could potentially introduce harmful code into your site and thus any computer system used to build that site. Enable Ruby Front Matter _only_ if you feel confident in your ability to control and monitor all on-going updates to repository files and data. {% end %} - -## Define custom front matter loaders - -If you're moving your site to Bridgetown from another static site generator, you may already have your front matter in another format. To ease the transition, you can define a new front matter loader using [the front matter loader API](/docs/plugins/front-matter-loaders). diff --git a/bridgetown-website/src/_docs/plugins.md b/bridgetown-website/src/_docs/plugins.md index 43158f15a..277d7b093 100644 --- a/bridgetown-website/src/_docs/plugins.md +++ b/bridgetown-website/src/_docs/plugins.md @@ -184,10 +184,6 @@ Define lambdas which will be run for any matching placeholders within a permalin Add new functionality to the resource objects in your site build. -### [Front Matter Loaders](/docs/plugins/front-matter-loaders) - -Add new types of front matter to the resource objects and layouts in your site. - ### [Commands](/docs/plugins/commands) Commands extend the `bridgetown` executable using the Thor CLI toolkit. diff --git a/bridgetown-website/src/_docs/plugins/front-matter-loaders.md b/bridgetown-website/src/_docs/plugins/front-matter-loaders.md deleted file mode 100644 index d7735a694..000000000 --- a/bridgetown-website/src/_docs/plugins/front-matter-loaders.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -title: Front Matter Loaders -order: 0 -top_section: Configuration -category: plugins ---- - -This API allows you or a third-party gem to augment resources with new types of front matter. To do so, create a new class inheriting from `Bridgetown::FrontMatter::Loaders::Base` that defines an override of the `#read` method, then register it using `Bridgetown::FrontMatter::Loaders.register`. - -Typically, loaders define two constants by convention: - -1. `HEADER` matches the opening line of the front matter -2. `BLOCK` matches the contents of the front matter block with the first capturing group being the content and the regular expression consuming the ending delimiter - -The `#read` method returns a nullable `Bridgetown::FrontMatter::Loaders::Result` with these three attributes: - -1. `content` - the content of the resource without the front matter -2. `front_matter` - the front matter hash after processing the front matter content -3. `line_count` - the number of lines making up the front matter content - -## Limitations - -Currently, front matter loaders process the contents of resources in First-In, First-Out (FIFO) order meaning the built-in loaders take precedence over any new ones. - -This means that loaders should not have overlapping delimiter definitions. Because Bridgetown is flexible in its delimiters — e.g. the YAML loader accepts triple- hyphens, tildes, backticks, or pounds for its code blocks — you must take care when picking delimiters so that multiple loaders do not overlap definitions.