Module: Middleman::Util

Extended by:
Memoist
Includes:
Contracts
Defined in:
middleman-core/lib/middleman-core/util.rb,
middleman-core/lib/middleman-core/util/data.rb,
middleman-core/lib/middleman-core/util/rack.rb,
middleman-core/lib/middleman-core/util/files.rb,
middleman-core/lib/middleman-core/util/paths.rb,
middleman-core/lib/middleman-core/util/binary.rb,
middleman-core/lib/middleman-core/util/uri_templates.rb

Defined Under Namespace

Modules: Data, UriTemplates Classes: BlogTemplateProcessor, EnhancedHash

Constant Summary collapse

IGNORE_DESCRIPTOR =
Or[Regexp, RespondTo[:call], String]
IGNORED_ASSET_EXTENSIONS =
Set.new %i[images fonts]

Constants included from Contracts

Contracts::ImmutableSortedSetOf, Contracts::PATH_MATCHER

Class Method Summary collapse

Methods included from Contracts

#Contract

Class Method Details

.all_files_under(path, &ignore) ⇒ Object


22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'middleman-core/lib/middleman-core/util/files.rb', line 22

def all_files_under(path, &ignore)
  path = Pathname(path)

  if ignore && yield(path)
    []
  elsif path.directory?
    path.children.flat_map do |child|
      all_files_under(child, &ignore)
    end.compact
  elsif path.file?
    [path]
  else
    []
  end
end

.asset_path(app, kind, source, options_hash = ::Middleman::EMPTY_HASH) ⇒ Object


82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'middleman-core/lib/middleman-core/util/paths.rb', line 82

def asset_path(app, kind, source, options_hash = ::Middleman::EMPTY_HASH)
  return source if source.to_s.include?('//') || source.to_s.start_with?('data:')

  asset_folder = case kind
                 when :css
                   app.config[:css_dir]
                 when :js
                   app.config[:js_dir]
                 when :images
                   app.config[:images_dir]
                 when :fonts
                   app.config[:fonts_dir]
                 else
                   kind.to_s
                 end

  source = source.to_s.tr(' ', '')
  ignore_extension = IGNORED_ASSET_EXTENSIONS.include? kind # don't append extension
  source = "#{source}.#{kind}" unless ignore_extension || source.end_with?(".#{kind}")
  asset_folder = '' if source.start_with?('/') # absolute path

  asset_url(app, source, asset_folder, options_hash)
end

.asset_url(app, path, prefix = '', options_hash = ::Middleman::EMPTY_HASH) ⇒ Object

Raises:

  • (ArgumentError)

113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'middleman-core/lib/middleman-core/util/paths.rb', line 113

def asset_url(app, path, prefix = '', options_hash = ::Middleman::EMPTY_HASH)
  # Don't touch assets which already have a full path
  return path if path.include?('//') || path.start_with?('data:')

  raise ArgumentError, '#asset_url must be run in a context with current_resource if relative: true' if options_hash[:relative] && !options_hash[:current_resource]

  uri = ::Middleman::Util.parse_uri(path)
  path = uri.path

  # Ensure the url we pass into by_destination_path is not a
  # relative path, since it only takes absolute url paths.
  dest_path = url_for(app, path, options_hash.merge(relative: false))

  resource = app.sitemap.by_path(dest_path) || app.sitemap.by_destination_path(dest_path)

  result = if resource
             resource.url
           else
             path = ::File.join(prefix, path)
             resource = app.sitemap.by_path(path)

             if resource
               resource.url
             else
               ::File.join(app.config[:http_prefix], path)
             end
           end

  final_result = ::Addressable::URI.encode(
    relative_path_from_resource(
      options_hash[:current_resource],
      result,
      options_hash[:relative]
    )
  )

  result_uri = ::Middleman::Util.parse_uri(final_result)
  result_uri.query = uri.query
  result_uri.fragment = uri.fragment
  result_uri.to_s
end

.binary?(filename) ⇒ Boolean

Returns:

  • (Boolean)

282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
# File 'middleman-core/lib/middleman-core/util/binary.rb', line 282

