diff --git a/bridgetown-core/lib/bridgetown-core.rb b/bridgetown-core/lib/bridgetown-core.rb index 5d3e858d0..5066d28b5 100644 --- a/bridgetown-core/lib/bridgetown-core.rb +++ b/bridgetown-core/lib/bridgetown-core.rb @@ -93,6 +93,7 @@ module Bridgetown # TODO: this is a poorly named, unclear class. Relocate to Utils: autoload :External, "bridgetown-core/external" autoload :FrontmatterDefaults, "bridgetown-core/frontmatter_defaults" + autoload :FrontMatterImporter, "bridgetown-core/concerns/front_matter_importer" autoload :Hooks, "bridgetown-core/hooks" autoload :Layout, "bridgetown-core/layout" autoload :LayoutPlaceable, "bridgetown-core/concerns/layout_placeable" diff --git a/bridgetown-core/lib/bridgetown-core/collection.rb b/bridgetown-core/lib/bridgetown-core/collection.rb index f3e310719..53979c45a 100644 --- a/bridgetown-core/lib/bridgetown-core/collection.rb +++ b/bridgetown-core/lib/bridgetown-core/collection.rb @@ -78,7 +78,8 @@ def read # rubocop:todo Metrics/PerceivedComplexity, Metrics/CyclomaticComplexit if site.uses_resource? next if File.basename(file_path).starts_with?("_") - if label == "data" || Utils.has_yaml_header?(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) @@ -256,7 +257,7 @@ def merge_data_resources sanitized_segment = sanitize_filename.(File.basename(segment, ".*")) hsh = nested.empty? ? data_contents : data_contents.dig(*nested) hsh[sanitized_segment] = if index == segments.length - 1 - data_resource.data.array || data_resource.data + data_resource.data.rows || data_resource.data else {} end @@ -279,6 +280,16 @@ def merge_environment_specific_metadata(data_contents) data_contents end + # Read in resource from repo path + # @param full_path [String] + def read_resource(full_path) + id = "repo://#{label}.collection/" + Addressable::URI.escape( + Pathname(full_path).relative_path_from(Pathname(site.source)).to_s + ) + resource = Bridgetown::Model::Base.find(id).to_resource.read! + resources << resource if site.unpublished || resource.published? + end + private def container @@ -290,14 +301,6 @@ def read_document(full_path) docs << doc if site.unpublished || doc.published? end - def read_resource(full_path) - id = "file://#{label}.collection/" + Addressable::URI.escape( - Pathname(full_path).relative_path_from(Pathname(site.source)).to_s - ) - resource = Bridgetown::Model::Base.find(id).to_resource.read! - resources << resource if site.unpublished || resource.published? - end - def sort_docs! if metadata["sort_by"].is_a?(String) sort_docs_by_key! 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..fac270140 --- /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 + SafeYAML.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..-1].join("\n"), file_path, 2) + elsif is_a?(Layout) + self.content = file_contents + {} + else + yaml_data = SafeYAML.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/content.rb b/bridgetown-core/lib/bridgetown-core/concerns/site/content.rb index bedee17e7..c7e68e6cf 100644 --- a/bridgetown-core/lib/bridgetown-core/concerns/site/content.rb +++ b/bridgetown-core/lib/bridgetown-core/concerns/site/content.rb @@ -173,9 +173,8 @@ def docs_to_write documents.select(&:write?) end - # Get all documents. - # @return [Array] an array of documents from the - # configuration + # Get all loaded resources. + # @return [Array] an array of resources def resources collections.each_with_object(Set.new) do |(_, collection), set| set.merge(collection.resources) diff --git a/bridgetown-core/lib/bridgetown-core/concerns/validatable.rb b/bridgetown-core/lib/bridgetown-core/concerns/validatable.rb index 81bfb552d..b3b79840e 100644 --- a/bridgetown-core/lib/bridgetown-core/concerns/validatable.rb +++ b/bridgetown-core/lib/bridgetown-core/concerns/validatable.rb @@ -3,10 +3,6 @@ module Bridgetown # TODO: to be retired once the Resource engine is made official module Validatable - # FIXME: there should be ONE TRUE METHOD to read the YAML frontmatter - # in the entire project. Both this and the equivalent Document method - # should be extracted and generalized. - # # Read the YAML frontmatter. # # base - The String path to the dir containing the file. diff --git a/bridgetown-core/lib/bridgetown-core/configuration.rb b/bridgetown-core/lib/bridgetown-core/configuration.rb index e5f3dbb4a..f836e6334 100644 --- a/bridgetown-core/lib/bridgetown-core/configuration.rb +++ b/bridgetown-core/lib/bridgetown-core/configuration.rb @@ -74,15 +74,16 @@ class Configuration < HashWithDotAccess::Hash }, "kramdown" => { - "auto_ids" => true, - "toc_levels" => (1..6).to_a, - "entity_output" => "as_char", - "smart_quotes" => "lsquo,rsquo,ldquo,rdquo", - "input" => "GFM", - "hard_wrap" => false, - "guess_lang" => true, - "footnote_nr" => 1, - "show_warnings" => false, + "auto_ids" => true, + "toc_levels" => (1..6).to_a, + "entity_output" => "as_char", + "smart_quotes" => "lsquo,rsquo,ldquo,rdquo", + "input" => "GFM", + "hard_wrap" => false, + "guess_lang" => true, + "footnote_nr" => 1, + "show_warnings" => false, + "include_extraction_tags" => false, }, }.each_with_object(Configuration.new) { |(k, v), hsh| hsh[k] = v.freeze }.freeze diff --git a/bridgetown-core/lib/bridgetown-core/converter.rb b/bridgetown-core/lib/bridgetown-core/converter.rb index ac38cc948..b0a60d3b1 100644 --- a/bridgetown-core/lib/bridgetown-core/converter.rb +++ b/bridgetown-core/lib/bridgetown-core/converter.rb @@ -53,6 +53,15 @@ def output_ext(_ext) ".html" end + def line_start(convertible) + if convertible.is_a?(Bridgetown::Resource::Base) && + convertible.model.origin.respond_to?(:front_matter_line_count) + convertible.model.origin.front_matter_line_count + 4 + else + 1 + end + end + def inspect "#<#{self.class}#{self.class.extname_list ? " #{self.class.extname_list.join(", ")}" : nil}>" end diff --git a/bridgetown-core/lib/bridgetown-core/converters/erb_templates.rb b/bridgetown-core/lib/bridgetown-core/converters/erb_templates.rb index 2d7b9a8ba..b92c575ab 100644 --- a/bridgetown-core/lib/bridgetown-core/converters/erb_templates.rb +++ b/bridgetown-core/lib/bridgetown-core/converters/erb_templates.rb @@ -115,6 +115,7 @@ def convert(content, convertible) erb_renderer = Tilt::ErubiTemplate.new( convertible.relative_path, + line_start(convertible), outvar: "@_erbout", bufval: "Bridgetown::OutputBuffer.new", engine_class: ERBEngine diff --git a/bridgetown-core/lib/bridgetown-core/converters/markdown.rb b/bridgetown-core/lib/bridgetown-core/converters/markdown.rb index 12321f094..77ea685e8 100644 --- a/bridgetown-core/lib/bridgetown-core/converters/markdown.rb +++ b/bridgetown-core/lib/bridgetown-core/converters/markdown.rb @@ -69,7 +69,7 @@ def convert(content, convertible = nil) end else output = @parser.convert(content) - if @parser.respond_to?(:extractions) + if convertible && @parser.respond_to?(:extractions) convertible.data.markdown_extractions = @parser.extractions end output diff --git a/bridgetown-core/lib/bridgetown-core/converters/ruby_templates.rb b/bridgetown-core/lib/bridgetown-core/converters/ruby_templates.rb new file mode 100644 index 000000000..959d8ea04 --- /dev/null +++ b/bridgetown-core/lib/bridgetown-core/converters/ruby_templates.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Bridgetown + module Converters + class RubyTemplates < Converter + priority :highest + input :rb + + def convert(content, convertible) + erb_view = Bridgetown::ERBView.new(convertible) + erb_view.instance_eval( + content, convertible.relative_path.to_s, line_start(convertible) + ).to_s + end + end + end +end diff --git a/bridgetown-core/lib/bridgetown-core/drops/resource_drop.rb b/bridgetown-core/lib/bridgetown-core/drops/resource_drop.rb index 3f6119b99..dac19672a 100644 --- a/bridgetown-core/lib/bridgetown-core/drops/resource_drop.rb +++ b/bridgetown-core/lib/bridgetown-core/drops/resource_drop.rb @@ -14,6 +14,7 @@ class ResourceDrop < Drop def_delegator :@obj, :relative_path, :path def_delegators :@obj, :id, + :data, :output, :content, :to_s, diff --git a/bridgetown-core/lib/bridgetown-core/drops/unified_payload_drop.rb b/bridgetown-core/lib/bridgetown-core/drops/unified_payload_drop.rb index 9516936ff..314925e79 100644 --- a/bridgetown-core/lib/bridgetown-core/drops/unified_payload_drop.rb +++ b/bridgetown-core/lib/bridgetown-core/drops/unified_payload_drop.rb @@ -6,6 +6,7 @@ class UnifiedPayloadDrop < Drop mutable true attr_accessor :page, :layout, :content, :paginator + alias_method :resource, :page def bridgetown BridgetownDrop.global diff --git a/bridgetown-core/lib/bridgetown-core/layout.rb b/bridgetown-core/lib/bridgetown-core/layout.rb index d99da2bd5..d25ed9b98 100644 --- a/bridgetown-core/lib/bridgetown-core/layout.rb +++ b/bridgetown-core/lib/bridgetown-core/layout.rb @@ -3,8 +3,8 @@ module Bridgetown class Layout include DataAccessible + include FrontMatterImporter include LiquidRenderable - include Validatable # Gets the Site object. attr_reader :site @@ -27,8 +27,12 @@ class Layout attr_accessor :data # Gets/Sets the content of this layout. + # @return [String] attr_accessor :content + # @return [Integer] + attr_accessor :front_matter_line_count + # Gets/Sets the current document (for layout-compatible converters) attr_accessor :current_document @@ -54,9 +58,29 @@ def initialize(site, base, name, from_plugin: false) @path = site.in_source_dir(base, name) end @relative_path = @path.sub(@base_dir, "") + @ext = File.extname(name) + + @data = read_front_matter(@path)&.with_dot_access + rescue SyntaxError => e + Bridgetown.logger.error "Error:", + "Ruby Exception in #{e.message}" + rescue StandardError => e + handle_read_error(e) + ensure + @data ||= HashWithDotAccess::Hash.new + end + + def handle_read_error(error) + if error.is_a? Psych::SyntaxError + Bridgetown.logger.warn "YAML Exception reading #{@path}: #{error.message}" + else + Bridgetown.logger.warn "Error reading file #{@path}: #{error.message}" + end - process(name) - read_yaml(base, name) + if site.config["strict_front_matter"] || + error.is_a?(Bridgetown::Errors::FatalException) + raise error + end end # The inspect string for this document. @@ -67,15 +91,6 @@ def inspect "#<#{self.class} #{@path}>" end - # Extract information from the layout filename. - # - # name - The String filename of the layout file. - # - # Returns nothing. - def process(name) - self.ext = File.extname(name) - end - # Provide this Layout's data to a Hash suitable for use by Liquid. # # Returns the Hash representation of this Layout. diff --git a/bridgetown-core/lib/bridgetown-core/model/origin.rb b/bridgetown-core/lib/bridgetown-core/model/origin.rb index 5e36b2317..280bf3b38 100644 --- a/bridgetown-core/lib/bridgetown-core/model/origin.rb +++ b/bridgetown-core/lib/bridgetown-core/model/origin.rb @@ -35,4 +35,4 @@ def exists? end require "bridgetown-core/model/builder_origin" -require "bridgetown-core/model/file_origin" +require "bridgetown-core/model/repo_origin" diff --git a/bridgetown-core/lib/bridgetown-core/model/file_origin.rb b/bridgetown-core/lib/bridgetown-core/model/repo_origin.rb similarity index 68% rename from bridgetown-core/lib/bridgetown-core/model/file_origin.rb rename to bridgetown-core/lib/bridgetown-core/model/repo_origin.rb index dcaa52e6f..1b672a968 100644 --- a/bridgetown-core/lib/bridgetown-core/model/file_origin.rb +++ b/bridgetown-core/lib/bridgetown-core/model/repo_origin.rb @@ -2,29 +2,48 @@ module Bridgetown module Model - class FileOrigin < Origin + class RepoOrigin < Origin + 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 + + # @return [Integer] + attr_accessor :front_matter_line_count class << self def handle_scheme?(scheme) - scheme == "file" + scheme == "repo" end def data_file_extensions - %w(.yaml .yml .json .csv .tsv).freeze + %w(.yaml .yml .json .csv .tsv .rb).freeze end end def read - @data = (in_data_collection? ? read_file_data : read_frontmatter) || {} + begin + @data = (in_data_collection? ? read_file_data : read_front_matter(original_path)) || {} + rescue SyntaxError => e + Bridgetown.logger.error "Error:", + "Ruby Exception in #{e.message}" + rescue StandardError => e + handle_read_error(e) + end + + @data ||= {} @data[:_id_] = id @data[:_origin_] = self @data[:_collection_] = collection - @data[:_content_] = @content if @content + @data[:_content_] = content if content @data - rescue StandardError => e - handle_read_error(e) end def url @@ -63,40 +82,28 @@ def in_data_collection? collection.data? end - def read_file_data + def read_file_data # rubocop:todo Metrics/MethodLength case original_path.extname.downcase when ".csv" { - array: + rows: CSV.read(original_path, headers: true, encoding: Bridgetown::Current.site.config["encoding"]).map(&:to_hash), } when ".tsv" { - array: + rows: CSV.read(original_path, col_sep: "\t", headers: true, encoding: Bridgetown::Current.site.config["encoding"]).map(&:to_hash), } + when ".rb" + process_ruby_data(File.read(original_path), original_path, 1) else yaml_data = SafeYAML.load_file(original_path) - yaml_data.is_a?(Array) ? { array: yaml_data } : yaml_data - end - end - - def read_frontmatter - @content = File.read( - original_path, **Bridgetown::Utils.merged_file_read_opts(Bridgetown::Current.site, {}) - ) - content_match = @content.match(YAML_FRONT_MATTER_REGEXP) - if content_match - @content = content_match.post_match - SafeYAML.load(content_match[1]) - else - yaml_data = SafeYAML.load_file(original_path) - yaml_data.is_a?(Array) ? { array: yaml_data } : yaml_data + yaml_data.is_a?(Array) ? { rows: yaml_data } : yaml_data end end diff --git a/bridgetown-core/lib/bridgetown-core/reader.rb b/bridgetown-core/lib/bridgetown-core/reader.rb index e9df4d46b..50a7df2d6 100644 --- a/bridgetown-core/lib/bridgetown-core/reader.rb +++ b/bridgetown-core/lib/bridgetown-core/reader.rb @@ -63,7 +63,7 @@ def read_directories(dir = "") file_path = @site.in_source_dir(base, entry) if File.directory?(file_path) dot_dirs << entry - elsif Utils.has_yaml_header?(file_path) + elsif Utils.has_yaml_header?(file_path) || Utils.has_rbfm_header?(file_path) dot_pages << entry else dot_static_files << entry @@ -113,7 +113,7 @@ def retrieve_dirs(_base, dir, dot_dirs) def retrieve_pages(dir, dot_pages) if site.uses_resource? dot_pages.each do |page_path| - site.collections.pages.send(:read_resource, site.in_source_dir(dir, page_path)) + site.collections.pages.read_resource(site.in_source_dir(dir, page_path)) end return end diff --git a/bridgetown-core/lib/bridgetown-core/resource/base.rb b/bridgetown-core/lib/bridgetown-core/resource/base.rb index 8c0a35743..7495792ab 100644 --- a/bridgetown-core/lib/bridgetown-core/resource/base.rb +++ b/bridgetown-core/lib/bridgetown-core/resource/base.rb @@ -82,9 +82,11 @@ def read! unless collection.data? self.untransformed_content = content - determine_slug_and_date normalize_categories_and_tags import_taxonomies_from_data + ensure_default_data + transformer.execute_inline_ruby! + set_date_from_string(data.date) end @destination = Destination.new(self) if requires_destination? @@ -96,7 +98,7 @@ def read! alias_method :read, :read! # TODO: eventually use the bang version only def transform! - transformer.process! unless collection.data? + transformer.process! if output_allowed? end def trigger_hooks(hook_name, *args) @@ -160,7 +162,7 @@ def id end def date - data["date"] ||= site.time # TODO: this doesn't reflect documented behavior + data["date"] ||= site.time end # @return [Hash Bridgetown::Resource::TaxonomyType, @@ -176,8 +178,12 @@ def taxonomies end end + def output_allowed? + !collection.data? && data.config&.output != false + end + def requires_destination? - collection.write? && data.config&.output != false + collection.write? && output_allowed? end def write? @@ -205,8 +211,31 @@ def to_liquid @to_liquid ||= Drops::ResourceDrop.new(self) end + def to_h + { + id: id, + absolute_url: absolute_url, + relative_path: relative_path, + relative_url: relative_url, + date: date, + data: data, + taxonomies: taxonomies, + untransformed_content: untransformed_content, + content: content, + output: output, + } + end + + def as_json(*) + to_h + end + + ruby2_keywords def to_json(*options) + as_json(*options).to_json(*options) + end + def inspect - "#<#{self.class} [#{collection.label}] #{relative_path}>" + "#<#{self.class} #{id}>" end # Compare this document against another document. @@ -240,23 +269,25 @@ def previous_resource private - def determine_slug_and_date - return unless relative_path.to_s =~ DATE_FILENAME_MATCHER - - new_date, slug = Regexp.last_match.captures - modify_date(new_date) + def ensure_default_data + slug = if matches = relative_path.to_s.match(DATE_FILENAME_MATCHER) # rubocop:disable Lint/AssignmentInCondition + set_date_from_string(matches[1]) unless data.date + matches[2] + else + basename_without_ext + end - slug.gsub!(%r!\.*\z!, "") data.slug ||= slug + data.title ||= Bridgetown::Utils.titleize_slug(slug) end - def modify_date(new_date) - if !data.date || data.date.to_i == site.time.to_i - data.date = Utils.parse_date( - new_date, - "Document '#{relative_path}' does not have a valid date in the #{model}." - ) - end + def set_date_from_string(new_date) # rubocop:disable Naming/AccessorMethodName + return unless new_date.is_a?(String) + + data.date = Bridgetown::Utils.parse_date( + new_date, + "Document '#{relative_path}' does not have a valid date in the #{model}." + ) end def normalize_categories_and_tags diff --git a/bridgetown-core/lib/bridgetown-core/resource/taxonomy_term.rb b/bridgetown-core/lib/bridgetown-core/resource/taxonomy_term.rb index 40863080f..b75816ddd 100644 --- a/bridgetown-core/lib/bridgetown-core/resource/taxonomy_term.rb +++ b/bridgetown-core/lib/bridgetown-core/resource/taxonomy_term.rb @@ -17,9 +17,18 @@ def initialize(resource:, label:, type:) def to_liquid { - label: label, + "label" => label, } end + alias_method :to_h, :to_liquid + + def as_json(*) + to_h + end + + ruby2_keywords def to_json(*options) + as_json(*options).to_json(*options) + end end end end diff --git a/bridgetown-core/lib/bridgetown-core/resource/taxonomy_type.rb b/bridgetown-core/lib/bridgetown-core/resource/taxonomy_type.rb index 8056775ef..7a0d5c90f 100644 --- a/bridgetown-core/lib/bridgetown-core/resource/taxonomy_type.rb +++ b/bridgetown-core/lib/bridgetown-core/resource/taxonomy_type.rb @@ -42,6 +42,15 @@ def to_liquid "metadata" => metadata, } end + alias_method :to_h, :to_liquid + + def as_json(*) + to_h + end + + ruby2_keywords def to_json(*options) + as_json(*options).to_json(*options) + end end end end diff --git a/bridgetown-core/lib/bridgetown-core/resource/transformer.rb b/bridgetown-core/lib/bridgetown-core/resource/transformer.rb index 8206c3873..48970c16f 100644 --- a/bridgetown-core/lib/bridgetown-core/resource/transformer.rb +++ b/bridgetown-core/lib/bridgetown-core/resource/transformer.rb @@ -12,18 +12,20 @@ class Transformer # @return [Bridgetown::Site] attr_reader :site - # @return [String] - attr_reader :output_ext - def initialize(resource) @resource = resource @site = resource.site - execute_inline_ruby - @output_ext = output_ext_from_converters + end + + # @return [String] + def output_ext + @output_ext ||= output_ext_from_converters end # @return [String] def final_ext + output_ext # we always need this to get run + permalink_ext || output_ext end @@ -35,6 +37,12 @@ def process! end end + def execute_inline_ruby! + return unless site.config.should_execute_inline_ruby? + + Bridgetown::Utils::RubyExec.search_data_for_ruby_code(resource, self) + end + def inspect "#<#{self.class} Conversion Steps: #{conversions.length}>" end @@ -102,12 +110,6 @@ def warn_on_missing_layout(layout, layout_name) ### Transformation Actions - def execute_inline_ruby - return unless site.config.should_execute_inline_ruby? - - Bridgetown::Utils::RubyExec.search_data_for_ruby_code(resource, self) - end - def run_conversions # rubocop:disable Metrics/AbcSize input = resource.content.to_s diff --git a/bridgetown-core/lib/bridgetown-core/utils.rb b/bridgetown-core/lib/bridgetown-core/utils.rb index ef56cd8bb..eca42fdc9 100644 --- a/bridgetown-core/lib/bridgetown-core/utils.rb +++ b/bridgetown-core/lib/bridgetown-core/utils.rb @@ -5,6 +5,7 @@ module Utils extend self autoload :Ansi, "bridgetown-core/utils/ansi" autoload :RubyExec, "bridgetown-core/utils/ruby_exec" + autoload :RubyFrontMatterDSL, "bridgetown-core/utils/ruby_front_matter" autoload :Platforms, "bridgetown-core/utils/platforms" autoload :ThreadEvent, "bridgetown-core/utils/thread_event" @@ -118,7 +119,13 @@ 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) - File.open(file, "rb", &:readline).match? %r!\A---\s*\r?\n! + File.open(file, "rb", &:readline).match? Bridgetown::FrontMatterImporter::YAML_HEADER + rescue EOFError + false + end + + def has_rbfm_header?(file) + File.open(file, "rb", &:readline).match? Bridgetown::FrontMatterImporter::RUBY_HEADER rescue EOFError false end diff --git a/bridgetown-core/lib/bridgetown-core/utils/ruby_exec.rb b/bridgetown-core/lib/bridgetown-core/utils/ruby_exec.rb index a9cf37dee..d979fda82 100644 --- a/bridgetown-core/lib/bridgetown-core/utils/ruby_exec.rb +++ b/bridgetown-core/lib/bridgetown-core/utils/ruby_exec.rb @@ -5,8 +5,8 @@ module Utils module RubyExec extend self - # rubocop:disable Metrics/AbcSize - def search_data_for_ruby_code(convertible, renderer) + # TODO: Deprecate storing Ruby code in YAML, Rb, etc. and just use native Ruby Front Matter + def search_data_for_ruby_code(convertible, renderer) # rubocop:todo Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity return if convertible.data.empty? # Iterate using `keys` here so inline Ruby script can add new data keys @@ -14,24 +14,21 @@ def search_data_for_ruby_code(convertible, renderer) data_keys = convertible.data.keys data_keys.each do |k| v = convertible.data[k] - next unless v.is_a?(Rb) || v.is_a?(Hash) + next unless v.is_a?(Rb) || v.is_a?(Hash) || v.is_a?(Proc) - if v.is_a?(Hash) + if v.is_a?(Proc) + convertible.data[k] = convertible.instance_exec(&v) + elsif v.is_a?(Hash) v.each do |nested_k, nested_v| next unless nested_v.is_a?(Rb) - Bridgetown.logger.debug("Executing inline Ruby…", convertible.relative_path) convertible.data[k][nested_k] = run(nested_v, convertible, renderer) - Bridgetown.logger.debug("Inline Ruby completed!", convertible.relative_path) end else - Bridgetown.logger.debug("Executing inline Ruby…", convertible.relative_path) convertible.data[k] = run(v, convertible, renderer) - Bridgetown.logger.debug("Inline Ruby completed!", convertible.relative_path) end end end - # rubocop:enable Metrics/AbcSize # Sets up a new context in which to eval Ruby coming from front matter. # diff --git a/bridgetown-core/lib/bridgetown-core/utils/ruby_front_matter.rb b/bridgetown-core/lib/bridgetown-core/utils/ruby_front_matter.rb new file mode 100644 index 000000000..4b00e3cc1 --- /dev/null +++ b/bridgetown-core/lib/bridgetown-core/utils/ruby_front_matter.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Bridgetown + module Utils + module RubyFrontMatterDSL + def front_matter(&block) + RubyFrontMatter.new.tap { |fm| fm.instance_exec(&block) } + end + end + + class RubyFrontMatter + def initialize + @data = {} + end + + def method_missing(key, value) # rubocop:disable Style/MissingRespondToMissing + return super if respond_to?(key) + + set(key, value) + end + + def each(&block) + @data.each(&block) + end + + def get(key) + @data[key] + end + + def set(key, value) + @data[key] = value + end + + def to_h + @data + end + end + end +end diff --git a/bridgetown-core/test/resources/src/_data/languages.rb b/bridgetown-core/test/resources/src/_data/languages.rb new file mode 100644 index 000000000..c1e3b0634 --- /dev/null +++ b/bridgetown-core/test/resources/src/_data/languages.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +%w[java ruby] diff --git a/bridgetown-core/test/resources/src/_data/languages.yml b/bridgetown-core/test/resources/src/_data/languages.yml deleted file mode 100644 index 6b0a250c0..000000000 --- a/bridgetown-core/test/resources/src/_data/languages.yml +++ /dev/null @@ -1,2 +0,0 @@ -- java -- ruby diff --git a/bridgetown-core/test/resources/src/_events/2020-12-25-christmas.erb b/bridgetown-core/test/resources/src/_events/2020-12-25-christmas.erb index 466047622..af6ad61c8 100644 --- a/bridgetown-core/test/resources/src/_events/2020-12-25-christmas.erb +++ b/bridgetown-core/test/resources/src/_events/2020-12-25-christmas.erb @@ -1,5 +1,8 @@ ---- -title: Christmas 2020 ---- +---<% +front_matter do + old_title "Christmas 2019" + title get(:old_title).sub("2019", "2020") +end +%>--- Fa <%= 8.times.map { "la" }.join(" ") %>! \ No newline at end of file diff --git a/bridgetown-core/test/resources/src/_pages/second-level-page.md b/bridgetown-core/test/resources/src/_pages/second-level-page.md index 6653550f9..2b4a69a84 100644 --- a/bridgetown-core/test/resources/src/_pages/second-level-page.md +++ b/bridgetown-core/test/resources/src/_pages/second-level-page.md @@ -1,5 +1,5 @@ ---- -title: I'm a Second Level Page ---- +~~~ruby +{ title: "I'm a Second Level Page" } +~~~ That's **nice**. \ No newline at end of file diff --git "a/bridgetown-core/test/resources/src/_posts/pass-thru/2019-09-09-bl\303\270g P\303\266st.md" "b/bridgetown-core/test/resources/src/_posts/pass-thru/2019-09-09-bl\303\270g P\303\266st.md" index 3e659b9cc..799bbc7e5 100644 --- "a/bridgetown-core/test/resources/src/_posts/pass-thru/2019-09-09-bl\303\270g P\303\266st.md" +++ "b/bridgetown-core/test/resources/src/_posts/pass-thru/2019-09-09-bl\303\270g P\303\266st.md" @@ -1,6 +1,8 @@ ---- -title: I'm a bløg pöst? 😄 -noodle: ramen ---- +```ruby +fm = Struct.new(:title, :noodle).new +fm.title = "I'm a bløg pöst? 😄" +fm.noodle = "ramen" +fm +``` W00t! \ No newline at end of file diff --git a/bridgetown-core/test/test_resource.rb b/bridgetown-core/test/test_resource.rb index bda8943c4..997fb1aca 100644 --- a/bridgetown-core/test/test_resource.rb +++ b/bridgetown-core/test/test_resource.rb @@ -99,6 +99,7 @@ class TestResource < BridgetownUnitTest end should "have transformed content" do + assert_equal "Christmas 2020", @resource.data.title assert_equal "Fa la la la la la la la la!", @resource.content.strip end end @@ -183,6 +184,10 @@ class TestResource < BridgetownUnitTest should "have a fancy title" do assert_equal "I'm a bløg pöst? 😄", @resource.data.title end + + should "include content" do + assert_equal "

