Module: Bridgetown::Utils

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

Overview

rubocop:todo Metrics/ModuleLength

Defined Under Namespace

Modules: Ansi, Aux, RequireGems, RubyExec, RubyFrontMatterDSL Classes: LoadersManager, RubyFrontMatter, 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



241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
# File 'lib/bridgetown-core/utils.rb', line 241

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



539
540
541
542
543
544
545
546
547
# File 'lib/bridgetown-core/utils.rb', line 539

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.



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

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.



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

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



475
476
477
478
479
480
481
482
# File 'lib/bridgetown-core/utils.rb', line 475

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)


549
550
551
552
553
# File 'lib/bridgetown-core/utils.rb', line 549

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)


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

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

#frontend_bundler_type(cwd = Dir.pwd) ⇒ Object



444
445
446
447
448
449
450
451
452
# File 'lib/bridgetown-core/utils.rb', line 444

def frontend_bundler_type(cwd = Dir.pwd)
  if File.exist?(File.join(cwd, "webpack.config.js"))
    :webpack
  elsif 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 {{



138
139
140
141
142
# File 'lib/bridgetown-core/utils.rb', line 138

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

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

#has_rbfm_header?(file) ⇒ Boolean

Returns:

  • (Boolean)


131
132
133
# File 'lib/bridgetown-core/utils.rb', line 131

def has_rbfm_header?(file)
  File.open(file, "rb", &:gets)&.match?(Bridgetown::FrontMatterImporter::RUBY_HEADER) || false
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.



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

def has_yaml_header?(file)
  File.open(file, "rb", &:gets)&.match?(Bridgetown::FrontMatterImporter::YAML_HEADER) || false
end

#live_reload_js(site) ⇒ Object

rubocop:disable Metrics/MethodLength



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
526
527
528
529
530
531
532
533
534
535
536
537
# File 'lib/bridgetown-core/utils.rb', line 484

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

  code = <<~JAVASCRIPT
    let lastmod = 0
    function startReloadConnection() {
      const evtSource = new EventSource("#{site.base_path(strip_slash_only: true)}/_bridgetown/live_reload")
      evtSource.onmessage = event => {
        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
          }
        }
      }
      evtSource.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)
      })
      evtSource.onerror = event => {
        if (evtSource.readyState === 2) {
          // reconnect with new object
          evtSource.close()
          console.warn("Live reload: attempting to reconnect in 3 seconds...")

          setTimeout(() => startReloadConnection(), 3000)
        }
      }
    }
    setTimeout(() => {
      startReloadConnection()
    }, 500)
  JAVASCRIPT

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

#log_frontend_asset_error(site, asset_type) ⇒ Object



430
431
432
433
434
435
436
437
438
439
440
441
442
# File 'lib/bridgetown-core/utils.rb', line 430

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)


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

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



293
294
295
296
297
298
299
300
301
302
# File 'lib/bridgetown-core/utils.rb', line 293

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



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

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_WEBPACK_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



398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
# File 'lib/bridgetown-core/utils.rb', line 398

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)


351
352
353
354
355
356
357
358
359
360
361
362
363
364
# File 'lib/bridgetown-core/utils.rb', line 351

def parse_frontend_manifest_file(site, asset_type)
  case frontend_bundler_type(site.root_dir)
  when :webpack
    parse_webpack_manifest_file(site, asset_type)
  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

#parse_webpack_manifest_file(site, asset_type) ⇒ String?

Return an asset path based on the Webpack manifest file

Parameters:

  • site (Bridgetown::Site)

    The current site object

  • asset_type (String)

    js or css, or filename in manifest

Returns:

  • (String)

    Returns "MISSING_WEBPACK_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



374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
# File 'lib/bridgetown-core/utils.rb', line 374

def parse_webpack_manifest_file(site, asset_type)
  return log_frontend_asset_error(site, "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_frontend_asset_error(site, asset_type) if asset_path.nil?

  static_frontend_path site, ["js", asset_path]
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)


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

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



307
308
309
310
311
312
313
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
# File 'lib/bridgetown-core/utils.rb', line 307

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



280
281
282
283
284
285
286
287
288
289
# File 'lib/bridgetown-core/utils.rb', line 280

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.



190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/bridgetown-core/utils.rb', line 190

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 = []) ⇒ Object



420
421
422
423
424
425
426
427
428
# File 'lib/bridgetown-core/utils.rb', line 420

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.



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

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

#update_esbuild_autogenerated_config(config) ⇒ Object



454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
# File 'lib/bridgetown-core/utils.rb', line 454

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



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

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.



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

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