def binary?(filename)
  @binary_cache ||= {}

  return @binary_cache[filename] if @binary_cache.key?(filename)

  @binary_cache[filename] = begin
    path = Pathname(filename)
    ext = path.extname
    without_dot = ext.sub('.', '')

    # We hardcode detecting of gzipped SVG files
    if KNOWN_BINARY_FILE_EXTENSIONS.include?(without_dot)
      true
    elsif ::Tilt.registered?(without_dot)
      false
    else
      dot_ext = ext.to_s[0] == '.' ? ext.dup : ".#{ext}"

      mime = ::Rack::Mime.mime_type(dot_ext, nil)

      if mime
        !nonbinary_mime?(mime)
      else
        file_contents_include_binary_bytes?(path.to_s)
      end
    end
  end
end

.collect_extensions(path) ⇒ Object


90
91
92
93
94
95
96
97
98
99
100
101
# File 'middleman-core/lib/middleman-core/util/files.rb', line 90

def collect_extensions(path)
  @@extensions_cache ||= {}

  base_name = ::File.basename(path)
  @@extensions_cache[base_name] ||= begin
    result = []

    step_through_extensions(base_name) { |e| result << e } unless base_name.start_with?('.')

    result
  end
end

.contains_frontmatter?(path, frontmatter_delims) ⇒ Boolean

Returns:

  • (Boolean)

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

def contains_frontmatter?(path, frontmatter_delims)
  file = ::File.open(path)
  first_line = file.gets

  first_line = file.gets if /\A(?:[^\r\n]*coding:[^\r\n]*\r?\n)/.match?(first_line)

  file.close

  possible_openers = possible_delim_openers(frontmatter_delims)
  !first_line.nil? && !first_line.match(possible_openers).nil?
rescue EOFError, IOError, ::Errno::ENOENT
  false
end

.current_directoryArray<String>

Get the PWD and try to keep path encoding consistent.

Parameters:

  • path (String)

    The glob path.

Returns:


54
55
56
57
58
59
60
# File 'middleman-core/lib/middleman-core/util/files.rb', line 54

def current_directory
  result = ::Dir.pwd

  return result unless RUBY_PLATFORM.match?(/darwin/)

  result.encode('UTF-8', 'UTF-8-MAC')
end

.eachString

Extract the text of a Rack response as a string. Useful for extensions implemented as Rack middleware.

Parameters:

  • response

    The response from #call

Returns:

  • (String)

    The whole response as a string.


15
# File 'middleman-core/lib/middleman-core/util/rack.rb', line 15

Contract RespondTo[:each] => String

.extract_response_text(response) ⇒ Object


16
17
18
19
20
21
22
23
# File 'middleman-core/lib/middleman-core/util/rack.rb', line 16

def extract_response_text(response)
  # The rack spec states all response bodies must respond to each
  result = []
  response.each do |part, _|
    result << part
  end
  result.join
end

.file_contents_include_binary_bytes?(filename) ⇒ Boolean

Returns:

  • (Boolean)

335
336
337
338
339
340
341
342
343
# File 'middleman-core/lib/middleman-core/util/binary.rb', line 335

def file_contents_include_binary_bytes?(filename)
  binary_bytes = [0, 1, 2, 3, 4, 5, 6, 11, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 28, 29, 30, 31]
  s = ::File.read(filename, 4096) || ''
  s.each_byte do |c|
    return true if binary_bytes.include?(c)
  end

  false
end

110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'middleman-core/lib/middleman-core/util/files.rb', line 110

def find_related_files(app, files)
  return [] if files.empty?

  file_set = ::Set.new(files)

  all_extensions = files.flat_map { |f| collect_extensions(f.to_s) }
  sass_type_aliasing = ['.scss', '.sass']
  erb_type_aliasing = ['.erb', '.haml', '.slim']

  all_extensions |= sass_type_aliasing unless (all_extensions & sass_type_aliasing).empty?
  all_extensions |= erb_type_aliasing unless (all_extensions & erb_type_aliasing).empty?

  all_extensions.uniq!

  app.sitemap.without_ignored.to_a.select do |r|
    if r.file_descriptor
      local_extensions = collect_extensions(r.file_descriptor[:full_path].to_s)
      local_extensions |= sass_type_aliasing unless (local_extensions & sass_type_aliasing).empty?
      local_extensions |= erb_type_aliasing unless (local_extensions & erb_type_aliasing).empty?

      local_extensions.uniq!

      !(all_extensions & local_extensions).empty? && !file_set.include?(r.file_descriptor[:full_path])
    else
      false
    end
  end.map(&:file_descriptor)
end

.full_path(path, app) ⇒ Object


