class Bridgetown::Document

TODO: to be retired once the Resource engine is made official

Constants

DATELESS_FILENAME_MATCHER
DATE_FILENAME_MATCHER
YAML_FILE_EXTS
YAML_FRONT_MATTER_REGEXP

Attributes

collection[R]
content[RW]
extname[R]
output[RW]
path[R]
site[R]
type[R]

Public Class Methods

new(path, relations = {}) click to toggle source

Create a new Document.

path - the path to the file relations - a hash with keys :site and :collection, the values of which

are the Bridgetown::Site and Bridgetown::Collection to which this
Document belong.

Returns nothing.

# File lib/bridgetown-core/document.rb, line 46
def initialize(path, relations = {})
  @site = relations[:site]
  @path = path
  @extname = File.extname(path)
  @collection = relations[:collection]
  @type = @collection.label.to_sym

  @has_yaml_header = nil

  data.default_proc = proc do |_, key|
    site.frontmatter_defaults.find(relative_path, type, key.to_s)
  end

  trigger_hooks(:post_init)
end
superdirs_regex(dirname) click to toggle source

Class-wide cache to stash and retrieve regexp to detect “super-directories” of a particular Bridgetown::Document object.

dirname - The *special directory* for the Document.

e.g. "_posts" for Documents from the `site.posts` collection.
# File lib/bridgetown-core/document.rb, line 31
def self.superdirs_regex(dirname)
  @superdirs_regex ||= {}
  @superdirs_regex[dirname] ||= %r!#{dirname}.*!
end

Public Instance Methods

<=>(other) click to toggle source

Compare this document against another document. Comparison is a comparison between the 2 paths of the documents.

Returns -1, 0, +1 or nil depending on whether this doc's path is less than,

equal or greater than the other doc's path. See String#<=> for more details.
# File lib/bridgetown-core/document.rb, line 259
def <=>(other)
  return nil unless other.respond_to?(:data)

  cmp = data["date"] <=> other.data["date"]
  cmp = path <=> other.path if cmp.nil? || cmp.zero?
  cmp
end
basename() click to toggle source

The base filename of the document.

Returns the base filename of the document.

# File lib/bridgetown-core/document.rb, line 123
def basename
  @basename ||= File.basename(path)
end
basename_without_ext() click to toggle source

The base filename of the document, without the file extname.

Returns the basename without the file extname.

# File lib/bridgetown-core/document.rb, line 116
def basename_without_ext
  @basename_without_ext ||= File.basename(path, ".*")
end
cleaned_relative_path() click to toggle source

Produces a “cleaned” relative path. The “cleaned” relative path is the relative path without the extname

and with the collection's directory removed as well.

This method is useful when building the URL of the document.

NOTE: `String#gsub` removes all trailing periods (in comparison to `String#chomp`)

Examples:

When relative_path is "_methods/site/generate...md":
  cleaned_relative_path
  # => "/site/generate"

Returns the cleaned relative path of the document.

# File lib/bridgetown-core/document.rb, line 140
def cleaned_relative_path
  @cleaned_relative_path ||=
    relative_path[0..-extname.length - 1]
      .sub(collection.relative_directory, "")
      .gsub(%r!\.*\z!, "")
end
data() click to toggle source

Fetch the Document's data.

Returns a Hash containing the data. An empty hash is returned if

no data was read.
# File lib/bridgetown-core/document.rb, line 66
def data
  @data ||= HashWithDotAccess::Hash.new
end
date() click to toggle source

Returns the document date. If metadata is not present then calculates it based on Bridgetown::Site#time or the document file modification time.

Return document date string.

# File lib/bridgetown-core/document.rb, line 84
def date
  data["date"] ||= site.time # TODO: this doesn't reflect documented behavior
end
destination(base_directory) click to toggle source

The full path to the output file.

base_directory - the base path of the output directory

Returns the full path to the output file of this document.

# File lib/bridgetown-core/document.rb, line 193
def destination(base_directory)
  dest = site.in_dest_dir(base_directory)
  path = site.in_dest_dir(dest, URL.unescape_path(url))
  if url.end_with? "/"
    path = File.join(path, "index.html")
  else
    path << output_ext unless path.end_with? output_ext
  end
  path
end
excerpt_separator() click to toggle source

The Document excerpt_separator, from the YAML Front-Matter or site default excerpt_separator value

Returns the document excerpt_separator

# File lib/bridgetown-core/document.rb, line 281
def excerpt_separator
  @excerpt_separator ||= (data["excerpt_separator"] || site.config["excerpt_separator"]).to_s
end
generate_excerpt?() click to toggle source

Whether to generate an excerpt

Returns true if the excerpt separator is configured.

# File lib/bridgetown-core/document.rb, line 288
def generate_excerpt?
  !excerpt_separator.empty?
end
id() click to toggle source
# File lib/bridgetown-core/document.rb, line 307
def id
  @id ||= File.join(File.dirname(url), (data["slug"] || basename_without_ext).to_s)
