middleman-core 4.0.0.alpha.2 → 4.0.0.alpha.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/features/asset_hash.feature +26 -8
  3. data/features/chained_templates.feature +88 -1
  4. data/features/cli_init.feature +0 -18
  5. data/features/collections.feature +145 -0
  6. data/features/console.feature +11 -0
  7. data/features/front-matter.feature +11 -0
  8. data/features/paginate.feature +204 -0
  9. data/features/partials.feature +0 -5
  10. data/features/slim.feature +1 -1
  11. data/features/working_directory.feature +33 -0
  12. data/fixtures/asset-hash-app/source/api.json.erb +1 -0
  13. data/fixtures/asset-hash-app/source/images/200px.jpg +0 -0
  14. data/fixtures/asset-hash-app/source/images/300px.jpg +0 -0
  15. data/fixtures/asset-hash-app/source/index.html.erb +1 -1
  16. data/fixtures/asset-hash-app/source/subdir/api.json.erb +1 -0
  17. data/fixtures/collections-app/config.rb +16 -0
  18. data/fixtures/collections-app/source/blog1/2011-01-01-new-article.html.markdown +7 -0
  19. data/fixtures/collections-app/source/blog1/2011-01-02-another-article.html.markdown +9 -0
  20. data/fixtures/collections-app/source/blog2/2011-01-01-new-article.html.markdown +7 -0
  21. data/fixtures/collections-app/source/blog2/2011-01-02-another-article.html.markdown +8 -0
  22. data/fixtures/collections-app/source/index.html.erb +26 -0
  23. data/fixtures/frontmatter-app/source/front-matter-pandoc.html.md.erb +13 -0
  24. data/fixtures/generator-test/config.rb +2 -7
  25. data/fixtures/multiple-data-sources-app/source/index.html.erb +0 -5
  26. data/fixtures/{more-instance-vars-app → paginate-app}/config.rb +0 -0
  27. data/fixtures/paginate-app/source/archive/2011/index.html.erb +20 -0
  28. data/fixtures/paginate-app/source/blog/2011-01-01-test-article.html.markdown +6 -0
  29. data/fixtures/paginate-app/source/blog/2011-01-02-test-article.html.markdown +6 -0
  30. data/fixtures/paginate-app/source/blog/2011-01-03-test-article.html.markdown +6 -0
  31. data/fixtures/paginate-app/source/blog/2011-01-04-test-article.html.markdown +6 -0
  32. data/fixtures/paginate-app/source/blog/2011-01-05-test-article.html.markdown +6 -0
  33. data/fixtures/paginate-app/source/blog/2011-02-01-test-article.html.markdown +6 -0
  34. data/fixtures/paginate-app/source/blog/2011-02-02-test-article.html.markdown +6 -0
  35. data/fixtures/paginate-app/source/index.html.erb +15 -0
  36. data/fixtures/paginate-app/source/tag.html.erb +23 -0
  37. data/fixtures/partial-chained_templates-app/config.rb +0 -0
  38. data/lib/middleman-core/application.rb +28 -1
  39. data/lib/middleman-core/builder.rb +2 -0
  40. data/lib/middleman-core/contracts.rb +19 -1
  41. data/lib/middleman-core/core_extensions.rb +5 -0
  42. data/lib/middleman-core/core_extensions/collections.rb +82 -0
  43. data/lib/middleman-core/core_extensions/collections/lazy_root.rb +30 -0
  44. data/lib/middleman-core/core_extensions/collections/lazy_step.rb +48 -0
  45. data/lib/middleman-core/core_extensions/collections/pagination.rb +59 -0
  46. data/lib/middleman-core/core_extensions/collections/step_context.rb +26 -0
  47. data/lib/middleman-core/core_extensions/data.rb +1 -1
  48. data/lib/middleman-core/core_extensions/default_helpers.rb +1 -2
  49. data/lib/middleman-core/core_extensions/front_matter.rb +10 -3
  50. data/lib/middleman-core/core_extensions/i18n.rb +6 -7
  51. data/lib/middleman-core/core_extensions/show_exceptions.rb +1 -1
  52. data/lib/middleman-core/extension.rb +1 -1
  53. data/lib/middleman-core/extensions/automatic_image_sizes.rb +0 -2
  54. data/lib/middleman-core/file_renderer.rb +3 -1
  55. data/lib/middleman-core/load_paths.rb +2 -1
  56. data/lib/middleman-core/logger.rb +1 -1
  57. data/lib/middleman-core/meta_pages/sitemap_resource.rb +1 -1
  58. data/lib/middleman-core/preview_server.rb +1 -1
  59. data/lib/middleman-core/renderers/sass.rb +1 -1
  60. data/lib/middleman-core/renderers/slim.rb +2 -2
  61. data/lib/middleman-core/sitemap/extensions/on_disk.rb +1 -1
  62. data/lib/middleman-core/sitemap/extensions/proxies.rb +36 -50
  63. data/lib/middleman-core/sitemap/resource.rb +42 -3
  64. data/lib/middleman-core/sitemap/store.rb +5 -0
  65. data/lib/middleman-core/sources.rb +64 -24
  66. data/lib/middleman-core/sources/source_watcher.rb +47 -23
  67. data/lib/middleman-core/step_definitions/server_steps.rb +52 -21
  68. data/lib/middleman-core/template_context.rb +26 -5
  69. data/lib/middleman-core/template_renderer.rb +50 -33
  70. data/lib/middleman-core/util.rb +94 -1
  71. data/lib/middleman-core/util/hash_with_indifferent_access.rb +1 -1
  72. data/lib/middleman-core/version.rb +1 -1
  73. data/middleman-core.gemspec +2 -0
  74. metadata +90 -15
  75. data/features/more-instance_vars.feature +0 -18
  76. data/fixtures/more-instance-vars-app/source/_vartial.erb +0 -5
  77. data/fixtures/more-instance-vars-app/source/instance-var-set.html.erb +0 -2
  78. data/fixtures/more-instance-vars-app/source/layout.erb +0 -3
  79. data/fixtures/more-instance-vars-app/source/no-instance-var.html.erb +0 -1
