Module: Bridgetown::Utils

Extended by:
Utils, Gem::Deprecate
Included in:
Utils
Defined in:
bridgetown-core/lib/bridgetown-core/utils.rb,
bridgetown-core/lib/bridgetown-core/utils/aux.rb,
bridgetown-core/lib/bridgetown-core/utils/ansi.rb,
bridgetown-core/lib/bridgetown-core/utils/ruby_exec.rb,
bridgetown-core/lib/bridgetown-core/utils/pid_tracker.rb,
bridgetown-core/lib/bridgetown-core/utils/require_gems.rb,
bridgetown-core/lib/bridgetown-core/utils/loaders_manager.rb,
bridgetown-core/lib/bridgetown-core/utils/smarty_pants_converter.rb

Overview

rubocop:todo Metrics/ModuleLength

Defined Under Namespace

Modules: Ansi, Aux, PidTracker, RequireGems, RubyExec Classes: LoadersManager, SmartyPantsConverter

Constant Summary collapse

SLUGIFY_MODES =

Constants for use in #slugify

%w(raw default pretty simple ascii latin).freeze
SLUGIFY_RAW_REGEXP =
Regexp.new("\\s+").freeze
SLUGIFY_DEFAULT_REGEXP =
Regexp.new("[^\\p{M}\\p{L}\\p{Nd}]+").freeze
SLUGIFY_PRETTY_REGEXP =
Regexp.new("[^\\p{M}\\p{L}\\p{Nd}._~!$&'()+,;=@]+").freeze
SLUGIFY_ASCII_REGEXP =
Regexp.new("[^[A-Za-z0-9]]+").freeze

Instance Method Summary collapse

Instance Method Details

Add an appropriate suffix to template so that it matches the specified permalink style.

template - permalink template without trailing slash or file extension permalink_style - permalink style, either built-in or custom

The returned permalink template will use the same ending style as specified in permalink_style. For example, if permalink_style contains a trailing slash (or is :pretty, which indirectly has a trailing slash), then so will the returned template. If permalink_style has a trailing “:output_ext” (or is :none, :date, or :ordinal) then so will the returned template. Otherwise, template will be returned without modification.

Examples: add_permalink_suffix(“/:basename”, :pretty) # => “/:basename/”

add_permalink_suffix(“/:basename”, :date) # => “/:basename:output_ext”

add_permalink_suffix(“/:basename”, “/:year/:month/:title/”) # => “/:basename/”

add_permalink_suffix(“/:basename”, “/:year/:month/:title”) # => “/:basename”

Returns the updated permalink template



248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
# File 'bridgetown-core/lib/bridgetown-core/utils.rb', line 248

def add_permalink_suffix(template, permalink_style)
  template = template.dup

  case permalink_style
  when :pretty, :simple
    template << "/"
  when :date, :ordinal, :none
    template << ":output_ext"
  else
    template << "/" if permalink_style.to_s.end_with?("/")
    template << ":output_ext" if permalink_style.to_s.end_with?(":output_ext")
  end

  template
end

#chomp_locale_suffix!(path, locale) ⇒ Object



527
528
529
530
531
532
533
534
535
# File 'bridgetown-core/lib/bridgetown-core/utils.rb', line 527

def chomp_locale_suffix!(path, locale)
  return path unless locale

  if path.ends_with?(".#{locale}")
    path.chomp!(".#{locale}")
  elsif path.ends_with?(".multi")
    path.chomp!(".multi")
  end
end

#deep_merge_hashes(master_hash, other_hash) ⇒ Object

Non-destructive version of deep_merge_hashes! See that method.

Returns the merged hashes.



44
45
46
# File 'bridgetown-core/lib/bridgetown-core/utils.rb', line 44

def deep_merge_hashes(master_hash, other_hash)
  deep_merge_hashes!(master_hash.dup, other_hash)
end

#deep_merge_hashes!(target, overwrite) ⇒ Object

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: http://gemjack.com/gems/tartan-0.1.1/classes/Hash.html

Thanks to whoever made it.



57
58
59
60
61
62
63
# File 'bridgetown-core/lib/bridgetown-core/utils.rb', line 57

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) ⇒ Object



454
455
456
457
458
459
460
461
# File 'bridgetown-core/lib/bridgetown-core/utils.rb', line 454

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"] || "main"
rescue StandardError => e
  Bridgetown.logger.warn("Unable to connect to GitHub API: #{e.message}")
  "main"
end

#dsd_tag(input, shadow_root_mode: :open) ⇒ Object

Raises:

  • (ArgumentError)


537
538
539
540
541
# File 'bridgetown-core/lib/bridgetown-core/utils.rb', line 537

