middleman-core 4.1.0.rc.2 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/features/asset_hash.feature +30 -32
  3. data/features/asset_host.feature +2 -0
  4. data/features/gzip.feature +1 -1
  5. data/features/import_files.feature +0 -2
  6. data/features/nested_layouts.feature +20 -17
  7. data/fixtures/asset-host-app/source/javascripts/asset_host.js +2 -0
  8. data/fixtures/frontmatter-neighbor-app/config.rb +1 -1
  9. data/fixtures/frontmatter-settings-neighbor-app/config.rb +1 -1
  10. data/fixtures/nested-layout-app/source/layouts/inner.erb +5 -2
  11. data/fixtures/nested-layout-app/source/layouts/inner_haml.haml +6 -2
  12. data/fixtures/nested-layout-app/source/layouts/inner_slim.slim +6 -2
  13. data/fixtures/nested-layout-app/source/layouts/master.erb +7 -1
  14. data/fixtures/nested-layout-app/source/layouts/master_haml.haml +5 -1
  15. data/fixtures/nested-layout-app/source/layouts/master_slim.slim +5 -1
  16. data/fixtures/nested-layout-app/source/layouts/outer.erb +6 -2
  17. data/fixtures/nested-layout-app/source/layouts/outer_haml.haml +5 -1
  18. data/fixtures/nested-layout-app/source/layouts/outer_slim.slim +5 -1
  19. data/lib/middleman-core.rb +0 -3
  20. data/lib/middleman-core/application.rb +7 -9
  21. data/lib/middleman-core/builder.rb +88 -44
  22. data/lib/middleman-core/contracts.rb +102 -13
  23. data/lib/middleman-core/core_extensions/data.rb +15 -10
  24. data/lib/middleman-core/core_extensions/default_helpers.rb +15 -6
  25. data/lib/middleman-core/core_extensions/file_watcher.rb +2 -2
  26. data/lib/middleman-core/core_extensions/front_matter.rb +11 -3
  27. data/lib/middleman-core/core_extensions/i18n.rb +1 -1
  28. data/lib/middleman-core/core_extensions/inline_url_rewriter.rb +2 -2
  29. data/lib/middleman-core/extension.rb +1 -1
  30. data/lib/middleman-core/extensions.rb +1 -1
  31. data/lib/middleman-core/extensions/asset_hash.rb +1 -1
  32. data/lib/middleman-core/extensions/asset_host.rb +1 -1
  33. data/lib/middleman-core/extensions/automatic_image_sizes.rb +1 -1
  34. data/lib/middleman-core/extensions/cache_buster.rb +1 -1
  35. data/lib/middleman-core/extensions/external_pipeline.rb +2 -1
  36. data/lib/middleman-core/extensions/gzip.rb +2 -2
  37. data/lib/middleman-core/extensions/minify_css.rb +1 -1
  38. data/lib/middleman-core/extensions/minify_javascript.rb +1 -1
  39. data/lib/middleman-core/extensions/relative_assets.rb +1 -1
  40. data/lib/middleman-core/file_renderer.rb +12 -9
  41. data/lib/middleman-core/logger.rb +1 -0
  42. data/lib/middleman-core/preview_server.rb +14 -14
  43. data/lib/middleman-core/renderers/haml.rb +3 -1
  44. data/lib/middleman-core/renderers/less.rb +1 -1
  45. data/lib/middleman-core/renderers/liquid.rb +1 -1
  46. data/lib/middleman-core/renderers/sass.rb +7 -2
  47. data/lib/middleman-core/sitemap/extensions/ignores.rb +2 -2
  48. data/lib/middleman-core/sitemap/extensions/import.rb +3 -1
  49. data/lib/middleman-core/sitemap/resource.rb +7 -6
  50. data/lib/middleman-core/sources.rb +30 -13
  51. data/lib/middleman-core/sources/source_watcher.rb +50 -12
  52. data/lib/middleman-core/step_definitions/middleman_steps.rb +2 -2
  53. data/lib/middleman-core/template_context.rb +1 -1
  54. data/lib/middleman-core/template_renderer.rb +13 -4
  55. data/lib/middleman-core/util.rb +6 -606
  56. data/lib/middleman-core/util/binary.rb +79 -0
  57. data/lib/middleman-core/util/data.rb +37 -8
  58. data/lib/middleman-core/util/files.rb +134 -0
  59. data/lib/middleman-core/util/paths.rb +251 -0
  60. data/lib/middleman-core/util/rack.rb +52 -0
  61. data/lib/middleman-core/util/uri_templates.rb +97 -0
  62. data/lib/middleman-core/version.rb +1 -1
  63. data/middleman-core.gemspec +1 -0
  64. metadata +25 -4