@@ -87,7 +87,7 @@ module Middleman
87
87
  begin
88
88
  @engine.render
89
89
  rescue ::Sass::SyntaxError => e
90
- ::Sass::SyntaxError.exception_to_css(e, full_exception: true)
90
+ ::Sass::SyntaxError.exception_to_css(e)
91
91
  end
92
92
  end
93
93
 
@@ -24,7 +24,7 @@ module Middleman
24
24
  super
25
25
 
26
26
  # Setup Slim options to work with partials
27
- ::Slim::Engine.set_default_options(
27
+ ::Slim::Engine.set_options(
28
28
  buffer: '@_out_buf',
29
29
  use_html_safe: true,
30
30
  generator: ::Temple::Generators::RailsOutputBuffer,
@@ -39,7 +39,7 @@ module Middleman
39
39
 
40
40
  ::Slim::Embedded::SassEngine.disable_option_validator!
41
41
  %w(sass scss markdown).each do |engine|
42
- ::Slim::Embedded.default_options[engine.to_sym] = context_hack
42
+ ::Slim::Embedded.options[engine.to_sym] = context_hack
43
43
  end
44
44
  end
45
45
  end
@@ -24,7 +24,7 @@ module Middleman
24
24
 
25
25
  Contract None => Any
26
26
  def before_configuration
27
- app.files.changed(:source, &method(:update_files))
27
+ app.files.on_change(:source, &method(:update_files))
28
28
  end
29
29
 
30
30
  def ignored?(file)
@@ -1,4 +1,5 @@
1
1
  require 'middleman-core/sitemap/resource'
2
+ require 'middleman-core/core_extensions/collections/step_context'
2
3
 
3
4
  module Middleman
4
5
  module Sitemap
@@ -13,6 +14,13 @@ module Middleman
13
14
  @app.define_singleton_method(:proxy, &method(:create_proxy))
14
15
 
15
16
  @proxy_configs = Set.new
17
+ @post_config = false
18
+ end
19
+
20
+ def after_configuration
21
+ @post_config = true
22
+
23
+ ::Middleman::CoreExtensions::Collections::StepContext.add_to_context(:proxy, &method(:create_anonymous_proxy))
16
24
  end
17
25
 
18
26
  # Setup a proxy from a path to a target
@@ -27,70 +35,48 @@ module Middleman
27
35
  Contract String, String, Maybe[Hash] => Any
28
36
  def create_proxy(path, target, opts={})
29
37
  options = opts.dup
30
-
31
38
  @app.ignore(target) if options.delete(:ignore)
32
39
 
33
- metadata = {
34
- options: options,
35
- locals: options.delete(:locals) || {},
36
- page: options.delete(:data) || {}
37
- }
38
-
39
- @proxy_configs << ProxyConfiguration.new(path: path, target: target, metadata: metadata)
40
-
40
+ @proxy_configs << create_anonymous_proxy(path, target, options)
41
41
  @app.sitemap.rebuild_resource_list!(:added_proxy)
42
42
  end
43
43
 
44
+ # Setup a proxy from a path to a target
45
+ # @param [String] path The new, proxied path to create
46
+ # @param [String] target The existing path that should be proxied to. This must be a real resource, not another proxy.
47
+ # @option opts [Boolean] ignore Ignore the target from the sitemap (so only the new, proxy resource ends up in the output)
48
+ # @option opts [Symbol, Boolean, String] layout The layout name to use (e.g. `:article`) or `false` to disable layout.
49
+ # @option opts [Boolean] directory_indexes Whether or not the `:directory_indexes` extension applies to these paths.
50
+ # @option opts [Hash] locals Local variables for the template. These will be available when the template renders.
51
+ # @option opts [Hash] data Extra metadata to add to the page. This is the same as frontmatter, though frontmatter will take precedence over metadata defined here. Available via {Resource#data}.
52
+ # @return [void]
53
+ def create_anonymous_proxy(path, target, options={})
54
+ ProxyDescriptor.new(
55
+ ::Middleman::Util.normalize_path(path),
56
+ ::Middleman::Util.normalize_path(target),
57
+ options
58
+ )
59
+ end
60
+
44
61
  # Update the main sitemap resource list
45
62
  # @return Array<Middleman::Sitemap::Resource>
46
63
  Contract ResourceList => ResourceList
47
64
  def manipulate_resource_list(resources)
48
- resources + @proxy_configs.map do |config|
49
- p = ProxyResource.new(
50
- @app.sitemap,
51
- config.path,
52
- config.target
53
- )
54
-
55
- p.add_metadata(config.metadata)
56
- p
57
- end
65
+ resources + @proxy_configs.map { |c| c.to_resource(@app) }
58
66
  end
59
67
  end
60
68
 
61
- # Configuration for a proxy instance
62
- class ProxyConfiguration
63
- # The path that this proxy will appear at in the sitemap
64
- attr_reader :path
65
- def path=(p)
66
- @path = ::Middleman::Util.normalize_path(p)
67
- end
68
-
69
- # The existing sitemap path that this will proxy to
70
- attr_reader :target
71
- def target=(t)
72
- @target = ::Middleman::Util.normalize_path(t)
73
- end
74
-
75
- # Additional metadata like locals to apply to the proxy
76
- attr_accessor :metadata
77
-
78
- # Create a new proxy configuration from hash options
79
- def initialize(options={})
80
- options.each do |key, value|
81
- send "#{key}=", value
69
+ ProxyDescriptor = Struct.new(:path, :target, :metadata) do
70
+ def to_resource(app)
71
+ ProxyResource.new(app.sitemap, path, target).tap do |p|
72
+ md = metadata.dup
73
+ p.add_metadata(
74
+ locals: md.delete(:locals) || {},
75
+ page: md.delete(:data) || {},
76
+ options: md
77
+ )
82
78
  end
83
79
  end
84
-
85
- # Two configurations are equal if they reference the same path
86
- def eql?(other)
87
- other.path == path
88
- end
89
-
90
- # Two configurations are equal if they reference the same path
91
- def hash
92
- path.hash
93
- end
94
80
  end
95
81
  end
96
82
 
@@ -42,12 +42,22 @@ module Middleman
42
42
  # @param [Middleman::Sitemap::Store] store
43
43
  # @param [String] path
44
44
  # @param [String] source_file
45
- Contract IsA['Middleman::Sitemap::Store'], String, Maybe[IsA['Middleman::SourceFile']] => Any
45
+ Contract IsA['Middleman::Sitemap::Store'], String, Maybe[Or[IsA['Middleman::SourceFile'], String]] => Any
46
46
  def initialize(store, path, source_file=nil)
47
47
  @store = store
48
48
  @app = @store.app
49
49
  @path = path.gsub(' ', '%20') # handle spaces in filenames
50
- @source_file = source_file
50
+
51
+ if source_file && source_file.is_a?(String)
52
+ source_file = Pathname(source_file)
53
+ end
54
+
55
+ if source_file && source_file.is_a?(Pathname)
56
+ @source_file = ::Middleman::SourceFile.new(source_file.relative_path_from(@app.source_dir), source_file, @app.source_dir, Set.new([:source]))
57
+ else
58
+ @source_file = source_file
59
+ end
60
+
51
61
  @destination_path = @path
52
62
 
53
63
  # Options are generally rendering/sitemap options
@@ -165,7 +175,11 @@ module Middleman
165
175
  # Ignore based on the source path (without template extensions)
166
176
  return true if @app.sitemap.ignored?(path)
167
177
  # This allows files to be ignored by their source file name (with template extensions)
168
- !self.is_a?(ProxyResource) && @app.sitemap.ignored?(source_file[:relative_path].to_s)
178
+ if !self.is_a?(ProxyResource) && source_file && @app.sitemap.ignored?(source_file[:relative_path].to_s)
179
+ true
180
+ else
181
+ false
182
+ end
169
183
  end
170
184
 
171
185
  # The preferred MIME content type for this resource based on extension or metadata
@@ -174,6 +188,31 @@ module Middleman
174
188
  def content_type
175
189
  options[:content_type] || ::Rack::Mime.mime_type(ext, nil)
176
190
  end
191
+
192
+ def to_s
193
+ "#<Middleman::Sitemap::Resource path=#{@path}>"
194
+ end
195
+ alias_method :inspect, :to_s # Ruby 2.0 calls inspect for NoMethodError instead of to_s
196
+ end
197
+
198
+ class StringResource < Resource
199
+ def initialize(store, path, contents=nil, &block)
200
+ @request_path = path
201
+ @contents = block_given? ? block : contents
202
+ super(store, path)
203
+ end
204
+
205
+ def template?
206
+ true
207
+ end
208
+
209
+ def render(*)
210
+ @contents.respond_to?(:call) ? @contents.call : @contents
211
+ end
212
+
213
+ def binary?
214
+ false
215
+ end
177
216
  end
178
217
  end
179
218
  end
@@ -49,11 +49,15 @@ module Middleman
49
49
  # @return [Middleman::Application]
50
50
  attr_reader :app
51
51
 
52
+ attr_reader :update_count
53
+
52
54
  # Initialize with parent app
53
55
  # @param [Middleman::Application] app
54
56
  def initialize(app)
55
57
  @app = app
56
58
  @resources = []
59
+ @update_count = 0
60
+
57
61
  # TODO: Should this be a set or hash?
58
62
  @resource_list_manipulators = []
59
63
  @needs_sitemap_rebuild = true
@@ -187,6 +191,7 @@ module Middleman
187
191
  end
188
192
 
189
193
  invalidate_resources_not_ignored_cache!
194
+ @update_count += 1
190
195
  end
191
196
  end
192
197
 
@@ -1,8 +1,9 @@
1
1
  require 'middleman-core/contracts'
2
+ require 'backports/2.0.0/enumerable/lazy'
2
3
 
3
4
  module Middleman
4
5
  # The standard "record" that contains information about a file on disk.
5
- SourceFile = Struct.new :relative_path, :full_path, :directory, :type
6
+ SourceFile = Struct.new :relative_path, :full_path, :directory, :types
6
7
 
7
8
  # Sources handle multiple on-disk collections of files which make up
8
9
  # a Middleman project. They are separated by `type` which can then be
@@ -18,7 +19,7 @@ module Middleman
18
19
  attr_reader :app
19
20
 
20
21
  # Duck-typed definition of a valid source watcher
21
- HANDLER = RespondTo[:changed]
22
+ HANDLER = RespondTo[:on_change]
22
23
 
23
24
  # Config
24
25
  Contract None => Hash
@@ -82,8 +83,8 @@ module Middleman
82
83
  Contract SourceFile => Bool
83
84
  def globally_ignored?(file)
84
85
  @ignores.values.any? do |descriptor|
85
- ((descriptor[:type] == :all) || (file[:type] == descriptor[:type])) &&
86
- matches?(descriptor[:validator], file)
86
+ ((descriptor[:type] == :all) || file[:types].include?(descriptor[:type])) &&
87
+ matches?(descriptor[:validator], file)
87
88
  end
88
89
  end
89
90
 
@@ -113,7 +114,7 @@ module Middleman
113
114
  [priority, n]
114
115
  end.reverse.freeze
115
116
 
116
- handler.changed(&method(:did_change))
117
+ handler.on_change(&method(:did_change))
117
118
 
118
119
  if @running
119
120
  handler.poll_once!
@@ -133,7 +134,7 @@ module Middleman
133
134
  #
134
135
  # @param [SourceWatcher] watcher The watcher to remove.
135
136
  # @return [void]
136
- Contract RespondTo[:changed] => Any
137
+ Contract RespondTo[:on_change] => Any
137
138
  def unwatch(watcher)
138
139
  @watchers.delete(watcher)
139
140
 
@@ -156,7 +157,7 @@ module Middleman
156
157
  # @return [Array<Middleman::SourceFile>]
157
158
  Contract None => ArrayOf[SourceFile]
158
159
  def files
159
- watchers.map(&:files).flatten.uniq { |f| f[:relative_path] }
160
+ watchers.flat_map(&:files).uniq { |f| f[:relative_path] }
160
161
  end
161
162
 
162
163
  # Find a file given a type and path.
@@ -168,10 +169,11 @@ module Middleman
168
169
  Contract Symbol, String, Maybe[Bool] => Maybe[SourceFile]
169
170
  def find(type, path, glob=false)
170
171
  watchers
171
- .select { |d| d.type == type }
172
- .map { |d| d.find(path, glob) }
173
- .compact
174
- .first
172
+ .lazy
173
+ .select { |d| d.type == type }
174
+ .map { |d| d.find(path, glob) }
175
+ .reject(&:nil?)
176
+ .first
175
177
  end
176
178
 
177
179
  # Check if a file for a given type exists.
@@ -182,8 +184,9 @@ module Middleman
182
184
  Contract Symbol, String => Bool
183
185
  def exists?(type, path)
184
186
  watchers
185
- .select { |d| d.type == type }
186
- .any? { |d| d.exists?(path) }
187
+ .lazy
188
+ .select { |d| d.type == type }
189
+ .any? { |d| d.exists?(path) }
187
190
  end
188
191
 
189
192
  # Check if a file for a given type exists.
@@ -191,11 +194,11 @@ module Middleman
191
194
  # @param [Symbol] type The file "type".
192
195
  # @param [String] path The file path relative to it's source root.
193
196
  # @return [Boolean]
194
- Contract Symbol, String => Maybe[HANDLER]
195
- def watcher_for_path(type, path)
197
+ Contract SetOf[Symbol], String => Maybe[HANDLER]
198
+ def watcher_for_path(types, path)
196
199
  watchers
197
- .select { |d| d.type == type }
198
- .find { |d| d.exists?(path) }
200
+ .select { |d| types.include?(d.type) }
201
+ .find { |d| d.exists?(path) }
199
202
  end
200
203
 
201
204
  # Manually poll all watchers for new content.
@@ -230,16 +233,53 @@ module Middleman
230
233
  # A callback requires a type and the proc to execute.
231
234
  CallbackDescriptor = Struct.new :type, :proc
232
235
 
233
- # Add callback to be run on file change
236
+ # Add callback to be run on file change or deletion
234
237
  #
235
- # @param [nil,Regexp] matcher A Regexp to match the change path against
238
+ # @param [Symbol] type The change type.
236
239
  # @return [Set<CallbackDescriptor>]
237
240
  Contract Symbol, Proc => ArrayOf[CallbackDescriptor]
238
- def changed(type, &block)
241
+ def on_change(type, &block)
239
242
  @on_change_callbacks << CallbackDescriptor.new(type, block)
240
243
  @on_change_callbacks
241
244
  end
242
245
 
246
+ # Backwards compatible change handler.
247
+ #
248
+ # @param [nil,Regexp] matcher A Regexp to match the change path against
249
+ # Contract Maybe[Regexp] => Any
250
+ def changed(matcher=nil, &block)
251
+ on_change :source do |updated, _removed|
252
+ updated.select { |f|
253
+ matcher.nil? ? true : matches?(matcher, f)
254
+ }.each do |f|
255
+ block.call(f[:relative_path])
256
+ end
257
+ end
258
+ end
259
+
260
+ # Backwards compatible delete handler.
261
+ #
262
+ # @param [nil,Regexp] matcher A Regexp to match the change path against
263
+ # Contract Maybe[Regexp] => Any
264
+ def deleted(matcher=nil, &block)
265
+ on_change :source do |_updated, removed|
266
+ removed.select { |f|
267
+ matcher.nil? ? true : matches?(matcher, f)
268
+ }.each do |f|
269
+ block.call(f[:relative_path])
270
+ end
271
+ end
272
+ end
273
+
274
+ # Backwards compatible ignored check.
275
+ #
276
+ # @param [Pathname,String] path The path to check.
277
+ Contract Or[Pathname, String] => Bool
278
+ def ignored?(path)
279
+ descriptor = find(:source, path)
280
+ !descriptor || globally_ignored?(descriptor)
281
+ end
282
+
243
283
  protected
244
284
 
245
285
  # Whether a validator matches a file.
@@ -272,11 +312,11 @@ module Middleman
272
312
  Contract ArrayOf[SourceFile], ArrayOf[SourceFile], HANDLER => Any
273
313
  def did_change(updated_files, removed_files, watcher)
274
314
  valid_updated = updated_files.select do |file|
275
- watcher_for_path(file[:type], file[:relative_path].to_s) == watcher
315
+ watcher_for_path(file[:types], file[:relative_path].to_s) == watcher
276
316
  end
277
317
 
278
318
  valid_removed = removed_files.select do |file|
279
- watcher_for_path(file[:type], file[:relative_path].to_s).nil?
319
+ watcher_for_path(file[:types], file[:relative_path].to_s).nil?
280
320
  end
281
321
 
282
322
  return if valid_updated.empty? && valid_removed.empty?
@@ -296,8 +336,8 @@ module Middleman
296
336
  if callback[:type] == :all
297
337
  callback[:proc].call(updated_files, removed_files)
298
338
  else
299
- valid_updated = updated_files.select { |f| callback[:type] == f[:type] }
300
- valid_removed = removed_files.select { |f| callback[:type] == f[:type] }
339
+ valid_updated = updated_files.select { |f| f[:types].include?(callback[:type]) }
340
+ valid_removed = removed_files.select { |f| f[:types].include?(callback[:type]) }
301
341
 
302
342
  callback[:proc].call(valid_updated, valid_removed)
303
343
  end
@@ -1,7 +1,7 @@
1
1
  # Watcher Library
2
2
  require 'listen'
3
-
4
3
  require 'middleman-core/contracts'
4
+ require 'backports/2.0.0/enumerable/lazy'
5
5
 
6
6
  module Middleman
7
7
  # The default source watcher implementation. Watches a directory on disk
@@ -43,6 +43,7 @@ module Middleman
43
43
  @directory = Pathname(directory)
44
44
 
45
45
  @files = {}
46
+ @extensionless_files = {}
46
47
 
47
48
  @validator = options.fetch(:validator, proc { true })
48
49
  @ignored = options.fetch(:ignored, proc { false })
@@ -103,10 +104,8 @@ module Middleman
103
104
  return nil if p.absolute? && !p.to_s.start_with?(@directory.to_s)
104
105
 
105
106
  p = @directory + p if p.relative?
106
-
107
107
  if glob
108
- found = @files.find { |_, v| v[:relative_path].fnmatch(path) }
109
- found ? found.last : nil
108
+ @extensionless_files[p]
110
109
  else
111
110
  @files[p]
112
111
  end
@@ -173,7 +172,7 @@ module Middleman
173
172
  # @param [Proc] matcher A Regexp to match the change path against
174
173
  # @return [Set<Proc>]
175
174
  Contract Proc => SetOf[Proc]
176
- def changed(&block)
175
+ def on_change(&block)
177
176
  @on_change_callbacks << block
178
177
  @on_change_callbacks
179
178
  end
@@ -211,25 +210,48 @@ module Middleman
211
210
  Contract ArrayOf[Pathname], ArrayOf[Pathname] => Any
212
211
  def update(updated_paths, removed_paths)
213
212
  valid_updates = updated_paths
214
- .map(&method(:path_to_source_file))
215
- .select(&method(:valid?))
216
-
217
- valid_updates.each do |f|
218
- @files[f[:full_path]] = f
219
- logger.debug "== Change (#{f[:type]}): #{f[:relative_path]}"
220
- end
213
+ .lazy
214
+ .map(&method(:path_to_source_file))
215
+ .select(&method(:valid?))
216
+ .to_a
217
+ .each do |f|
218
+ add_file_to_cache(f)
219
+ logger.debug "== Change (#{f[:types].inspect}): #{f[:relative_path]}"
220
+ end
221
221
 
222
222
  valid_removes = removed_paths
223
- .select(&@files.method(:key?))
224
- .map(&@files.method(:[]))
225
- .select(&method(:valid?))
223
+ .lazy
224
+ .select(&@files.method(:key?))
225
+ .map(&@files.method(:[]))
226
+ .select(&method(:valid?))
227
+ .to_a
228
+ .each do |f|
229
+ remove_file_from_cache(f)
230
+ logger.debug "== Deletion (#{f[:types].inspect}): #{f[:relative_path]}"
231
+ end
232
+
233
+ run_callbacks(
234
+ @on_change_callbacks,
235
+ valid_updates,
236
+ valid_removes
237
+ ) unless valid_updates.empty? && valid_removes.empty?
238
+ end
226
239
 
227
- valid_removes.each do |f|
228
- @files.delete(f[:full_path])
229
- logger.debug "== Deletion (#{f[:type]}): #{f[:relative_path]}"
230
- end
240
+ def add_file_to_cache(f)
241
+ @files[f[:full_path]] = f
242
+ @extensionless_files[strip_extensions(f[:full_path])] = f
243
+ end
231
244
 
232
- run_callbacks(@on_change_callbacks, valid_updates, valid_removes) unless valid_updates.empty? && valid_removes.empty?
245
+ def remove_file_from_cache(f)
246
+ @files.delete(f[:full_path])
247
+ @extensionless_files.delete(strip_extensions(f[:full_path]))
248
+ end
249
+
250
+ def strip_extensions(p)
251
+ while ::Tilt[p.to_s] || p.extname === '.html'
252
+ p = p.sub_ext('')
253
+ end
254
+ Pathname(p.to_s + '.*')
233
255
  end
234
256
 
235
257
  # Check if this watcher should care about a file.
@@ -239,8 +261,8 @@ module Middleman
239
261
  Contract IsA['Middleman::SourceFile'] => Bool
240
262
  def valid?(file)
241
263
  @validator.call(file) &&
242
- !globally_ignored?(file) &&
243
- !@ignored.call(file)
264
+ !globally_ignored?(file) &&
265
+ !@ignored.call(file)
244
266
  end
245
267
 
246
268
  # Convert a path to a file resprentation.
@@ -249,8 +271,10 @@ module Middleman
249
271
  # @return [Middleman::SourceFile]
250
272
  Contract Pathname => IsA['Middleman::SourceFile']
251
273
  def path_to_source_file(path)
274
+ types = Set.new([@type])
275
+
252
276
  ::Middleman::SourceFile.new(
253
- path.relative_path_from(@directory), path, @directory, @type)
277
+ path.relative_path_from(@directory), path, @directory, types)
254
278
  end
255
279
 
256
280
  # Notify callbacks for a file given an array of callbacks