W00t!

\n", @resource.content + end end context "a resource in the posts collection" do @@ -251,4 +256,12 @@ class TestResource < BridgetownUnitTest assert_equal 5.3, @site.data.categories.dairy.products.first.price end end + + context "a Ruby data resource" do + should "provide an array" do + @site = resources_site + @site.process + assert_equal "ruby", @site.data.languages[1] + end + end end diff --git a/bridgetown-website/src/_docs/datafiles.md b/bridgetown-website/src/_docs/datafiles.md index 729a883e0..a19ad85d2 100644 --- a/bridgetown-website/src/_docs/datafiles.md +++ b/bridgetown-website/src/_docs/datafiles.md @@ -5,31 +5,36 @@ top_section: Content category: datafiles --- -In addition to the [built-in variables]({{'/docs/variables/' | relative_url }}) available from Bridgetown, -you can specify your own custom data that can be accessed via Liquid. +In addition to [built-in variables](/docs/variables) and [front matter](/docs/front-matter), you can specify custom datasets which are accessible via Liquid and Ruby templates as well as plugins. -Bridgetown supports loading data from [YAML](http://yaml.org/), [JSON](http://www.json.org/), [CSV](https://en.wikipedia.org/wiki/Comma-separated_values), and [TSV](https://en.wikipedia.org/wiki/Tab-separated_values) files located in the `_data` folder. -Note that CSV and TSV files *must* contain a header row. +Bridgetown supports loading data from [YAML](http://yaml.org/), [JSON](http://www.json.org/), [CSV](https://en.wikipedia.org/wiki/Comma-separated_values), and [TSV](https://en.wikipedia.org/wiki/Tab-separated_values) files located in the `src/_data` folder. Note that CSV and TSV files *must* contain a header row. -This powerful feature allows you to avoid repetition in your templates and to -set site specific options without changing `bridgetown.config.yml`. +And starting in Bridgetown v0.21, you can save standard Ruby files (`.rb`) to `_data` which get automatically evaluated. The return value at the end of the file can either be an array or any object which responds to `to_h` (and thus returns a `Hash`). (Requires you to be using the [Resource content engine](/docs/resources).) + +This powerful feature allows you to avoid repetition in your templates and set site-specific options without changing `bridgetown.config.yml`—and in the case of Ruby data files, perform powerful processing tasks to populate your site content. {% toc %} ## The Data Folder -The `_data` folder is where you can store additional data for Bridgetown to use when -generating your site. These files must be YAML, JSON, or CSV files (using either -the `.yml`, `.yaml`, `.json` or `.csv` extension), and they will be -accessible via `site.data`. +The `_data` folder is where you can save YAML, JSON, or CSV files (using either the `.yml`, `.yaml`, `.json` or `.csv` extension), and they will be accessible via `site.data`. In Bridgetown 0.21 and later, any files ending in `.rb` within the data folder will be evaluated as Ruby code when using the Resource content engine. ## The Metadata File -You can store site-wide metadata variables in `_data/site_metadata.yml` so -they'll be easy to access and will regenerate pages when changed. This is a good -place to put `` content like your website title, description, favicon, social media handles, etc. Then you can reference {{ site.metadata.title }}, etc. in your Liquid templates. +You can store site-wide metadata variables in `_data/site_metadata.yml` so they'll be easy to access and will regenerate pages when changed. This is a good place to put `` content like your website title, description, favicon, social media handles, etc. Then you can reference `site.metadata.title`, etc. in your Liquid and Ruby templates. + +Want to switch to using a `site_metadata.rb` file where you have more programmatic control over the data values, can easily load in `ENV` variable, etc.? Now you can! For example: + +```ruby +# src/_data/site_metadata.rb +{ + title: "Your Ruby Website", + lang: ENV["LANG"], + tagline: "All we need is Ruby" +} +``` -## Example: List of members +## Example: Define a List of members Here is a basic example of using Data Files to avoid copy-pasting large chunks of code in your Bridgetown templates: diff --git a/bridgetown-website/src/_docs/front-matter.md b/bridgetown-website/src/_docs/front-matter.md index a786e9f07..987bb7c23 100644 --- a/bridgetown-website/src/_docs/front-matter.md +++ b/bridgetown-website/src/_docs/front-matter.md @@ -5,12 +5,7 @@ top_section: Content category: front-matter --- -Front matter is a snippet of [YAML](https://yaml.org/) which sits between two -triple-dashed lines at the top of a file. You use front matter to add metadata, -like a title or a description, to files such as pages and documents as well as site -layouts. Front matter can be used in various ways to set configuration options -on a per-file basis, and starting with Bridgetown v0.13, you can even write Ruby -code for dynamic front matter variables. +Front matter is a snippet of [YAML](https://yaml.org/) or Ruby data which sits at the top of a file between special line delimiters. You can think of front matter as a datastore consisting of one or more key-value pairs (aka a `Hash` in Ruby). You use front matter to add metadata, like a title or a description, to files such as pages and documents as well as site layouts. Front matter can be used in various ways to set configuration options on a per-file basis, and if you need more dynamic handling of variable data, you can write Ruby code for processing as front matter. {% rendercontent "docs/note", title: "Don't repeat yourself" %} If you'd like to avoid repeating your frequently used variables @@ -27,8 +22,8 @@ Bridgetown. Files without front matter are considered [static files](/docs/stati and are copied verbatim from the source folder to destination during the build process. -The front matter must be the first thing in the file and must take the form of valid -YAML set between triple-dashed lines. Here is a basic example: +The front matter must be the first thing in the file and must either take the form of valid +YAML set between triple-dashed lines, or one of several Ruby-based formats (more on that below). Here is a basic example: ```yaml --- @@ -238,17 +233,87 @@ If you use UTF-8 encoding, make sure that no `BOM` header characters exist in yo you may encounter build errors. {% endrendercontent %} -## Ruby Front Matter +## The Power of Ruby, in Front Matter -For advanced use cases where you wish to generate dynamic values for front matter variables, you can use Ruby Front Matter (new in Bridgetown v0.13). This feature is available for pages, posts, and other documents–as well as layouts for site-wide access to your Ruby return values. +For advanced use cases where you wish to generate dynamic values for front matter variables, you can use Ruby Front Matter (hereafter named rbfm). (Requires Bridgetown v0.21 or greater. Only applies to sites using the [Resource content engine](/docs/resources)—otherwise read the Legacy Format of Ruby Front Matter section below.) -{% rendercontent "docs/note" %} -Prior to v0.17, this required the environment variable `BRIDGETOWN_RUBY_IN_FRONT_MATTER` to be set to `"true"`, otherwise the code would not be executed and would be treated as a raw string. -{% endrendercontent %} +Any valid Ruby code is allowed in rbfm as long as it returns a `Hash`—or an object which `respond_to?(:to_h)`. There are several different ways you can define rbfm at the top of your file. This is so syntax highlighting will work in various different template scenarios. + +For Markdown files, you can use backticks or tildes plus the term `ruby` to take advantage of GFM (GitHub-flavored Markdown) syntax highlighting. + +~~~md +```ruby +{ + layout: :page, + title: "About" +} +``` + +I'm a **Markdown** file. +~~~ + +or + +```md +~~~ruby +{ + layout: :page, + title: "About" +} +~~~ + +I'm a **Markdown** file. +``` + +For ERB or Serbea files, you can use `---<%` / `%>---` or {% raw %}`---{%` / `%}---`{% endraw %} delimeters respectively. (You can substitute `~` instead of `-` if you prefer.) + +For all-Ruby files, you can use `---ruby` / `---` or `###ruby` / `###` delimeters. + +However you define your rbfm, bear in mind that the front matter code is executed _prior_ to any processing of the template file itself and within a different context. (rbfm will be executed initially within either `Bridgetown::Model::RepoOrigin` or `Bridgetown::Layout`.) + +Thankfully, there is a solution for when you want a front matter variable resolved within the execution context of a resource (aka `Bridgetown::Resource::Base`): use a lambda. Any lambda (or proc in general) will be resolved at the time a resource has been fully initialized. A good use case for this would be to define a custom permalink based on other front matter variables. For example: -[Here's a blog post with a high-level overview](/feature/supercharge-your-bridgetown-site-with-ruby-front-matter/){:data-no-swup="true"} of what Ruby Front Matter is capable of and why you might want to use it. +```md +~~~ruby +{ + layout: :page, + segments: ["custom", "permalink"], + title: "About Us", + permalink: -> { "#{data.segments.join("/")}/#{Bridgetown::Utils.slugify(data.title)}" } +} +~~~ + +This will now show up for the path: /custom/permalink/about-us +``` + +Besides using a simple `Hash`, you can also use the handy `front_matter` DSL. Any valid method call made directly in the block will translate to a front matter key. Let's rewrite the above example: + +```md +~~~ruby +front_matter do + layout :page + + url_segments = ["custom"] + url_segments << "permalink" + segments url_segments + + title "About Us" + permalink -> { "#{data.segments.join("/")}/#{Bridgetown::Utils.slugify(data.title)}" } +end +~~~ + +This will now show up for the path: /custom/permalink/about-us +``` + +As you can see, literally any valid Ruby code has the potential to be transformed into front matter. The sky's the limit! + +## Legacy Format of Ruby Front Matter + +{% rendercontent "docs/note", type: "warning" %} +Embedding Ruby code within YAML is deprecated and will be removed by the release of Bridgetown 1.0. +{% endrendercontent %} -To write Ruby code in your front matter, use the special tagged string `!ruby/string:Rb`. Here is an example: +This feature is available for pages, posts, and other documents–as well as layouts for site-wide access to your Ruby return values. To write Ruby code in your front matter, use the special tagged string `!ruby/string:Rb`. Here is an example: {% raw %} ```liquid diff --git a/bridgetown-website/src/_docs/resources.md b/bridgetown-website/src/_docs/resources.md index 09d7a8e46..663b26064 100644 --- a/bridgetown-website/src/_docs/resources.md +++ b/bridgetown-website/src/_docs/resources.md @@ -33,7 +33,7 @@ Resources come with a merry band of objects to help them along the way. These ar Let's say you add a new blog post by saving `src/_posts/2021-05-10-super-cool-blog-post.md`. To make the transition from a Markdown file with Liquid or ERB template syntax to a final URL on your website, Bridgetown takes your data through several steps: -1. It finds the appropriate origin class to load the post. The posts collection file reader uses a special **origin ID** identify the file (in this case: `file://posts.collection/_posts/2021-05-10-super-cool-blog-post.md`). Other origin classes could handle different protocols to download content from third-party APIs or load in content directly from scripts. +1. It finds the appropriate origin class to load the post. The posts collection file reader uses a special **origin ID** identify the file (in this case: `repo://posts.collection/_posts/2021-05-10-super-cool-blog-post.md`). Other origin classes could handle different protocols to download content from third-party APIs or load in content directly from scripts. 2. Once the origin provides the post's data it is used to create a model object. The model will be a `Bridgetown::Model::Base` object by default, but you can create your own subclasses to alter and enhance data, or for use in a Ruby-based CMS environment. For example, `class Post < Bridgetown::Model::Base; end` will get used automatically for the `posts` collection (because Bridgetown will use the Rails inflector to map `posts` to `Post`). You can save subclasses in your `plugins` folder. 3. The model then "emits" a resource object. The resource is provided a clone of the model data which it can then process for use within template like Liquid, ERB, and so forth. Resources may also point to other resources within their collection, and templates can access resources through various means (looping through collections, referencing resources by source paths, etc.) 4. The resource is transformed by a transformer object which runs a pipeline to convert Markdown to HTML, render Liquid or ERB templates, and any other conversions specified—as well as optionally place the resource output within a converted layout. @@ -148,9 +148,9 @@ You can easily loop through collection resources by name, e.g., `collections.pos {% for post in paginator.resources %} {% endfor %} ``` @@ -178,7 +178,7 @@ Accessing taxonomies for resources is simple as well: Title: {{ site.taxonomy_types.genres.metadata.title }} -{% for term in page.taxonomies.genres.terms %} +{% for term in resource.taxonomies.genres.terms %} Term: {{ term.label }} {% endfor %} ``` @@ -188,7 +188,7 @@ Title: {{ site.taxonomy_types.genres.metadata.title }} Title: <%= site.taxonomy_types.genres.metadata.title %> -<% page.taxonomies.genres.terms.each do |term| %> +<% resource.taxonomies.genres.terms.each do |term| %> Term: <%= term.label %> <% end %> ``` @@ -307,9 +307,49 @@ collections: permalink: /lots-of/:collection/:year/:title/ ``` +## Ruby Front Matter and All-Ruby Templates + +For advanced use cases where you wish to generate dynamic values for front matter variables, you can use Ruby Front Matter. [Read the documentation here.](/docs/front-matter) + +In addition, you can add all-Ruby page templates to your site besides just the typical Markdown/Liquid/ERB options. Yes, you're reading that right: put `.rb` files directly in your `src` folder! As long as the final statement in your code returns a string or can be converted to a string via `to_s`, you're golden. Ruby templates are evaluated in a `Bridgetown::ERBView` context (even though they aren't actually ERB), so all the usual Ruby template helpers are available. + +For example, if we were to convert the out-of-the-box `about.md` page to `about.rb`, it would look something like this: + +```ruby +###ruby +front_matter do + layout :page + title "About Us" +end +### + +output = Array("This is the basic Bridgetown site template. You can find out more info about customizing your Bridgetown site, as well as basic Bridgetown usage documentation at [bridgetownrb.com](https://bridgetownrb.com/)") + +output << "" +output << "You can find the source code for Bridgetown at GitHub:" +output << "[bridgetownrb](https://github.com/bridgetownrb) /" +output << "[bridgetown](https://github.com/bridgetownrb/bridgetown)" + +markdownify output.join("\n") +``` + +Now obviously it's silly to build up Markdown content in an array of strings in a Ruby code file…but imagine building or using third-party DSLs to generate sophisticated markup and advanced structural documents of all kinds. [Arbre](https://activeadmin.github.io/arbre/) is but one example of a Ruby-first approach to creating templates. + +``` +# What if your .rb template looked like this? + +Arbre::Context.new do + h1 "Hello World" + + para "I'm a Ruby template. w00t" +end +``` + ## Differences Between Resource and Legacy Engines * The most obvious differences are what you use in templates (Liquid or ERB). For example, instead of `site.posts` in Liquid or `site.posts.docs` in ERB, you'd use `collections.posts.resources` (in both Liquid and ERB). (`site.collection_name_here` syntax is no longer available.) Pages are just another collection now so you can iterate through them as well via `collections.pages.resources`. +* Front matter data is now accessed in Liquid through the `data` variable just like in ERB and skipping `data` is deprecated. Use `{{ post.data.description }}` instead of just `{{ post.description }}`. +* In addition, instead of referencing the current "page" through `page` (aka `page.data.title`), you can use `resource` instead: `resource.data.title`. * Resources don't have a `url` variable. Your templates/plugins will need to reference either `relative_url` or `absolute_url`. Also, the site's `baseurl` (if configured) is built into both values, so you won't need to prepend it manually. * Whereas the `id` of a document is the relative destination URL, the `id` of a resource is its origin id. You can define an id in front matter separately however. * The paginator items are now accessed via `paginator.resources` instead of `paginator.documents`. @@ -318,8 +358,7 @@ collections: * Since user-authored pages are no longer loaded as `Page` objects and everything formerly loaded as `Document` will now be a `Resource::Base`, plugins will need to be adapted accordingly. The `Page` class will eventually be renamed to `GeneratedPage` to indicate it is only used for content generated by plugins. * With the legacy engine, any folder starting with an underscore within a collection would be skipped. With the resource engine, folders can start with underscores but they aren't included in the final permalink. (Files starting with an underscore are always skipped however.) * The `YYYY-MM-DD-slug.ext` filename format will now work for any collection, not just posts. -* Structured data files (aka YAML, JSON, CSV, etc.) starting with triple-dashes/front-matter can be placed in collection folders, and they will be read and transformed like any other resource. (CSV/TSV data gets loaded into the `rows` front matter key). * The [Document Builder API](/docs/plugins/external-apis) no longer works when the resource content engine is configured. We'll be restoring this functionality in a future point release of Bridgetown. * Automatic excerpts are not included in the current resource featureset. We'll be opening up a brand-new Excerpt/Summary API in the near future. -{% endraw %} \ No newline at end of file +{% endraw %}