@@ -0,0 +1,79 @@
1
+ # Template and Mime detection
2
+ require 'tilt'
3
+ require 'rack/mime'
4
+
5
+ require 'middleman-core/contracts'
6
+
7
+ module Middleman
8
+ module Util
9
+ include Contracts
10
+
11
+ module_function
12
+
13
+ # Whether the source file is binary.
14
+ #
15
+ # @param [String] filename The file to check.
16
+ # @return [Boolean]
17
+ Contract Or[String, Pathname] => Bool
18
+ def binary?(filename)
19
+ @binary_cache ||= {}
20
+
21
+ return @binary_cache[filename] if @binary_cache.key?(filename)
22
+
23
+ @binary_cache[filename] = begin
24
+ path = Pathname(filename)
25
+ ext = path.extname
26
+
27
+ # We hardcode detecting of gzipped SVG files
28
+ if ext == '.svgz'
29
+ true
30
+ elsif ::Tilt.registered?(ext.sub('.', ''))
31
+ false
32
+ else
33
+ dot_ext = (ext.to_s[0] == '.') ? ext.dup : ".#{ext}"
34
+
35
+ if mime = ::Rack::Mime.mime_type(dot_ext, nil)
36
+ !nonbinary_mime?(mime)
37
+ else
38
+ file_contents_include_binary_bytes?(path.to_s)
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ # Is mime type known to be non-binary?
45
+ #
46
+ # @param [String] mime The mimetype to check.
47
+ # @return [Boolean]
48
+ Contract String => Bool
49
+ def nonbinary_mime?(mime)
50
+ case
51
+ when mime.start_with?('text/')
52
+ true
53
+ when mime.include?('xml') && !mime.include?('officedocument')
54
+ true
55
+ when mime.include?('json')
56
+ true
57
+ when mime.include?('javascript')
58
+ true
59
+ else
60
+ false
61
+ end
62
+ end
63
+
64
+ # Read a few bytes from the file and see if they are binary.
65
+ #
66
+ # @param [String] filename The file to check.
67
+ # @return [Boolean]
68
+ Contract String => Bool
69
+ def file_contents_include_binary_bytes?(filename)
70
+ 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]
71
+ s = ::File.read(filename, 4096) || ''
72
+ s.each_byte do |c|
73
+ return true if binary_bytes.include?(c)
74
+ end
75
+
76
+ false
77
+ end
78
+ end
79
+ end
@@ -1,12 +1,40 @@
1
1
  require 'yaml'
2
2
  require 'json'
3
3
  require 'pathname'
4
- require 'middleman-core/util'
5
- require 'middleman-core/contracts'
6
4
  require 'backports/2.1.0/array/to_h'
5
+ require 'hashie'
6
+
7
+ require 'middleman-core/util/binary'
8
+ require 'middleman-core/contracts'
7
9
 
8
10
  module Middleman
9
11
  module Util
12
+ include Contracts
13
+
14
+ module_function
15
+
16
+ class EnhancedHash < ::Hashie::Mash
17
+ # include ::Hashie::Extensions::MergeInitializer
18
+ # include ::Hashie::Extensions::MethodReader
19
+ # include ::Hashie::Extensions::IndifferentAccess
20
+ end
21
+
22
+ # Recursively convert a normal Hash into a EnhancedHash
23
+ #
24
+ # @private
25
+ # @param [Hash] data Normal hash
26
+ # @return [Hash]
27
+ Contract Any => Maybe[Or[Array, EnhancedHash]]
28
+ def recursively_enhance(obj)
29
+ if obj.is_a? ::Array
30
+ obj.map { |e| recursively_enhance(e) }
31
+ elsif obj.is_a? ::Hash
32
+ ::Hashie::Mash.new(obj)
33
+ else
34
+ obj
35
+ end
36
+ end
37
+
10
38
  module Data
