Module: Bridgetown::Utils

Extended by:
Utils
Included in:
Utils
Defined in:
lib/bridgetown-core/utils.rb,
lib/bridgetown-core/utils/aux.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/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



472
473
474
475
476
477
478
479
480
# File 'lib/bridgetown-core/utils.rb', line 472

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 '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) ⇒ Hash

Merges a master hash with another hash, recursively. This code was lovingly stolen from some random gem: https://rubygems.org/gems/tartan Thanks to whoever made it.

Parameters:

  • target (Hash)

    the "parent" hash whose values will be overridden

  • overwrite (Hash)

    the other hash whose values will be persisted after the merge

Returns:

  • (Hash)


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



399
400
401
402
403
404
405
406
# File 'lib/bridgetown-core/utils.rb', line 399

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)


482
483
484
485
486
# File 'lib/bridgetown-core/utils.rb', line 482

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)


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

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

#frontend_bundler_type(cwd = Dir.pwd) ⇒ Object



370
371
372
373
374
375
376
# File 'lib/bridgetown-core/utils.rb', line 370

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)


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

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.



122
123
124
125
126
127
128
# File 'lib/bridgetown-core/utils.rb', line 122

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



408
409
410
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
# File 'lib/bridgetown-core/utils.rb', line 408

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



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

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)


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

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

Returns:

  • (Hash)

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



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

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

Parse a date/time and throw an error if invalid

Parameters:

  • input (String)

    the date/time to parse

  • msg (String) (defaults to: "Input could not be parsed.")

    the error message to show the user

Returns:

  • (Time)

    the parsed date if successful, throws a FatalException if not



113
114
115
116
117
# File 'lib/bridgetown-core/utils.rb', line 113

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



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

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)


303
304
305
306
307
308
309
310
311
312
313
314
# File 'lib/bridgetown-core/utils.rb', line 303

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)


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

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

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

Parameters:

  • input (String)

Returns:

  • (String)


259
260
261
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
# File 'lib/bridgetown-core/utils.rb', line 259

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

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"]

Parameters:

  • dir (String)

    the dir where glob will be executed under (the dir will be included to each result)

  • patterns (String, Array<String>)

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

  • flags (Integer) (defaults to: 0)

    the flags which will be applied to the pattern, a bitwise OR of the File::FNM_XXX constants

Returns:

  • (Array<String>)

    matched pathes



230
231
232
233
234
235
236
237
238
239
# File 'lib/bridgetown-core/utils.rb', line 230

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

Slugify a filename or title

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"

Parameters:

  • string (String)

    filename or title to slugify

  • mode (String) (defaults to: nil)

    how string is slugified

  • cased (Boolean) (defaults to: false)

    whether to replace all uppercase letters with their lowercase counterparts

Returns:

  • (String)

    the slugified string.



182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/bridgetown-core/utils.rb', line 182

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



346
347
348
349
350
351
352
353
354
# File 'lib/bridgetown-core/utils.rb', line 346

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



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

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



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

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 'lib/bridgetown-core/utils.rb', line 34

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