252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
# File 'middleman-core/lib/middleman-core/util/paths.rb', line 252

def full_path(path, app)
  resource = app.sitemap.by_destination_path(path)

  unless resource
    # Try it with /index.html at the end
    indexed_path = ::File.join(path.sub(%r{/$}, ''), app.config[:index_file])
    resource = app.sitemap.by_destination_path(indexed_path)
  end

  if resource
    "/#{resource.destination_path}"
  else
    "/#{normalize_path(path)}"
  end
end

.glob_directory(path) ⇒ Array<String>

Glob a directory and try to keep path encoding consistent.

Parameters:

  • path (String)

    The glob path.

Returns:


42
43
44
45
46
47
48
# File 'middleman-core/lib/middleman-core/util/files.rb', line 42

def glob_directory(path)
  results = ::Dir[path]

  return results unless RUBY_PLATFORM.match?(/darwin/)

  results.map { |r| r.encode('UTF-8', 'UTF-8-MAC') }
end

.hash_file(path) ⇒ Object


146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'middleman-core/lib/middleman-core/util/files.rb', line 146

def hash_file(path)
  # puts "Read (hash): #{path}"

  if ENV['MIDDLEMAN_SHELL_OUT_TO_GIT_HASH'] == 'true'
    output, status = ::Open3.capture2e("git hash-object #{::Shellwords.escape(path)}")

    raise "Failed to get hash for '#{path}' from git." if status.exitstatus != 0 || output.empty?

    output.strip
  else
    ::Digest::SHA1.file(path).hexdigest
  end
end

.hash_string(data) ⇒ Object


161
162
163
# File 'middleman-core/lib/middleman-core/util/files.rb', line 161

def hash_string(data)
  ::Digest::SHA1.hexdigest(data)
end

.instrument(name, payload = {}, &block) ⇒ Object

Facade for ActiveSupport/Notification


18
19
20
21
# File 'middleman-core/lib/middleman-core/util.rb', line 18

def instrument(name, payload = {}, &block)
  suffixed_name = /\.middleman$/.match?(name) ? name.dup : "#{name}.middleman"
  ::ActiveSupport::Notifications.instrument(suffixed_name, payload, &block)
end

.nonbinary_mime?(mime) ⇒ Boolean

Returns:

  • (Boolean)

316
317
318
319
320
321
322
323
324
325
326
327
328
# File 'middleman-core/lib/middleman-core/util/binary.rb', line 316

def nonbinary_mime?(mime)
  if mime.start_with?('text/')
    true
  elsif mime.include?('xml') && !mime.include?('officedocument')
    true
  elsif mime.include?('json')
    true
  elsif mime.include?('javascript')
    true
  else
    false
  end
end

.normalize_path(path) ⇒ Object


36
37
38
39
40
41
# File 'middleman-core/lib/middleman-core/util/paths.rb', line 36

def normalize_path(path)
  return path unless path.is_a?(String)

  # The tr call works around a bug in Ruby's Unicode handling
  WEBrick::HTTPUtils.unescape(path).sub(%r{^/}, '').tr('', '')
end

.parse_uri(uri) ⇒ Object


21
22
23
# File 'middleman-core/lib/middleman-core/util/paths.rb', line 21

def parse_uri(uri)
  ::Addressable::URI.parse(uri)
end

.path_match(matcher, path) ⇒ Object


302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
# File 'middleman-core/lib/middleman-core/util/paths.rb', line 302

def path_match(matcher, path)
  if matcher.is_a?(String)
    if matcher.include? '*'
      ::File.fnmatch(matcher, path)
    else
      path == matcher
    end
  elsif matcher.respond_to?(:match)
    !matcher.match(path).nil?
  elsif matcher.respond_to?(:call)
    matcher.call(path)
  else
    ::File.fnmatch(matcher.to_s, path)
  end
end

.PATH_MATCHERBoolean

Takes a matcher, which can be a literal string or a string containing glob expressions, or a regexp, or a proc, or anything else that responds to #match or #call, and returns whether or not the given path matches that matcher.