11
39
  include Contracts
12
40
 
@@ -15,14 +43,15 @@ module Middleman
15
43
  # Get the frontmatter and plain content from a file
16
44
  # @param [String] path
17
45
  # @return [Array<Hash, String>]
18
- Contract Pathname, Maybe[Symbol] => [Hash, Maybe[String]]
19
- def parse(full_path, frontmatter_delims, known_type=nil)
20
- return [{}, nil] if ::Middleman::Util.binary?(full_path)
46
+ Contract IsA['Middleman::SourceFile'], Maybe[Symbol] => [Hash, Maybe[String]]
47
+ def parse(file, frontmatter_delims, known_type=nil)
48
+ full_path = file[:full_path]
49
+ return [{}, nil] if ::Middleman::Util.binary?(full_path) || file[:types].include?(:binary)
21
50
 
22
51
  # Avoid weird race condition when a file is renamed
23
52
  begin
24
- content = File.read(full_path)
25
- rescue EOFError, IOError, Errno::ENOENT
53
+ content = file.read
54
+ rescue EOFError, IOError, ::Errno::ENOENT
26
55
  return [{}, nil]
27
56
  end
28
57
 
@@ -30,7 +59,7 @@ module Middleman
30
59
  .values
31
60
  .flatten(1)
32
61
  .transpose
33
- .map(&Regexp.method(:union))
62
+ .map(&::Regexp.method(:union))
34
63
 
35
64
  match = /
36
65
  \A(?:[^\r\n]*coding:[^\r\n]*\r?\n)?