def dsd_tag(input, shadow_root_mode: :open)
  raise ArgumentError unless [:open, :closed].include? shadow_root_mode

  %(<template shadowrootmode="#{shadow_root_mode}">#{input}</template>).html_safe
end

#duplicable?(obj) ⇒ Boolean

Returns:

  • (Boolean)


71
72
73
74
75
76
77
78
# File 'bridgetown-core/lib/bridgetown-core/utils.rb', line 71

def duplicable?(obj)
  case obj
  when nil, false, true, Symbol, Numeric
    false
  else
    true
  end
end

#frontend_bundler_type(cwd = Dir.pwd) ⇒ Object



425
426
427
428
429
430
431
# File 'bridgetown-core/lib/bridgetown-core/utils.rb', line 425

def frontend_bundler_type(cwd = Dir.pwd)
  if File.exist?(File.join(cwd, "esbuild.config.js"))
    :esbuild
  else
    :unknown
  end
end

#has_liquid_construct?(content) ⇒ Boolean

Determine whether the given content string contains Liquid Tags or Vaiables

Returns:

  • (Boolean)

    if the string contains sequences of {% or {{



145
146
147
148
149
# File 'bridgetown-core/lib/bridgetown-core/utils.rb', line 145

def has_liquid_construct?(content)
  return false if content.nil? || content.empty?

  content.include?("{%") || content.include?("{{")
end

#has_rbfm_header?(file) ⇒ Boolean

Returns:

  • (Boolean)


134
135
136
137
138
139
140
# File 'bridgetown-core/lib/bridgetown-core/utils.rb', line 134

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)
end

#has_yaml_header?(file) ⇒ Boolean

Determines whether a given file has

rubocop: disable Naming/PredicateName

Returns:

  • (Boolean)

    if the YAML front matter is present.



126
127
128
129
130
131
132
# File 'bridgetown-core/lib/bridgetown-core/utils.rb', line 126

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)
end

#live_reload_js(site) ⇒ Object

rubocop:disable Metrics/MethodLength



463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
# File 'bridgetown-core/lib/bridgetown-core/utils.rb', line 463

def live_reload_js(site) # rubocop:disable Metrics/MethodLength
  return "" unless Bridgetown.env.development? && !site.config.skip_live_reload

  path = File.join(site.base_path, "/_bridgetown/live_reload")
  code = <<~JAVASCRIPT
    let lastmod = 0
    let reconnectAttempts = 0
    function startLiveReload() {
      const connection = new EventSource("#{path}")

      connection.addEventListener("message", event => {
        reconnectAttempts = 0
        if (document.querySelector("#bridgetown-build-error")) document.querySelector("#bridgetown-build-error").close()
        if (event.data == "reloaded!") {
          location.reload()
        } else {
          const newmod = Number(event.data)
          if (lastmod > 0 && newmod > 0 && lastmod < newmod) {
            location.reload()
          } else {
            lastmod = newmod
          }
        }
      })

      connection.addEventListener("builderror", event => {
        let dialog = document.querySelector("#bridgetown-build-error")
        if (!dialog) {
          dialog = document.createElement("dialog")
          dialog.id = "bridgetown-build-error"
          dialog.style.borderColor = "red"
          dialog.style.fontSize = "110%"
          dialog.innerHTML = `
            <p style="color:red">There was an error when building the site:</p>
            <output><pre></pre></output>
            <p><small>Check your Bridgetown logs for further details.</small></p>
          `
          document.body.appendChild(dialog)
          dialog.showModal()
        }
        dialog.querySelector("pre").textContent = JSON.parse(event.data)
      })

      connection.addEventListener("error", () => {
        if (connection.readyState === 2) {
          // reconnect with new object
          connection.close()
          reconnectAttempts++
          if (reconnectAttempts < 25) {
            console.warn("Live reload: attempting to reconnect in 3 seconds...")
            setTimeout(() => startLiveReload(), 3000)
          } else {
            console.error("Too many live reload connections failed. Refresh the page to try again.")
          }
        }
      })
    }

    startLiveReload()
  JAVASCRIPT

  %(<script type="module">#{code}</script>).html_safe
end

#log_frontend_asset_error(site, asset_type) ⇒ Object



411
412
413
414
415
416
417
418
419
420
421
422
423
# File 'bridgetown-core/lib/bridgetown-core/utils.rb', line 411

def log_frontend_asset_error(site, asset_type)
  site.data[:__frontend_asset_errors] ||= {}
  site.data[:__frontend_asset_errors][asset_type] ||= begin
    Bridgetown.logger.warn("#{frontend_bundler_type}:", "The #{asset_type} could not be found.")
    Bridgetown.logger.warn(
      "#{frontend_bundler_type}:",
      "Double-check your frontend config or re-run `bin/bridgetown frontend:build'"
    )
    true
  end

  "MISSING_#{frontend_bundler_type.upcase}_ASSET"
end

#mergeable?(value) ⇒ Boolean Also known as: mergable?

Returns:

  • (Boolean)


65
66
67
# File 'bridgetown-core/lib/bridgetown-core/utils.rb', line 65

def mergeable?(value)
  value.is_a?(Hash) || value.is_a?(Drops::Drop)
end

#merged_file_read_opts(site, opts) ⇒ Object

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



300
301
302
303
304
305
306
307
308
309
# File 'bridgetown-core/lib/bridgetown-core/utils.rb', line 300

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.") ⇒ Object

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



116
117
118
119
120
# File 'bridgetown-core/lib/bridgetown-core/utils.rb', line 116

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_esbuild_manifest_file(site, asset_type) ⇒ String?

Return an asset path based on the esbuild manifest file

Parameters:

  • site (Bridgetown::Site)

    The current site object

  • asset_type (String)

    js or css, or filename in manifest

Returns:

  • (String)

    Returns “MISSING_ESBUILD_MANIFEST” if the manifest file isnt found

  • (nil)

    Returns nil if the asset isnt found

  • (String)

    Returns the path to the asset if no issues parsing



379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
# File 'bridgetown-core/lib/bridgetown-core/utils.rb', line 379

def parse_esbuild_manifest_file(site, asset_type) # rubocop:disable Metrics/PerceivedComplexity
  return log_frontend_asset_error(site, "esbuild manifest") if site.frontend_manifest.nil?

  asset_path = case asset_type
               when "css"
                 site.frontend_manifest["styles/index.css"] ||
                   site.frontend_manifest["styles/index.scss"] ||
                   site.frontend_manifest["styles/index.sass"]
               when "js"
                 site.frontend_manifest["javascript/index.js"] ||
                   site.frontend_manifest["javascript/index.js.rb"]
               else
                 site.frontend_manifest.find do |item, _|
                   item.sub(%r{^../(frontend/|src/)?}, "") == asset_type
                 end&.last
               end

  return log_frontend_asset_error(site, "`#{asset_type}' asset") if asset_path.nil?

  static_frontend_path site, [asset_path]
end

#parse_frontend_manifest_file(site, asset_type) ⇒ String?

Return an asset path based on a frontend manifest file

Parameters:

  • site (Bridgetown::Site)

    The current site object

  • asset_type (String)

    js or css, or filename in manifest

Returns:

  • (String, nil)


358
359
360
361
362
363
364
365
366
367
368
369
# File 'bridgetown-core/lib/bridgetown-core/utils.rb', line 358

def parse_frontend_manifest_file(site, asset_type)
  case frontend_bundler_type(site.root_dir)
  when :esbuild
    parse_esbuild_manifest_file(site, asset_type)
  else
    Bridgetown.logger.warn(
      "Frontend:",
      "No frontend bundling configuration was found."
    )
    "MISSING_FRONTEND_BUNDLING_CONFIG"
  end
end

#pluralized_array_from_hash(hsh, singular_key, plural_key) ⇒ Array

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

Parameters:

  • hsh (Hash)

    the hash to read from

  • singular_key (Symbol)

    the singular key

  • plural_key (Symbol)

    the plural key

Returns:

  • (Array)


87
88
89
90
91
92
93
94
95
96
97
# File 'bridgetown-core/lib/bridgetown-core/utils.rb', line 87

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) ⇒ Object

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



314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
# File 'bridgetown-core/lib/bridgetown-core/utils.rb', line 314

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

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

    if continue_processing
      line_indentation = line.match(%r!^ +!).then 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) ⇒ Object

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



287
288
289
290
291
292
293
294
295
296
# File 'bridgetown-core/lib/bridgetown-core/utils.rb', line 287

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) ⇒ Object

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.



