module Bridgetown::Utils

Constants

SLUGIFY_ASCII_REGEXP
SLUGIFY_DEFAULT_REGEXP
SLUGIFY_MODES

Constants for use in slugify

SLUGIFY_PRETTY_REGEXP
SLUGIFY_RAW_REGEXP

Public Instance Methods

deep_merge_hashes(master_hash, other_hash) click to toggle source

Non-destructive version of deep_merge_hashes! See that method.

Returns the merged hashes.

# File lib/bridgetown-core/utils.rb, line 40
def deep_merge_hashes(master_hash, other_hash)
  deep_merge_hashes!(master_hash.dup, other_hash)
end
deep_merge_hashes!(target, overwrite) click to toggle source

Merges a master hash with another hash, recursively.

master_hash - the “parent” hash whose values will be overridden other_hash - the other hash whose values will be persisted after the merge

This code was lovingly stolen from some random gem: gemjack.com/gems/tartan-0.1.1/classes/Hash.html

Thanks to whoever made it.

# File lib/bridgetown-core/utils.rb, line 53
def deep_merge_hashes!(target, overwrite)
  merge_values(target, overwrite)
  merge_default_proc(target, overwrite)
  duplicate_frozen_values(target)

  target
end
default_github_branch_name(repo_url) click to toggle source
# File lib/bridgetown-core/utils.rb, line 392
def default_github_branch_name(repo_url)
  repo_match = Bridgetown::Commands::Actions::GITHUB_REPO_REGEX.match(repo_url)
  api_endpoint = "https://api.github.com/repos/#{repo_match[1]}"
  JSON.parse(Faraday.get(api_endpoint).body)["default_branch"] || "master"
rescue StandardError => e
  Bridgetown.logger.warn("Unable to connect to GitHub API: #{e.message}")
  "master"
end
duplicable?(obj) click to toggle source
# File lib/bridgetown-core/utils.rb, line 65
def duplicable?(obj)
  case obj
  when nil, false, true, Symbol, Numeric
    false
  else
    true
  end
end
has_liquid_construct?(content) click to toggle source

Determine whether the given content string contains Liquid Tags or Vaiables

@return [Boolean] if the string contains sequences of `{%` or `{{`

# File lib/bridgetown-core/utils.rb, line 135
def has_liquid_construct?(content)
  return false if content.nil? || content.empty?

  content.include?("{%") || content.include?("{{")
end
has_rbfm_header?(file) click to toggle source
# File lib/bridgetown-core/utils.rb, line 126
def has_rbfm_header?(file)
  File.open(file, "rb", &:readline).match? Bridgetown::FrontMatterImporter::RUBY_HEADER
rescue EOFError
  false
end
has_yaml_header?(file) click to toggle source

Determines whether a given file has

@return [Boolean] if the YAML front matter is present. rubocop: disable Naming/PredicateName

# File lib/bridgetown-core/utils.rb, line 120
def has_yaml_header?(file)
  File.open(file, "rb", &:readline).match? Bridgetown::FrontMatterImporter::YAML_HEADER
rescue EOFError
  false
end
log_webpack_asset_error(asset_type) click to toggle source
# File lib/bridgetown-core/utils.rb, line 382
def log_webpack_asset_error(asset_type)
  Bridgetown.logger.warn(
    "Webpack:",
    "There was an error parsing your #{asset_type} file. \
    Please check your #{asset_type} file for any errors."
  )

  "MISSING_WEBPACK_MANIFEST_FILE"
end
mergable?(value) click to toggle source
# File lib/bridgetown-core/utils.rb, line 61
def mergable?(value)
  value.is_a?(Hash) || value.is_a?(Drops::Drop)
end
merged_file_read_opts(site, opts) click to toggle source

Returns merged option hash for File.read of self.site (if exists) and a given param

# File lib/bridgetown-core/utils.rb, line 290
def merged_file_read_opts(site, opts)
  merged = (site ? site.file_read_opts : {}).merge(opts)
  if merged[:encoding] && !merged[:encoding].start_with?("bom|")
    merged[:encoding] = "bom|#{merged[:encoding]}"
  end
  if merged["encoding"] && !merged["encoding"].start_with?("bom|")
    merged["encoding"] = "bom|#{merged["encoding"]}"
  end
  merged
end
parse_date(input, msg = "Input could not be parsed.") click to toggle source

Parse a date/time and throw an error if invalid

input - the date/time to parse msg - (optional) the error message to show the user

Returns the parsed date if successful, throws a FatalException if not

# File lib/bridgetown-core/utils.rb, line 110
def parse_date(input, msg = "Input could not be parsed.")
  Time.parse(input).localtime
rescue ArgumentError
  raise Errors::InvalidDateError, "Invalid date '#{input}': #{msg}"