end
inspect() click to toggle source

The inspect string for this document. Includes the relative path and the collection label.

Returns the inspect string for this document.

# File lib/bridgetown-core/document.rb, line 250
def inspect
  "#<#{self.class} #{relative_path} collection=#{collection.label}>"
end
merge_data!(other, source: "YAML front matter") click to toggle source

Merge some data in with this document's data.

Returns the merged data.

# File lib/bridgetown-core/document.rb, line 73
def merge_data!(other, source: "YAML front matter")
  merge_categories!(other)
  Utils.deep_merge_hashes!(data, other)
  merge_date!(source)
  data
end
method_missing(method, *args, &blck) click to toggle source

Override of method_missing to check in @data for the key.

Calls superclass method
# File lib/bridgetown-core/document.rb, line 319
def method_missing(method, *args, &blck)
  if data.key?(method.to_s)
    Bridgetown::Deprecator.deprecation_message "Document##{method} is now a key "\
                       "in the #data hash."
    Bridgetown::Deprecator.deprecation_message "Called by #{caller(0..0)}."
    data[method.to_s]
  else
    super
  end
end
next_doc() click to toggle source
# File lib/bridgetown-core/document.rb, line 292
def next_doc
  pos = collection.docs.index { |post| post.equal?(self) }
  collection.docs[pos + 1] if pos && pos < collection.docs.length - 1
end
output_ext() click to toggle source

FIXME: spinning up a new Renderer object just to get an extension seems excessive

The output extension of the document.

Returns the output extension

# File lib/bridgetown-core/document.rb, line 109
def output_ext
  @output_ext ||= Bridgetown::Renderer.new(site, self).output_ext
end
previous_doc() click to toggle source
# File lib/bridgetown-core/document.rb, line 297
def previous_doc
  pos = collection.docs.index { |post| post.equal?(self) }
  collection.docs[pos - 1] if pos&.positive?
end
read(opts = {}) click to toggle source

Read in the file and assign the content and data based on the file contents. Merge the frontmatter of the file with the frontmatter default values

Returns nothing.

# File lib/bridgetown-core/document.rb, line 223
def read(opts = {})
  Bridgetown.logger.debug "Reading:", relative_path

  if yaml_file?
    @data = YAMLParser.load_file(path)
  else
    begin
      merge_defaults
      read_content(**opts)
      read_post_data
    rescue StandardError => e
      handle_read_error(e)
    end
  end
end
relative_path() click to toggle source

The path to the document, relative to the collections_dir.

Returns a String path which represents the relative path from the collections_dir to this document.

# File lib/bridgetown-core/document.rb, line 99
def relative_path
  @relative_path ||= path.sub("#{site.collections_path}/", "")
end
respond_to_missing?(method, *) click to toggle source
Calls superclass method
# File lib/bridgetown-core/document.rb, line 330
def respond_to_missing?(method, *)
  data.key?(method.to_s) || super
end
source_file_mtime() click to toggle source

Return document file modification time in the form of a Time object.

Return document file modification Time object.

# File lib/bridgetown-core/document.rb, line 91
def source_file_mtime
  File.mtime(path)
end
to_liquid() click to toggle source

Create a Liquid-understandable version of this Document.

Returns a Hash representing this Document's data.

# File lib/bridgetown-core/document.rb, line 242
def to_liquid
  @to_liquid ||= Drops::DocumentDrop.new(self)
end
trigger_hooks(hook_name, *args) click to toggle source
# File lib/bridgetown-core/document.rb, line 302
def trigger_hooks(hook_name, *args)
  Bridgetown::Hooks.trigger collection.label.to_sym, hook_name, self, *args if collection
  Bridgetown::Hooks.trigger :documents, hook_name, self, *args
end
url() click to toggle source

The computed URL for the document. See `Bridgetown::URL#to_s` for more details.

Returns the computed URL for the document.

# File lib/bridgetown-core/document.rb, line 180
def url
  @url ||= URL.new(
    template: url_template,
    placeholders: url_placeholders,
    permalink: permalink
  ).to_s
end
url_placeholders() click to toggle source

Construct a Hash of key-value pairs which contain a mapping between

a key in the URL template and the corresponding value for this document.

Returns the Hash of key-value pairs for replacement in the URL.

# File lib/bridgetown-core/document.rb, line 165
def url_placeholders
  @url_placeholders ||= Drops::UrlDrop.new(self)
end
url_template() click to toggle source

The URL template where the document would be accessible.

Returns the URL template for the document.

# File lib/bridgetown-core/document.rb, line 157
def url_template
  collection.url_template
end
write(dest) click to toggle source

Write the generated Document file to the destination directory.

dest - The String path to the destination dir.

Returns nothing.

# File lib/bridgetown-core/document.rb, line 209
def write(dest)
  path = destination(dest)
  FileUtils.mkdir_p(File.dirname(path))
  Bridgetown.logger.debug "Writing:", path
  File.write(path, output, mode: "wb")

  trigger_hooks(:post_write)