197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'bridgetown-core/lib/bridgetown-core/utils.rb', line 197

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:)

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

  slug.downcase! unless cased

  slug
end

#static_frontend_path(site, additional_parts = []) ⇒ Object



401
402
403
404
405
406
407
408
409
# File 'bridgetown-core/lib/bridgetown-core/utils.rb', line 401

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) ⇒ Object

Takes a slug and turns it into a simple title.



23
24
25
# File 'bridgetown-core/lib/bridgetown-core/utils.rb', line 23

def titleize_slug(slug)
  slug.gsub(%r![_ ]!, "-").split("-").map!(&:capitalize).join(" ")
end

#update_esbuild_autogenerated_config(config) ⇒ Object



433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
# File 'bridgetown-core/lib/bridgetown-core/utils.rb', line 433

def update_esbuild_autogenerated_config(config)
  defaults_file = File.join(config[:root_dir], "config", "esbuild.defaults.js")
  return unless File.exist?(defaults_file)

  config_hash = {
    source: Pathname.new(config[:source]).relative_path_from(config[:root_dir]),
    destination: Pathname.new(config[:destination]).relative_path_from(config[:root_dir]),
    componentsDir: config[:components_dir],
    islandsDir: config[:islands_dir],
  }

  defaults_file_contents = File.read(defaults_file)
  File.write(
    defaults_file,
    defaults_file_contents.sub(
      %r{(const autogeneratedBridgetownConfig = ){\n.*?}}m,
      "\\1#{JSON.pretty_generate config_hash}"
    )
  )
end

#value_from_plural_key(hsh, key) ⇒ Object



99
100
101
102
103
104
105
106
107
# File 'bridgetown-core/lib/bridgetown-core/utils.rb', line 99

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) ⇒ String

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

Examples

xml_escape(‘foo “bar” ') # => "foo "bar" <baz>"

Parameters:

  • input (String)

    The String to escape.

Returns:

  • (String)

    the escaped String.



37
38
39
# File 'bridgetown-core/lib/bridgetown-core/utils.rb', line 37

def xml_escape(input)
  input.to_s.encode(xml: :attr).gsub(%r!\A"|"\Z!, "")
end