Parameters:

  • matcher (String, #match, #call)

    A matcher String, RegExp, Proc, etc.

  • path (String)

    A path as a string

Returns:

  • (Boolean)

    Whether the path matches the matcher


301
# File 'middleman-core/lib/middleman-core/util/paths.rb', line 301

Contract PATH_MATCHER, String => Bool

.possible_delim_openers(frontmatter_delims) ⇒ Object


374
375
376
377
378
379
380
381
382
# File 'middleman-core/lib/middleman-core/util/binary.rb', line 374

def possible_delim_openers(frontmatter_delims)
  all_possible = frontmatter_delims
                 .values
                 .flatten(1)
                 .map(&:first)
                 .uniq

  /\A#{::Regexp.union(all_possible)}/
end

.read_file(path, bytes = nil) ⇒ Object


140
141
142
143
# File 'middleman-core/lib/middleman-core/util/files.rb', line 140

def read_file(path, bytes = nil)
  # puts "Read: #{path}"
  File.read(path, bytes)
end

.recursively_enhance(obj) ⇒ Object


30
31
32
33
34
35
36
37
38
# File 'middleman-core/lib/middleman-core/util/data.rb', line 30

def recursively_enhance(obj)
  if obj.is_a? ::Array
    obj.map { |e| recursively_enhance(e) }
  elsif obj.is_a? ::Hash
    EnhancedHash.new(obj)
  else
    obj
  end
end

.relative_path_from_resource(curr_resource, resource_url, relative) ⇒ Object


275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
# File 'middleman-core/lib/middleman-core/util/paths.rb', line 275

def relative_path_from_resource(curr_resource, resource_url, relative)
  # Switch to the relative path between resource and the given resource
  # if we've been asked to.
  if relative
    # Output urls relative to the destination path, not the source path
    current_dir = Pathname("/#{curr_resource.destination_path}").dirname
    relative_path = Pathname(resource_url).relative_path_from(current_dir).to_s

    # Put back the trailing slash to avoid unnecessary Apache redirects
    relative_path = "#{relative_path}/" if resource_url.end_with?('/') && !relative_path.end_with?('/')

    relative_path
  else
    resource_url
  end
end

.remove_templating_extensions(path) ⇒ Object


82
83
84
# File 'middleman-core/lib/middleman-core/util/files.rb', line 82

def remove_templating_extensions(path)
  step_through_extensions(path)
end

.rewrite_paths(body, path, exts, app, &_block) ⇒ Object


26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'middleman-core/lib/middleman-core/util/rack.rb', line 26

def rewrite_paths(body, path, exts, app, &_block)
  sorted_exts = exts.to_a.sort_by { |ext| -ext.size }
  matcher = /([\'\"\(,]\s*|# sourceMappingURL=)([^\s\'\"\)\(>]+(#{::Regexp.union(sorted_exts)}))/

  url_fn_prefix = 'url('

  body.dup.gsub(matcher) do |match|
    opening_character = Regexp.last_match(1)
    asset_path = Regexp.last_match(2)

    if asset_path.start_with?(url_fn_prefix)
      opening_character = "#{opening_character}#{url_fn_prefix}"
      asset_path = asset_path[url_fn_prefix.length..-1]
    end

    current_resource = app.sitemap.by_destination_path(path)

    begin
      uri = ::Middleman::Util.parse_uri(asset_path)

      if uri.relative? && uri.host.nil? && asset_path !~ %r{^[^/].*[a-z]+\.[a-z]+/.*}
        dest_path = ::Middleman::Util.url_for(app, asset_path, relative: false, current_resource: current_resource)

        resource = app.sitemap.by_destination_path(dest_path)

        if resource && (result = yield(asset_path))
          "#{opening_character}#{result}"
        else
          match
        end
      else
        match
      end
    rescue ::Addressable::URI::InvalidURIError
      match
    end
  end
end

.should_ignore?(validator, value) ⇒ Boolean

Returns:

  • (Boolean)

55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'middleman-core/lib/middleman-core/util/paths.rb', line 55

def should_ignore?(validator, value)
  if validator.is_a? Regexp
    # Treat as Regexp
    !validator.match(value).nil?
  elsif validator.respond_to? :call
    # Treat as proc
    validator.call(value)
  elsif validator.is_a? String
    # Treat as glob
    File.fnmatch(value, validator)
  else
    # If some unknown thing, don't ignore
    false
  end
end

.static_file?(path, frontmatter_delims) ⇒ Boolean

Returns:

  • (Boolean)

346
347
348
349
350
351
352
353
354
355
356
# File 'middleman-core/lib/middleman-core/util/binary.rb', line 346

def static_file?(path, frontmatter_delims)
  path = Pathname(path)
  ext = path.extname
  without_dot = ext.sub('.', '')

  if KNOWN_NON_STATIC_FILE_EXTENSIONS.include?(without_dot) || contains_frontmatter?(path, frontmatter_delims)
    false
  else
    !::Tilt.registered?(without_dot)
  end
end

.step_through_extensions(path) {|::File.extname(path)| ... } ⇒ Object

Yields:

  • (::File.extname(path))

63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'middleman-core/lib/middleman-core/util/files.rb', line 63

def step_through_extensions(path)
  while (ext = File.extname(path))
    break if ext.empty? || !::Middleman::Util.tilt_class(ext)

    yield ext if block_given?

    # Strip templating extensions as long as Tilt knows them
    path = path[0..-(ext.length + 1)]
  end

  yield ::File.extname(path) if block_given?

  path
end

.StringString

Expand a path to include the index file if it's a directory

Parameters:

Returns:

  • (String)

    Path with index file if necessary.


251
# File 'middleman-core/lib/middleman-core/util/paths.rb', line 251

Contract String, ::Middleman::Application => String

.strip_leading_slash(path) ⇒ Object


47
48
49
# File 'middleman-core/lib/middleman-core/util/paths.rb', line 47

def strip_leading_slash(path)
  path.sub(%r{^/}, '')
end

.tilt_class(path) ⇒ Object


27
28
29
# File 'middleman-core/lib/middleman-core/util/paths.rb', line 27

def tilt_class(path)
  ::Tilt[path]
end

.url_for(app, path_or_resource, options_hash = ::Middleman::EMPTY_HASH) ⇒ Object


159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'middleman-core/lib/middleman-core/util/paths.rb', line 159

def url_for(app, path_or_resource, options_hash = ::Middleman::EMPTY_HASH)
  if path_or_resource.is_a?(String) || path_or_resource.is_a?(Symbol)
    r = app.sitemap.by_page_id(path_or_resource)

    path_or_resource = r || path_or_resource.to_s
  end

  # Handle Resources and other things which define their own url method
  url = if path_or_resource.respond_to?(:url)
          path_or_resource.url
        else
          path_or_resource.dup
        end

  # Try to parse URL
  begin
    uri = ::Middleman::Util.parse_uri(url)
  rescue ::Addressable::URI::InvalidURIError
    # Nothing we can do with it, it's not really a URI
    return url
  end

  relative = options_hash[:relative]
  raise "Can't use the relative option with an external URL" if relative && uri.host

  # Allow people to turn on relative paths for all links with
  # set :relative_links, true
  # but still override on a case by case basis with the :relative parameter.
  effective_relative = relative || false
  effective_relative = true if relative.nil? && app.config[:relative_links]

  # Try to find a sitemap resource corresponding to the desired path
  this_resource = options_hash[:current_resource]

  if path_or_resource.is_a?(::Middleman::Sitemap::Resource)
    resource = path_or_resource
    resource_url = url
  elsif this_resource && uri.path && !uri.host
    # Handle relative urls
    url_path = Pathname(uri.path)
    current_source_dir = Pathname("/#{this_resource.path}").dirname
    url_path = current_source_dir.join(url_path) if url_path.relative?
    resource = app.sitemap.by_path(url_path.to_s)
    if resource
      resource_url = resource.url
    else
      # Try to find a resource relative to destination paths
      url_path = Pathname(uri.path)
      current_source_dir = Pathname("/#{this_resource.destination_path}").dirname
      url_path = current_source_dir.join(url_path) if url_path.relative?
      resource = app.sitemap.by_destination_path(url_path.to_s)
      resource_url = resource.url if resource
    end
  elsif options_hash[:find_resource] && uri.path && !uri.host
    resource = app.sitemap.by_path(uri.path)
    resource_url = resource.url if resource
  end

  if resource
    uri.path = if this_resource
                 ::Addressable::URI.encode(
                   relative_path_from_resource(
                     this_resource,
                     resource_url,
                     effective_relative
                   )
                 )
               else
                 resource_url
               end
  end

  # Support a :query option that can be a string or hash
  query = options_hash[:query]

  if query
    uri.query = query.respond_to?(:to_param) ? query.to_param : query.to_s
  end

  # Support a :fragment or :anchor option just like Padrino
  fragment = options_hash[:anchor] || options_hash[:fragment]
  uri.fragment = fragment.to_s if fragment

  # Finally make the URL back into a string
  uri.to_s
end