@@ -0,0 +1,134 @@
1
+ module Middleman
2
+ module Util
3
+ include Contracts
4
+
5
+ module_function
6
+
7
+ # Get a recusive list of files inside a path.
8
+ # Works with symlinks.
9
+ #
10
+ # @param path Some path string or Pathname
11
+ # @param ignore A proc/block that returns true if a given path should be ignored - if a path
12
+ # is ignored, nothing below it will be searched either.
13
+ # @return [Array<Pathname>] An array of Pathnames for each file (no directories)
14
+ Contract Or[String, Pathname], Proc => ArrayOf[Pathname]
15
+ def all_files_under(path, &ignore)
16
+ path = Pathname(path)
17
+
18
+ if ignore && yield(path)
19
+ []
20
+ elsif path.directory?
21
+ path.children.flat_map do |child|
22
+ all_files_under(child, &ignore)
23
+ end.compact
24
+ elsif path.file?
25
+ [path]
26
+ else
27
+ []
28
+ end
29
+ end
30
+
31
+ # Glob a directory and try to keep path encoding consistent.
32
+ #
33
+ # @param [String] path The glob path.
34
+ # @return [Array<String>]
35
+ def glob_directory(path)
36
+ results = ::Dir[path]
37
+
38
+ return results unless RUBY_PLATFORM =~ /darwin/
39
+
40
+ results.map { |r| r.encode('UTF-8', 'UTF-8-MAC') }
41
+ end
42
+
43
+ # Get the PWD and try to keep path encoding consistent.
44
+ #
45
+ # @param [String] path The glob path.
46
+ # @return [Array<String>]
47
+ def current_directory
48
+ result = ::Dir.pwd
49
+
50
+ return result unless RUBY_PLATFORM =~ /darwin/
51
+
52
+ result.encode('UTF-8', 'UTF-8-MAC')
53
+ end
54
+
55
+ Contract String => String
56
+ def step_through_extensions(path)
57
+ while ::Tilt[path]
58
+ ext = ::File.extname(path)
59
+ yield ext if block_given?
60
+
61
+ # Strip templating extensions as long as Tilt knows them
62
+ path = path[0..-(ext.length + 1)]
63
+ end
64
+
65
+ yield ::File.extname(path) if block_given?
66
+
67
+ path
68
+ end
69
+
70
+ # Removes the templating extensions, while keeping the others
71
+ # @param [String] path
72
+ # @return [String]
73
+ Contract String => String
74
+ def remove_templating_extensions(path)
75
+ step_through_extensions(path)
76
+ end
77
+
78
+ # Removes the templating extensions, while keeping the others
79
+ # @param [String] path
80
+ # @return [String]
81
+ Contract String => ArrayOf[String]
82
+ def collect_extensions(path)
83
+ return [] if ::File.basename(path).start_with?('.')
84
+
85
+ result = []
86
+
87
+ step_through_extensions(path) { |e| result << e }
88
+
89
+ result
90
+ end
91
+
92
+ # Finds files which should also be considered to be dirty when
93
+ # the given file(s) are touched.
94
+ #
95
+ # @param [Middleman::Application] app The app.
96
+ # @param [Pathname] files The original touched file paths.
97
+ # @return [Middleman::SourceFile] All related file paths, not including the source file paths.
98
+ Contract ::Middleman::Application, ArrayOf[Pathname] => ArrayOf[::Middleman::SourceFile]
99
+ def find_related_files(app, files)
100
+ return [] if files.empty?
101
+
102
+ all_extensions = files.flat_map { |f| collect_extensions(f.to_s) }
103
+
104
+ sass_type_aliasing = ['.scss', '.sass']
105
+ erb_type_aliasing = ['.erb', '.haml', '.slim']
106
+
107
+ if (all_extensions & sass_type_aliasing).length > 0
108
+ all_extensions |= sass_type_aliasing
109
+ end
110
+
111
+ if (all_extensions & erb_type_aliasing).length > 0
112
+ all_extensions |= erb_type_aliasing
113
+ end
114
+
115
+ all_extensions.uniq!
116
+
117
+ app.sitemap.resources.select(&:file_descriptor).select { |r|
118
+ local_extensions = collect_extensions(r.file_descriptor[:full_path].to_s)
119
+
120
+ if (local_extensions & sass_type_aliasing).length > 0
121
+ local_extensions |= sass_type_aliasing
122
+ end
123
+
124
+ if (local_extensions & erb_type_aliasing).length > 0
125
+ local_extensions |= erb_type_aliasing
126
+ end
127
+
128
+ local_extensions.uniq!
129
+
130
+ ((all_extensions & local_extensions).length > 0) && files.none? { |f| f == r.file_descriptor[:full_path] }
131
+ }.map(&:file_descriptor)
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,251 @@
1
+ # Core Pathname library used for traversal
2
+ require 'pathname'
3
+ require 'uri'
4
+
5
+ require 'middleman-core/contracts'
6
+
7
+ # rubocop:disable ModuleLength
8
+ module Middleman
9
+ module Util
10
+ include Contracts
11
+
12
+ module_function
13
+
14
+ # Normalize a path to not include a leading slash
15
+ # @param [String] path
16
+ # @return [String]
17
+ Contract String => String
18
+ def normalize_path(path)
19
+ # The tr call works around a bug in Ruby's Unicode handling
20
+ ::URI.decode(path).sub(%r{^/}, '').tr('', '')
21
+ end
22
+
23
+ # This is a separate method from normalize_path in case we
24
+ # change how we normalize paths
25
+ Contract String => String
26
+ def strip_leading_slash(path)
27
+ path.sub(%r{^/}, '')
28
+ end
29
+
30
+ # Get the path of a file of a given type
31
+ #
32
+ # @param [Middleman::Application] app The app.
33
+ # @param [Symbol] kind The type of file
34
+ # @param [String, Symbol] source The path to the file
35
+ # @param [Hash] options Data to pass through.
36
+ # @return [String]
37
+ Contract ::Middleman::Application, Symbol, Or[String, Symbol], Hash => String
38
+ def asset_path(app, kind, source, options={})
39
+ return source if source.to_s.include?('//') || source.to_s.start_with?('data:')
40
+
41
+ asset_folder = case kind
42
+ when :css
43
+ app.config[:css_dir]
44
+ when :js
45
+ app.config[:js_dir]
46
+ when :images
47
+ app.config[:images_dir]
48
+ when :fonts
49
+ app.config[:fonts_dir]
50
+ else
51
+ kind.to_s
52
+ end
53
+
54
+ source = source.to_s.tr(' ', '')
55
+ ignore_extension = (kind == :images || kind == :fonts) # don't append extension
56
+ source << ".#{kind}" unless ignore_extension || source.end_with?(".#{kind}")
57
+ asset_folder = '' if source.start_with?('/') # absolute path
58
+
59
+ asset_url(app, source, asset_folder, options)
60
+ end
61
+
62
+ # Get the URL of an asset given a type/prefix
63
+ #
64
+ # @param [String] path The path (such as "photo.jpg")
65
+ # @param [String] prefix The type prefix (such as "images")
66
+ # @param [Hash] options Data to pass through.
67
+ # @return [String] The fully qualified asset url
68
+ Contract ::Middleman::Application, String, String, Hash => String
69
+ def asset_url(app, path, prefix='', options={})
70
+ # Don't touch assets which already have a full path
71
+ return path if path.include?('//') || path.start_with?('data:')
72
+
73
+ if options[:relative] && !options[:current_resource]
74
+ raise ArgumentError, '#asset_url must be run in a context with current_resource if relative: true'
75
+ end
76
+
77
+ uri = URI(path)
78
+ path = uri.path
79
+
80
+ result = if resource = app.sitemap.find_resource_by_destination_path(url_for(app, path, options))
81
+ resource.url
82
+ else
83
+ path = ::File.join(prefix, path)
84
+ if resource = app.sitemap.find_resource_by_path(path)
85
+ resource.url
86
+ else
87
+ ::File.join(app.config[:http_prefix], path)
88
+ end
89
+ end
90
+
91
+ final_result = ::URI.encode(relative_path_from_resource(options[:current_resource], result, options[:relative]))
92
+
93
+ result_uri = URI(final_result)
94
+ result_uri.query = uri.query
95
+ result_uri.fragment = uri.fragment
96
+ result_uri.to_s
97
+ end
98
+
99
+ # Given a source path (referenced either absolutely or relatively)
100
+ # or a Resource, this will produce the nice URL configured for that
101
+ # path, respecting :relative_links, directory indexes, etc.
102
+ Contract ::Middleman::Application, Or[String, ::Middleman::Sitemap::Resource], Hash => String
103
+ def url_for(app, path_or_resource, options={})
104
+ # Handle Resources and other things which define their own url method
105
+ url = if path_or_resource.respond_to?(:url)
106
+ path_or_resource.url
107
+ else
108
+ path_or_resource.dup
109
+ end
110
+
111
+ # Try to parse URL
112
+ begin
113
+ uri = URI(url)
114
+ rescue ::URI::InvalidURIError
115
+ # Nothing we can do with it, it's not really a URI
116
+ return url
117
+ end
118
+
119
+ relative = options[:relative]
120
+ raise "Can't use the relative option with an external URL" if relative && uri.host
121
+
122
+ # Allow people to turn on relative paths for all links with
123
+ # set :relative_links, true
124
+ # but still override on a case by case basis with the :relative parameter.
125
+ effective_relative = relative || false
126
+ effective_relative = true if relative.nil? && app.config[:relative_links]
127
+
128
+ # Try to find a sitemap resource corresponding to the desired path
129
+ this_resource = options[:current_resource]
130
+
131
+ if path_or_resource.is_a?(::Middleman::Sitemap::Resource)
132
+ resource = path_or_resource
133
+ resource_url = url
134
+ elsif this_resource && uri.path && !uri.host
135
+ # Handle relative urls
136
+ url_path = Pathname(uri.path)
137
+ current_source_dir = Pathname('/' + this_resource.path).dirname
138
+ url_path = current_source_dir.join(url_path) if url_path.relative?
139
+ resource = app.sitemap.find_resource_by_path(url_path.to_s)
140
+ if resource
141
+ resource_url = resource.url
142
+ else
143
+ # Try to find a resource relative to destination paths
144
+ url_path = Pathname(uri.path)
145
+ current_source_dir = Pathname('/' + this_resource.destination_path).dirname
146
+ url_path = current_source_dir.join(url_path) if url_path.relative?
147
+ resource = app.sitemap.find_resource_by_destination_path(url_path.to_s)
148
+ resource_url = resource.url if resource
149
+ end
150
+ elsif options[:find_resource] && uri.path && !uri.host
151
+ resource = app.sitemap.find_resource_by_path(uri.path)
152
+ resource_url = resource.url if resource
153
+ end
154
+
155
+ if resource
156
+ uri.path = if this_resource
157
+ ::URI.encode(relative_path_from_resource(this_resource, resource_url, effective_relative))
158
+ else
159
+ resource_url
160
+ end
161
+ end
162
+
163
+ # Support a :query option that can be a string or hash
164
+ if query = options[:query]
165
+ uri.query = query.respond_to?(:to_param) ? query.to_param : query.to_s
166
+ end
167
+
168
+ # Support a :fragment or :anchor option just like Padrino
169
+ fragment = options[:anchor] || options[:fragment]
170
+ uri.fragment = fragment.to_s if fragment
171
+
172
+ # Finally make the URL back into a string
173
+ uri.to_s
174
+ end
175
+
176
+ # Expand a path to include the index file if it's a directory
177
+ #
178
+ # @param [String] path Request path/
179
+ # @param [Middleman::Application] app The requesting app.
180
+ # @return [String] Path with index file if necessary.
181
+ Contract String, ::Middleman::Application => String
182
+ def full_path(path, app)
183
+ resource = app.sitemap.find_resource_by_destination_path(path)
184
+
185
+ unless resource
186
+ # Try it with /index.html at the end
187
+ indexed_path = ::File.join(path.sub(%r{/$}, ''), app.config[:index_file])
188
+ resource = app.sitemap.find_resource_by_destination_path(indexed_path)
189
+ end
190
+
191
+ if resource
192
+ '/' + resource.destination_path
193
+ else
194
+ '/' + normalize_path(path)
195
+ end
196
+ end
197
+
198
+ # Get a relative path to a resource.
199
+ #
200
+ # @param [Middleman::Sitemap::Resource] curr_resource The resource.
201
+ # @param [String] resource_url The target url.
202
+ # @param [Boolean] relative If the path should be relative.
203
+ # @return [String]
204
+ Contract ::Middleman::Sitemap::Resource, String, Bool => String
205
+ def relative_path_from_resource(curr_resource, resource_url, relative)
206
+ # Switch to the relative path between resource and the given resource
207
+ # if we've been asked to.
208
+ if relative
209
+ # Output urls relative to the destination path, not the source path
210
+ current_dir = Pathname('/' + curr_resource.destination_path).dirname
211
+ relative_path = Pathname(resource_url).relative_path_from(current_dir).to_s
212
+
213
+ # Put back the trailing slash to avoid unnecessary Apache redirects
214
+ if resource_url.end_with?('/') && !relative_path.end_with?('/')
215
+ relative_path << '/'
216
+ end
217
+
218
+ relative_path
219
+ else
220
+ resource_url
221
+ end
222
+ end
223
+
224
+ # Takes a matcher, which can be a literal string
225
+ # or a string containing glob expressions, or a
226
+ # regexp, or a proc, or anything else that responds
227
+ # to #match or #call, and returns whether or not the
228
+ # given path matches that matcher.
229
+ #
230
+ # @param [String, #match, #call] matcher A matcher String, RegExp, Proc, etc.
231
+ # @param [String] path A path as a string
232
+ # @return [Boolean] Whether the path matches the matcher
233
+ Contract PATH_MATCHER, String => Bool
234
+ def path_match(matcher, path)
235
+ case
236
+ when matcher.is_a?(String)
237
+ if matcher.include? '*'
238
+ ::File.fnmatch(matcher, path)
239
+ else
240
+ path == matcher
241
+ end
242
+ when matcher.respond_to?(:match)
243
+ !!(path =~ matcher)
244
+ when matcher.respond_to?(:call)
245
+ matcher.call(path)
246
+ else
247
+ ::File.fnmatch(matcher.to_s, path)
248
+ end
249
+ end
250
+ end
251
+ end