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::ImmutableSetOf, Contracts::ImmutableSortedSetOf, Contracts::OldResourceList, Contracts::PATH_MATCHER, Contracts::ResourceList, Contracts::VectorOf

Class Method Summary collapse

Methods included from Contracts

#Contract

Class Method Details

.all_files_under(path, &ignore) ⇒ Object


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

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


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

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 << ".#{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)

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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'middleman-core/lib/middleman-core/util/paths.rb', line 111

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


280
281
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
# File 'middleman-core/lib/middleman-core/util/binary.rb', line 280

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


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

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


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

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

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

  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.


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

def current_directory
  result = ::Dir.pwd

  return result unless RUBY_PLATFORM =~ /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.


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

Contract RespondTo[:each] => String

.extract_response_text(response) ⇒ Object


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

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
end

.file_contents_include_binary_bytes?(filename) ⇒ Boolean


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

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

108
109
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
# File 'middleman-core/lib/middleman-core/util/files.rb', line 108

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


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

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.


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

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

  return results unless RUBY_PLATFORM =~ /darwin/

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

.hash_file(path) ⇒ Object


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

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


159
160
161
# File 'middleman-core/lib/middleman-core/util/files.rb', line 159

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

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

Facade for ActiveSupport/Notification


16
17
18
19
# File 'middleman-core/lib/middleman-core/util.rb', line 16

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


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

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


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

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

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

.parse_uri(uri) ⇒ Object


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

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

.path_match(matcher, path) ⇒ Object


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

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.


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

Contract PATH_MATCHER, String => Bool

.possible_delim_openers(frontmatter_delims) ⇒ Object


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

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


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

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

.recursively_enhance(obj) ⇒ Object


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

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


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

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 << '/' if resource_url.end_with?('/') && !relative_path.end_with?('/')

    relative_path
  else
    resource_url
  end
end

.remove_templating_extensions(path) ⇒ Object


80
81
82
# File 'middleman-core/lib/middleman-core/util/files.rb', line 80

def remove_templating_extensions(path)
  step_through_extensions(path)
end

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


24
25
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
# File 'middleman-core/lib/middleman-core/util/rack.rb', line 24

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 << 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 !~ /^[^\/].*[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


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

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


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

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

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

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


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

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

.strip_leading_slash(path) ⇒ Object


45
46
47
# File 'middleman-core/lib/middleman-core/util/paths.rb', line 45

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

.tilt_class(path) ⇒ Object


25
26
27
# File 'middleman-core/lib/middleman-core/util/paths.rb', line 25

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

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


157
158
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
# File 'middleman-core/lib/middleman-core/util/paths.rb', line 157

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