end
parse_webpack_manifest_file(site, asset_type) click to toggle source

Return an asset path based on the Webpack manifest file @param site [Bridgetown::Site] The current site object @param asset_type [String] js or css, or filename in manifest

@return [String] Returns “MISSING_WEBPACK_MANIFEST” if the manifest file isnt found @return [nil] Returns nil if the asset isnt found @return [String] Returns the path to the asset if no issues parsing

@raise [WebpackAssetError] if unable to find css or js in the manifest file

# File lib/bridgetown-core/utils.rb, line 356
def parse_webpack_manifest_file(site, asset_type)
  return log_webpack_asset_error("Webpack manifest") if site.frontend_manifest.nil?

  asset_path = if %w(js css).include?(asset_type)
                 site.frontend_manifest["main.#{asset_type}"]
               else
                 site.frontend_manifest.find do |item, _|
                   item.sub(%r{^../(frontend/|src/)?}, "") == asset_type
                 end&.last
               end

  return log_webpack_asset_error(asset_type) if asset_path.nil?

  static_frontend_path site, ["js", asset_path]
end
pluralized_array_from_hash(hsh, singular_key, plural_key) click to toggle source

Read array from the supplied hash, merging the singular key with the plural key as needing, and handling any nil or duplicate entries.

@param hsh [Hash] the hash to read from @param singular_key [Symbol] the singular key @param plural_key [Symbol] the plural key @return [Array]

# File lib/bridgetown-core/utils.rb, line 81
def pluralized_array_from_hash(hsh, singular_key, plural_key)
  array = [
    hsh[singular_key],
    value_from_plural_key(hsh, plural_key),
  ]

  array.flatten!
  array.compact!
  array.uniq!
  array
end
reindent_for_markdown(input) click to toggle source

Returns a string that's been reindented so that Markdown's four+ spaces = code doesn't get triggered for nested Liquid components rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity

# File lib/bridgetown-core/utils.rb, line 304
def reindent_for_markdown(input)
  lines = input.lines
  return input if lines.first.nil?

  starting_indentation = lines.find { |line| line != "\n" }&.match(%r!^ +!)
  return input unless starting_indentation

  starting_indent_length = starting_indentation[0].length

  skip_pre_lines = false
  lines.map do |line|
    continue_processing = !skip_pre_lines

    if skip_pre_lines
      skip_pre_lines = false if line.include?("</pre>")
    end
    if line.include?("<pre")
      skip_pre_lines = true
      continue_processing = false
    end

    if continue_processing
      line_indentation = line.match(%r!^ +!).yield_self do |indent|
        indent.nil? ? "" : indent[0]
      end
      new_indentation = line_indentation.rjust(starting_indent_length, " ")

      if %r!^ +!.match?(line)
        line
          .sub(%r!^ {1,#{starting_indent_length}}!, new_indentation)
          .sub(%r!^#{new_indentation}!, "")
      else
        line
      end
    else
      line
    end
  end.join("")
end
safe_glob(dir, patterns, flags = 0) click to toggle source

Work the same way as Dir.glob but seperating the input into two parts ('dir' + '/' + 'pattern') to make sure the first part('dir') does not act as a pattern.

For example, Dir.glob(“path[/*”) always returns an empty array, because the method fails to find the closing pattern to '[' which is ']'

Examples:

safe_glob("path[", "*")
# => ["path[/file1", "path[/file2"]

safe_glob("path", "*", File::FNM_DOTMATCH)
# => ["path/.", "path/..", "path/file1"]

safe_glob("path", ["**", "*"])
# => ["path[/file1", "path[/folder/file2"]

dir - the dir where glob will be executed under

(the dir will be included to each result)

patterns - the patterns (or the pattern) which will be applied under the dir flags - the flags which will be applied to the pattern

Returns matched pathes

# File lib/bridgetown-core/utils.rb, line 277
def safe_glob(dir, patterns, flags = 0)
  return [] unless Dir.exist?(dir)

  pattern = File.join(Array(patterns))
  return [dir] if pattern.empty?

  Dir.chdir(dir) do
    Dir.glob(pattern, flags).map { |f| File.join(dir, f) }
  end
end
slugify(string, mode: nil, cased: false) click to toggle source

Slugify a filename or title.

string - the filename or title to slugify mode - how string is slugified cased - whether to replace all uppercase letters with their lowercase counterparts

When mode is “none”, return the given string.

When mode is “raw”, return the given string, with every sequence of spaces characters replaced with a hyphen.

When mode is “default”, “simple”, or nil, non-alphabetic characters are replaced with a hyphen too.

When mode is “pretty”, some non-alphabetic characters (._~!$&'()+,;=@) are not replaced with hyphen.

When mode is “ascii”, some everything else except ASCII characters a-z (lowercase), A-Z (uppercase) and 0-9 (numbers) are not replaced with hyphen.

When mode is “latin”, the input string is first preprocessed so that any letters with accents are replaced with the plain letter. Afterwards, it follows the “default” mode of operation.

If cased is true, all uppercase letters in the result string are replaced with their lowercase counterparts.

Examples:

slugify("The _config.yml file")
# => "the-config-yml-file"

slugify("The _config.yml file", "pretty")
# => "the-_config.yml-file"

slugify("The _config.yml file", "pretty", true)
# => "The-_config.yml file"

slugify("The _config.yml file", "ascii")
# => "the-config-yml-file"

slugify("The _config.yml file", "latin")
# => "the-config-yml-file"

Returns the slugified string.

# File lib/bridgetown-core/utils.rb, line 187
def slugify(string, mode: nil, cased: false)
  mode ||= "default"
  return nil if string.nil?

  unless SLUGIFY_MODES.include?(mode)
    return cased ? string : string.downcase
  end

  # Drop accent marks from latin characters. Everything else turns to ?
  if mode == "latin"
    I18n.config.available_locales = :en if I18n.config.available_locales.empty?
    string = I18n.transliterate(string)
  end

  slug = replace_character_sequence_with_hyphen(string, mode: mode)

  # Remove leading/trailing hyphen
  slug.gsub!(%r!^\-|\-$!i, "")

  slug.downcase! unless cased

  slug
end
static_frontend_path(site, additional_parts = []) click to toggle source
# File lib/bridgetown-core/utils.rb, line 372
def static_frontend_path(site, additional_parts = [])
  path_parts = [
    site.base_path.gsub(%r(^/|/$), ""),
    "_bridgetown/static",
    *additional_parts,
  ]
  path_parts[0] = "/#{path_parts[0]}" unless path_parts[0].empty?
  Addressable::URI.parse(path_parts.join("/")).normalize.to_s
end
titleize_slug(slug) click to toggle source

Takes a slug and turns it into a simple title.

# File lib/bridgetown-core/utils.rb, line 19
def titleize_slug(slug)
  slug.gsub(%r![_ ]!, "-").split("-").map!(&:capitalize).join(" ")
end
value_from_plural_key(hsh, key) click to toggle source
# File lib/bridgetown-core/utils.rb, line 93
def value_from_plural_key(hsh, key)
  val = hsh[key]
  case val
  when String
    val.split
  when Array
    val.compact
  end
end
xml_escape(input) click to toggle source

XML escape a string for use. Replaces any special characters with appropriate HTML entity replacements.

Examples

xml_escape('foo "bar" <baz>')
# => "foo &quot;bar&quot; &lt;baz&gt;"

@param input [String] The String to escape. @return [String] the escaped String.

# File lib/bridgetown-core/utils.rb, line 33
def xml_escape(input)
  input.to_s.encode(xml: :attr).gsub(%r!\A"|"\Z!, "")
end

Private Instance Methods

duplicate_frozen_values(target) click to toggle source
# File lib/bridgetown-core/utils.rb, line 421
def duplicate_frozen_values(target)
  target.each do |key, val|
    target[key] = val.dup if val.frozen? && duplicable?(val)
  end
end
merge_default_proc(target, overwrite) click to toggle source
# File lib/bridgetown-core/utils.rb, line 415
def merge_default_proc(target, overwrite)
  if target.is_a?(Hash) && overwrite.is_a?(Hash) && target.default_proc.nil?
    target.default_proc = overwrite.default_proc
  end
end
merge_values(target, overwrite) click to toggle source
# File lib/bridgetown-core/utils.rb, line 403
def merge_values(target, overwrite)
  target.merge!(overwrite) do |_key, old_val, new_val|
    if new_val.nil?
      old_val
    elsif mergable?(old_val) && mergable?(new_val)
      deep_merge_hashes(old_val, new_val)
    else
      new_val
    end
  end
end
replace_character_sequence_with_hyphen(string, mode: "default") click to toggle source

Replace each character sequence with a hyphen.

See Utils#slugify for a description of the character sequence specified by each mode.

# File lib/bridgetown-core/utils.rb, line 431
def replace_character_sequence_with_hyphen(string, mode: "default")
  replaceable_char =
    case mode
    when "raw"
      SLUGIFY_RAW_REGEXP
    when "pretty"
      # "._~!$&'()+,;=@" is human readable (not URI-escaped) in URL
      # and is allowed in both extN and NTFS.
      SLUGIFY_PRETTY_REGEXP
    when "ascii"
      # For web servers not being able to handle Unicode, the safe
      # method is to ditch anything else but latin letters and numeric
      # digits.
      SLUGIFY_ASCII_REGEXP
    else
      SLUGIFY_DEFAULT_REGEXP
    end

  # Strip according to the mode
  string.gsub(replaceable_char, "-")
end