end
write?() click to toggle source

Determine whether this document should be written. Based on the Collection to which it belongs.

True if the document has a collection and if that collection's write? method returns true, and if the site's Publisher will publish the document. False otherwise.

# File lib/bridgetown-core/document.rb, line 273
def write?
  collection&.write? && site.publisher.publish?(self)
end
yaml_file?() click to toggle source

Determine whether the document is a YAML file.

Returns true if the extname is either .yml or .yaml, false otherwise.

# File lib/bridgetown-core/document.rb, line 150
def yaml_file?
  YAML_FILE_EXTS.include?(extname)
end

Private Instance Methods

determine_locale() click to toggle source
# File lib/bridgetown-core/document.rb, line 415
def determine_locale
  unless data["locale"]
    # if locale key isn't directly set, look for alternative front matter
    # or look at the filename pattern: slug.locale.ext
    alernative = data["language"] || data["lang"] ||
      basename_without_ext.split(".")[1..-1].last

    data["locale"] = alernative if !alernative.nil? &&
      site.config[:available_locales].include?(alernative)
  end
end
generate_excerpt() click to toggle source
# File lib/bridgetown-core/document.rb, line 433
def generate_excerpt
  data["excerpt"] ||= Bridgetown::Excerpt.new(self) if generate_excerpt?
end
handle_read_error(error) click to toggle source
# File lib/bridgetown-core/document.rb, line 374
def handle_read_error(error)
  if error.is_a? Psych::SyntaxError
    Bridgetown.logger.error "Error:", "YAML Exception reading #{path}: #{error.message}"
  else
    Bridgetown.logger.error "Error:", "could not read file #{path}: #{error.message}"
  end

  if site.config["strict_front_matter"] || error.is_a?(Bridgetown::Errors::FatalException)
    raise error
  end
end
merge_categories!(other) click to toggle source
# File lib/bridgetown-core/document.rb, line 336
def merge_categories!(other)
  if other.key?("categories") && !other["categories"].nil?
    other["categories"] = other["categories"].split if other["categories"].is_a?(String)
    other["categories"] = (data["categories"] || []) | other["categories"]
  end
end
merge_date!(source) click to toggle source
# File lib/bridgetown-core/document.rb, line 343
def merge_date!(source)
  if data.key?("date")
    data["date"] = Utils.parse_date(
      data["date"].to_s,
      "Document '#{relative_path}' does not have a valid date in the #{source}."
    )
  end
end
merge_defaults() click to toggle source
# File lib/bridgetown-core/document.rb, line 352
def merge_defaults
  defaults = @site.frontmatter_defaults.all(relative_path, type)
  merge_data!(defaults, source: "front matter defaults") unless defaults.empty?
end
modify_date(date) click to toggle source
# File lib/bridgetown-core/document.rb, line 427
def modify_date(date)
  if !data["date"] || data["date"].to_i == site.time.to_i
    merge_data!({ "date" => date }, source: "filename")
  end
end
populate_categories() click to toggle source
# File lib/bridgetown-core/document.rb, line 405
def populate_categories
  data.categories = Utils.pluralized_array_from_hash(
    data, :category, :categories
  ).map(&:to_s)
end
populate_tags() click to toggle source
# File lib/bridgetown-core/document.rb, line 411
def populate_tags
  data.tags = Utils.pluralized_array_from_hash(data, :tag, :tags)
end
populate_title() click to toggle source
# File lib/bridgetown-core/document.rb, line 386
def populate_title
  if relative_path =~ DATE_FILENAME_MATCHER
    date, slug, ext = Regexp.last_match.captures
    modify_date(date)
  elsif relative_path =~ DATELESS_FILENAME_MATCHER
    slug, ext = Regexp.last_match.captures
  end

  # slugs shouldn't end with a period
  # `String#gsub!` removes all trailing periods (in comparison to `String#chomp!`)
  slug.gsub!(%r!\.*\z!, "")

  # Try to ensure the user gets a title.
  data["title"] ||= Utils.titleize_slug(slug)
  # Only overwrite slug & ext if they aren't specified.
  data["slug"]  ||= slug
  data["ext"]   ||= ext
end
read_content(**opts) click to toggle source
# File lib/bridgetown-core/document.rb, line 357
def read_content(**opts)
  self.content = File.read(path, **Utils.merged_file_read_opts(site, opts))
  if content =~ YAML_FRONT_MATTER_REGEXP
    self.content = $POSTMATCH
    data_file = YAMLParser.load(Regexp.last_match(1))
    merge_data!(data_file, source: "YAML front matter") if data_file
  end
end
read_post_data() click to toggle source
# File lib/bridgetown-core/document.rb, line 366
def read_post_data
  populate_title
  populate_categories
  populate_tags
  determine_locale
  generate_excerpt
end