Module: Bridgetown::Utils

Extended by:
Utils
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/ruby_exec.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: Aux, 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

#chomp_locale_suffix!(path, locale) ⇒ Object



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

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.



48
49
50
# File 'bridgetown-core/lib/bridgetown-core/utils.rb', line 48

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.



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

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



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

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)


485
486
487
488
489
# File 'bridgetown-core/lib/bridgetown-core/utils.rb', line 485

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)


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

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

#frontend_bundler_type(cwd = Dir.pwd) ⇒ Object



373
374
375
376
377
378
379
# File 'bridgetown-core/lib/bridgetown-core/utils.rb', line 373

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

#has_rbfm_header?(file) ⇒ Boolean

rubocop: disable Naming/PredicateName

Returns:

  • (Boolean)


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

def has_rbfm_header?(file) # rubocop: disable Naming/PredicateName
  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

Returns:

  • (Boolean)

    if the YAML front matter is present.



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

def has_yaml_header?(file) # rubocop: disable Naming/PredicateName
  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



411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
# File 'bridgetown-core/lib/bridgetown-core/utils.rb', line 411

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



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

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

Returns:

  • (Boolean)


69
70
71
# File 'bridgetown-core/lib/bridgetown-core/utils.rb', line 69

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



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

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



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

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



327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
# File 'bridgetown-core/lib/bridgetown-core/utils.rb', line 327

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)


306
307
308
309
310
311
312
313
314
315
316
317
# File 'bridgetown-core/lib/bridgetown-core/utils.rb', line 306

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)


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

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



262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
# File 'bridgetown-core/lib/bridgetown-core/utils.rb', line 262

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



235
236
237
238
239
240
241
242
243
244
# File 'bridgetown-core/lib/bridgetown-core/utils.rb', line 235

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.



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

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



349
350
351
352
353
354
355
356
357
# File 'bridgetown-core/lib/bridgetown-core/utils.rb', line 349

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.



20
21
22
# File 'bridgetown-core/lib/bridgetown-core/utils.rb', line 20

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

#unencode_uri(path) ⇒ Object



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

def unencode_uri(path)
  path = path.encode("utf-8")
  return path unless path.include?("%")

  Addressable::URI.unencode(path)
end

#update_esbuild_autogenerated_config(config) ⇒ Object



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

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



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

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.



34
35
36
# File 'bridgetown-core/lib/bridgetown-core/utils.rb', line 34

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