middleman-core 4.0.0.beta.2 → 4.0.0.rc.1

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 (76) hide show
  1. checksums.yaml +4 -4
  2. data/features/builder.feature +9 -1
  3. data/features/collections.feature +43 -2
  4. data/features/data.feature +7 -0
  5. data/features/extension_hooks.feature +13 -0
  6. data/features/front-matter-neighbor.feature +0 -11
  7. data/features/front-matter.feature +0 -22
  8. data/features/markdown_kramdown_in_slim.feature +42 -0
  9. data/fixtures/extension-hooks-app/config.rb +39 -0
  10. data/fixtures/extension-hooks-app/source/index.html.erb +9 -0
  11. data/fixtures/frontmatter-app/source/front-matter-line-2.html.erb +4 -1
  12. data/fixtures/frontmatter-neighbor-app/config.rb +3 -3
  13. data/fixtures/frontmatter-settings-neighbor-app/config.rb +4 -4
  14. data/fixtures/markdown-in-slim-app/config.rb +0 -0
  15. data/fixtures/markdown-in-slim-app/source/images/blank.gif +0 -0
  16. data/fixtures/markdown-in-slim-app/source/link_target.html.markdown +4 -0
  17. data/fixtures/more-traversal-app/source/layout.erb +1 -1
  18. data/fixtures/nested-data-app/data/examples/withcontent.yaml +11 -0
  19. data/fixtures/nested-data-app/source/extracontent.html.haml.erb +4 -0
  20. data/fixtures/traversal-app/source/layout.erb +1 -1
  21. data/lib/middleman-core/application.rb +7 -3
  22. data/lib/middleman-core/builder.rb +2 -1
  23. data/lib/middleman-core/configuration.rb +0 -1
  24. data/lib/middleman-core/contracts.rb +0 -10
  25. data/lib/middleman-core/core_extensions/collections.rb +30 -6
  26. data/lib/middleman-core/core_extensions/data.rb +4 -4
  27. data/lib/middleman-core/core_extensions/file_watcher.rb +1 -1
  28. data/lib/middleman-core/core_extensions/front_matter.rb +10 -104
  29. data/lib/middleman-core/core_extensions/show_exceptions.rb +1 -1
  30. data/lib/middleman-core/extension_manager.rb +2 -1
  31. data/lib/middleman-core/extensions/asset_hash.rb +2 -1
  32. data/lib/middleman-core/extensions/asset_host.rb +1 -1
  33. data/lib/middleman-core/extensions/external_pipeline.rb +4 -1
  34. data/lib/middleman-core/extensions/minify_css.rb +1 -1
  35. data/lib/middleman-core/extensions/minify_javascript.rb +1 -1
  36. data/lib/middleman-core/extensions/relative_assets.rb +1 -1
  37. data/lib/middleman-core/extensions.rb +1 -1
  38. data/lib/middleman-core/file_renderer.rb +6 -4
  39. data/lib/middleman-core/meta_pages/sitemap_resource.rb +1 -1
  40. data/lib/middleman-core/meta_pages/templates/config.html.erb +14 -14
  41. data/lib/middleman-core/meta_pages.rb +6 -1
  42. data/lib/middleman-core/preview_server.rb +21 -2
  43. data/lib/middleman-core/rack.rb +5 -3
  44. data/lib/middleman-core/renderers/haml.rb +22 -6
  45. data/lib/middleman-core/renderers/kramdown.rb +10 -3
  46. data/lib/middleman-core/renderers/liquid.rb +22 -3
  47. data/lib/middleman-core/renderers/redcarpet.rb +7 -5
  48. data/lib/middleman-core/renderers/sass.rb +1 -5
  49. data/lib/middleman-core/renderers/slim.rb +16 -12
  50. data/lib/middleman-core/sitemap/extensions/ignores.rb +2 -0
  51. data/lib/middleman-core/sitemap/extensions/on_disk.rb +2 -0
  52. data/lib/middleman-core/sitemap/extensions/proxies.rb +18 -3
  53. data/lib/middleman-core/sitemap/extensions/redirects.rb +2 -0
  54. data/lib/middleman-core/sitemap/extensions/request_endpoints.rb +2 -0
  55. data/lib/middleman-core/sitemap/resource.rb +24 -17
  56. data/lib/middleman-core/sitemap/store.rb +1 -0
  57. data/lib/middleman-core/sources/source_watcher.rb +26 -1
  58. data/lib/middleman-core/step_definitions/builder_steps.rb +8 -8
  59. data/lib/middleman-core/step_definitions/middleman_steps.rb +8 -2
  60. data/lib/middleman-core/step_definitions/server_steps.rb +17 -18
  61. data/lib/middleman-core/step_definitions.rb +0 -1
  62. data/lib/middleman-core/template_context.rb +2 -2
  63. data/lib/middleman-core/util/data.rb +153 -0
  64. data/lib/middleman-core/util.rb +14 -45
  65. data/lib/middleman-core/version.rb +1 -1
  66. data/middleman-core.gemspec +3 -2
  67. data/spec/middleman-core/util_spec.rb +3 -23
  68. metadata +37 -26
  69. data/fixtures/frontmatter-app/source/json-front-matter-2.php.erb +0 -7
  70. data/fixtures/frontmatter-app/source/json-front-matter-encoding.html.erb +0 -7
  71. data/fixtures/frontmatter-app/source/json-front-matter-line-2.html.erb +0 -7
  72. data/fixtures/frontmatter-app/source/json-front-matter.html.erb +0 -6
  73. data/fixtures/frontmatter-neighbor-app/source/json-front-matter-2.php.erb +0 -2
  74. data/fixtures/frontmatter-neighbor-app/source/json-front-matter-2.php.erb.frontmatter +0 -4
  75. data/fixtures/frontmatter-neighbor-app/source/json-front-matter.html.erb +0 -1
  76. data/fixtures/frontmatter-neighbor-app/source/json-front-matter.html.erb.frontmatter +0 -4
@@ -7,6 +7,8 @@ module Middleman
7
7
  # Manages the list of proxy configurations and manipulates the sitemap
8
8
  # to include new resources based on those configurations
9
9
  class Proxies < Extension
10
+ self.resource_list_manipulator_priority = 0
11
+
10
12
  # Expose `create_proxy` as `app.proxy`
11
13
  expose_to_application proxy: :create_proxy
12
14
 
@@ -83,11 +85,20 @@ module Middleman
83
85
  end
84
86
  end
85
87
 
88
+ class Resource
89
+ def proxy_to(path)
90
+ throw "Resource#proxy_to has been removed. Use ProxyResource class instead."
91
+ end
92
+ end
93
+
86
94
  class ProxyResource < ::Middleman::Sitemap::Resource
95
+ Contract String
96
+ attr_reader :target
97
+
87
98
  # Initialize resource with parent store and URL
88
99
  # @param [Middleman::Sitemap::Store] store
89
100
  # @param [String] path
90
- # @param [String] source_file
101
+ # @param [String] target
91
102
  def initialize(store, path, target)
92
103
  super(store, path)
93
104
 
@@ -115,8 +126,12 @@ module Middleman
115
126
  end
116
127
 
117
128
  Contract IsA['Middleman::SourceFile']
118
- def source_file
119
- target_resource.source_file
129
+ def file_descriptor
130
+ target_resource.file_descriptor
131
+ end
132
+
133
+ def metadata
134
+ target_resource.metadata.deep_merge super
120
135
  end
121
136
 
122
137
  Contract Maybe[String]
@@ -7,6 +7,8 @@ module Middleman
7
7
  # Manages the list of proxy configurations and manipulates the sitemap
8
8
  # to include new resources based on those configurations
9
9
  class Redirects < Extension
10
+ self.resource_list_manipulator_priority = 0
11
+
10
12
  # Expose `create_redirect` to config as `redirect`
11
13
  expose_to_config redirect: :create_redirect
12
14
 
@@ -4,6 +4,8 @@ module Middleman
4
4
  module Sitemap
5
5
  module Extensions
6
6
  class RequestEndpoints < Extension
7
+ self.resource_list_manipulator_priority = 0
8
+
7
9
  # Expose `create_endpoint` to config as `endpoint`
8
10
  expose_to_config endpoint: :create_endpoint
9
11
 
@@ -24,7 +24,7 @@ module Middleman
24
24
  # The on-disk source file for this resource, if there is one
25
25
  # @return [String]
26
26
  Contract Maybe[IsA['Middleman::SourceFile']]
27
- attr_reader :source_file
27
+ attr_reader :file_descriptor
28
28
 
29
29
  # The path to use when requesting this resource. Normally it's
30
30
  # the same as {#destination_path} but it can be overridden in subclasses.
@@ -41,21 +41,21 @@ module Middleman
41
41
  # Initialize resource with parent store and URL
42
42
  # @param [Middleman::Sitemap::Store] store
43
43
  # @param [String] path
44
- # @param [String] source_file
44
+ # @param [String] source
45
45
  Contract IsA['Middleman::Sitemap::Store'], String, Maybe[Or[IsA['Middleman::SourceFile'], String]] => Any
46
- def initialize(store, path, source_file=nil)
46
+ def initialize(store, path, source=nil)
47
47
  @store = store
48
48
  @app = @store.app
49
49
  @path = path
50
50
 
51
- if source_file && source_file.is_a?(String)
52
- source_file = Pathname(source_file)
51
+ if source && source.is_a?(String)
52
+ source = Pathname(source)
53
53
  end
54
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]))
55
+ if source && source.is_a?(Pathname)
56
+ @file_descriptor = ::Middleman::SourceFile.new(source.relative_path_from(@app.source_dir), source, @app.source_dir, Set.new([:source]))
57
57
  else
58
- @source_file = source_file
58
+ @file_descriptor = source
59
59
  end
60
60
 
61
61
  @destination_path = @path
@@ -71,8 +71,15 @@ module Middleman
71
71
  # @return [Boolean]
72
72
  Contract Bool
73
73
  def template?
74
- return false if source_file.nil?
75
- !::Tilt[source_file[:full_path].to_s].nil?
74
+ return false if file_descriptor.nil?
75
+ !::Tilt[file_descriptor[:full_path].to_s].nil?
76
+ end
77
+
78
+ # Backwards compatible method for turning descriptor into a string.
79
+ # @return [String]
80
+ Contract String
81
+ def source_file
82
+ file_descriptor && file_descriptor[:full_path].to_s
76
83
  end
77
84
 
78
85
  # Merge in new metadata specific to this resource.
@@ -87,8 +94,8 @@ module Middleman
87
94
  end
88
95
 
89
96
  # Data about this resource, populated from frontmatter or extensions.
90
- # @return [IndifferentHash]
91
- Contract IsA['Middleman::Util::IndifferentHash']
97
+ # @return [Hash]
98
+ Contract RespondTo[:indifferent_access?]
92
99
  def data
93
100
  ::Middleman::Util.recursively_enhance(metadata[:page])
94
101
  end
@@ -119,9 +126,9 @@ module Middleman
119
126
  # @return [String]
120
127
  Contract Hash, Hash => String
121
128
  def render(opts={}, locs={})
122
- return ::Middleman::FileRenderer.new(@app, source_file[:full_path].to_s).template_data_for_file unless template?
129
+ return ::Middleman::FileRenderer.new(@app, file_descriptor[:full_path].to_s).template_data_for_file unless template?
123
130
 
124
- ::Middleman::Util.instrument 'render.resource', path: source_file[:full_path].to_s, destination_path: destination_path do
131
+ ::Middleman::Util.instrument 'render.resource', path: file_descriptor[:full_path].to_s, destination_path: destination_path do
125
132
  md = metadata
126
133
  opts = md[:options].deep_merge(opts)
127
134
  locs = md[:locals].deep_merge(locs)
@@ -132,7 +139,7 @@ module Middleman
132
139
  opts[:layout] = false if %w(.js .json .css .txt).include?(ext)
133
140
  end
134
141
 
135
- renderer = ::Middleman::TemplateRenderer.new(@app, source_file[:full_path].to_s)
142
+ renderer = ::Middleman::TemplateRenderer.new(@app, file_descriptor[:full_path].to_s)
136
143
  renderer.render(locs, opts)
137
144
  end
138
145
  end
@@ -155,7 +162,7 @@ module Middleman
155
162
  # @return [Boolean]
156
163
  Contract Bool
157
164
  def binary?
158
- !source_file.nil? && ::Middleman::Util.binary?(source_file[:full_path].to_s)
165
+ !file_descriptor.nil? && ::Middleman::Util.binary?(file_descriptor[:full_path].to_s)
159
166
  end
160
167
 
161
168
  # Ignore a resource directly, without going through the whole
@@ -174,7 +181,7 @@ module Middleman
174
181
  # Ignore based on the source path (without template extensions)
175
182
  return true if @app.sitemap.ignored?(path)
176
183
  # This allows files to be ignored by their source file name (with template extensions)
177
- if !self.is_a?(ProxyResource) && source_file && @app.sitemap.ignored?(source_file[:relative_path].to_s)
184
+ if !self.is_a?(ProxyResource) && file_descriptor && @app.sitemap.ignored?(file_descriptor[:relative_path].to_s)
178
185
  true
179
186
  else
180
187
  false
@@ -188,6 +188,7 @@ module Middleman
188
188
  @app.logger.debug '== Rebuilding resource list'
189
189
 
190
190
  @resources = @resource_list_manipulators.reduce([]) do |result, m|
191
+ @app.logger.debug "== Running manipulator: #{m[:name]}"
191
192
  newres = m[:manipulator].send(m[:custom_name] || :manipulate_resource_list, result)
192
193
 
193
194
  # Reset lookup cache
@@ -4,6 +4,26 @@ require 'middleman-core/contracts'
4
4
  require 'middleman-core/contracts'
5
5
  require 'backports/2.0.0/enumerable/lazy'
6
6
 
7
+ # Monkey patch Listen silencer so `only` works on directories too
8
+ module Listen
9
+ class Silencer
10
+ # TODO: switch type and path places - and verify
11
+ def silenced?(relative_path, type)
12
+ path = relative_path.to_s
13
+
14
+ # if only_patterns && type == :file
15
+ # return true unless only_patterns.any? { |pattern| path =~ pattern }
16
+ # end
17
+
18
+ if only_patterns
19
+ return !only_patterns.any? { |pattern| path =~ pattern }
20
+ end
21
+
22
+ ignore_patterns.any? { |pattern| path =~ pattern }
23
+ end
24
+ end
25
+ end
26
+
7
27
  module Middleman
8
28
  # The default source watcher implementation. Watches a directory on disk
9
29
  # and responds to events on changes.
@@ -29,6 +49,9 @@ module Middleman
29
49
  Contract Hash
30
50
  attr_reader :options
31
51
 
52
+ # Reference to lower level listener
53
+ attr_reader :listener
54
+
32
55
  # Construct a new SourceWatcher
33
56
  #
34
57
  # @param [Middleman::Sources] parent The parent collection.
@@ -138,9 +161,11 @@ module Middleman
138
161
  config[:latency] = @latency if @latency
139
162
 
140
163
  @listener = ::Listen.to(@directory.to_s, config, &method(:on_listener_change))
141
- @listener.start
142
164
 
165
+ @listener.ignore(/^\.sass-cache/)
143
166
  @listener.only(@only) unless @only.empty?
167
+
168
+ @listener.start
144
169
  end
145
170
 
146
171
  # Stop the listener.
@@ -6,8 +6,8 @@ end
6
6
 
7
7
  Given /^app "([^\"]*)" is using config "([^\"]*)"$/ do |path, config_name|
8
8
  target = File.join(PROJECT_ROOT_PATH, 'fixtures', path)
9
- config_path = File.join(current_dir, "config-#{config_name}.rb")
10
- config_dest = File.join(current_dir, 'config.rb')
9
+ config_path = File.join(expand_path("."), "config-#{config_name}.rb")
10
+ config_dest = File.join(expand_path("."), 'config.rb')
11
11
  FileUtils.cp(config_path, config_dest)
12
12
  end
13
13
 
@@ -22,12 +22,12 @@ Given /^a fixture app "([^\"]*)"$/ do |path|
22
22
 
23
23
  # This step can be reentered from several places but we don't want
24
24
  # to keep re-copying and re-cd-ing into ever-deeper directories
25
- next if File.basename(current_dir) == path
25
+ next if File.basename(expand_path(".")) == path
26
26
 
27
27
  step %Q{a directory named "#{path}"}
28
28
 
29
29
  target_path = File.join(PROJECT_ROOT_PATH, 'fixtures', path)
30
- FileUtils.cp_r(target_path, current_dir)
30
+ FileUtils.cp_r(target_path, expand_path("."))
31
31
 
32
32
  step %Q{I cd to "#{path}"}
33
33
  end
@@ -58,20 +58,20 @@ Given /^a successfully built app at "([^\"]*)" with flags "([^\"]*)"$/ do |path,
58
58
  end
59
59
 
60
60
  Given /^a modification time for a file named "([^\"]*)"$/ do |file|
61
- target = File.join(current_dir, file)
61
+ target = File.join(expand_path("."), file)
62
62
  @modification_times[target] = File.mtime(target)
63
63
  end
64
64
 
65
65
  Then /^the file "([^\"]*)" should not have been updated$/ do |file|
66
- target = File.join(current_dir, file)
66
+ target = File.join(expand_path("."), file)
67
67
  expect(File.mtime(target)).to eq(@modification_times[target])
68
68
  end
69
69
 
70
70
  # Provide this Aruba overload in case we're matching something with quotes in it
71
71
  Then /^the file "([^"]*)" should contain '([^']*)'$/ do |file, partial_content|
72
- check_file_content(file, Regexp.new(Regexp.escape(partial_content)), true)
72
+ expect(file).to have_file_content(Regexp.new(Regexp.escape(partial_content)), true)
73
73
  end
74
74
 
75
75
  And /the file "(.*)" should be gzipped/ do |file|
76
- expect(File.binread(File.join(current_dir, file), 2)).to eq(['1F8B'].pack('H*'))
76
+ expect(File.binread(File.join(expand_path("."), file), 2)).to eq(['1F8B'].pack('H*'))
77
77
  end
@@ -1,9 +1,15 @@
1
1
  Then /^the file "([^\"]*)" has the contents$/ do |path, contents|
2
2
  write_file(path, contents)
3
- @server_inst.files.find_new_files!
3
+
4
+ cd(".") do
5
+ @server_inst.files.find_new_files!
6
+ end
4
7
  end
5
8
 
6
9
  Then /^the file "([^\"]*)" is removed$/ do |path|
7
10
  step %Q{I remove the file "#{path}"}
8
- @server_inst.files.find_new_files!
11
+
12
+ cd(".") do
13
+ @server_inst.files.find_new_files!
14
+ end
9
15
  end
@@ -31,8 +31,7 @@ Given /^"([^\"]*)" is set to "([^\"]*)"$/ do |variable, value|
31
31
  end
32
32
 
33
33
  Given /^the Server is running$/ do
34
- root_dir = File.expand_path(current_dir)
35
-
34
+ root_dir = File.expand_path(expand_path("."))
36
35
 
37
36
  if File.exists?(File.join(root_dir, 'source'))
38
37
  ENV['MM_SOURCE'] = 'source'
@@ -44,7 +43,7 @@ Given /^the Server is running$/ do
44
43
 
45
44
  initialize_commands = @initialize_commands || []
46
45
 
47
- in_current_dir do
46
+ cd(".") do
48
47
  @server_inst = ::Middleman::Application.new do
49
48
  config[:watcher_disable] = true
50
49
  config[:show_exceptions] = false
@@ -53,10 +52,10 @@ Given /^the Server is running$/ do
53
52
  instance_exec(&p)
54
53
  end
55
54
  end
56
- end
57
55
 
58
- rack = ::Middleman::Rack.new(@server_inst)
59
- @browser = ::Rack::MockRequest.new(rack.to_app)
56
+ rack = ::Middleman::Rack.new(@server_inst)
57
+ @browser = ::Rack::MockRequest.new(rack.to_app)
58
+ end
60
59
  end
61
60
 
62
61
  Given /^the Server is running at "([^\"]*)"$/ do |app_path|
@@ -69,13 +68,13 @@ Given /^a template named "([^\"]*)" with:$/ do |name, string|
69
68
  end
70
69
 
71
70
  When /^I go to "([^\"]*)"$/ do |url|
72
- in_current_dir do
71
+ cd(".") do
73
72
  @last_response = @browser.get(URI.encode(url))
74
73
  end
75
74
  end
76
75
 
77
76
  Then /^going to "([^\"]*)" should not raise an exception$/ do |url|
78
- in_current_dir do
77
+ cd(".") do
79
78
  last_response = nil
80
79
  expect {
81
80
  last_response = @browser.get(URI.encode(url))
@@ -85,61 +84,61 @@ Then /^going to "([^\"]*)" should not raise an exception$/ do |url|
85
84
  end
86
85
 
87
86
  Then /^the content type should be "([^\"]*)"$/ do |expected|
88
- in_current_dir do
87
+ cd(".") do
89
88
  expect(@last_response.content_type).to start_with(expected)
90
89
  end
91
90
  end
92
91
 
93
92
  Then /^I should see "([^\"]*)"$/ do |expected|
94
- in_current_dir do
93
+ cd(".") do
95
94
  expect(@last_response.body).to include(expected)
96
95
  end
97
96
  end
98
97
 
99
98
  Then /^I should see '([^\']*)'$/ do |expected|
100
- in_current_dir do
99
+ cd(".") do
101
100
  expect(@last_response.body).to include(expected)
102
101
  end
103
102
  end
104
103
 
105
104
  Then /^I should see:$/ do |expected|
106
- in_current_dir do
105
+ cd(".") do
107
106
  expect(@last_response.body).to include(expected)
108
107
  end
109
108
  end
110
109
 
111
110
  Then /^I should not see "([^\"]*)"$/ do |expected|
112
- in_current_dir do
111
+ cd(".") do
113
112
  expect(@last_response.body).to_not include(expected)
114
113
  end
115
114
  end
116
115
 
117
116
  Then /^I should see content matching %r{(.*)}$/ do |expected|
118
- in_current_dir do
117
+ cd(".") do
119
118
  expect(@last_response.body).to match(expected)
120
119
  end
121
120
  end
122
121
 
123
122
  Then /^I should not see content matching %r{(.*)}$/ do |expected|
124
- in_current_dir do
123
+ cd(".") do
125
124
  expect(@last_response.body).to_not match(expected)
126
125
  end
127
126
  end
128
127
 
129
128
  Then /^I should not see:$/ do |expected|
130
- in_current_dir do
129
+ cd(".") do
131
130
  expect(@browser.last_response.body).to_not include(expected.chomp)
132
131
  end
133
132
  end
134
133
 
135
134
  Then /^the status code should be "([^\"]*)"$/ do |expected|
136
- in_current_dir do
135
+ cd(".") do
137
136
  expect(@browser.last_response.status).to eq expected.to_i
138
137
  end
139
138
  end
140
139
 
141
140
  Then /^I should see "([^\"]*)" lines$/ do |lines|
142
- in_current_dir do
141
+ cd(".") do
143
142
  expect(@last_response.body.chomp.split($/).length).to eq(lines.to_i)
144
143
  end
145
144
  end
@@ -1,5 +1,4 @@
1
1
  require 'aruba/cucumber'
2
- require 'aruba/jruby'
3
2
  require 'middleman-core/step_definitions/middleman_steps'
4
3
  require 'middleman-core/step_definitions/builder_steps'
5
4
  require 'middleman-core/step_definitions/server_steps'
@@ -128,7 +128,7 @@ module Middleman
128
128
  return unless resource = sitemap.find_resource_by_destination_path(current_path)
129
129
 
130
130
  # Look for partials relative to the current path
131
- current_dir = resource.source_file[:relative_path].dirname
131
+ current_dir = resource.file_descriptor[:relative_path].dirname
132
132
  non_root = partial_path.to_s.sub(/^\//, '')
133
133
  relative_dir = current_dir + Pathname(non_root)
134
134
 
@@ -138,7 +138,7 @@ module Middleman
138
138
  partial_file = nil
139
139
 
140
140
  [
141
- [relative_dir.to_s, { preferred_engine: resource.source_file[:relative_path].extname[1..-1].to_sym }],
141
+ [relative_dir.to_s, { preferred_engine: resource.file_descriptor[:relative_path].extname[1..-1].to_sym }],
142
142
  [non_root],
143
143
  [non_root, { try_static: try_static }],
144
144
  [relative_dir_no_underscore.to_s, { try_static: try_static }],
@@ -0,0 +1,153 @@
1
+ # Core Pathname library used for traversal
2
+ require 'pathname'
3
+
4
+ # DbC
5
+ require 'middleman-core/contracts'
6
+
7
+ # Shared util methods
8
+ require 'middleman-core/util'
9
+
10
+ # Parsing YAML data
11
+ require 'yaml'
12
+
13
+ # Parsing JSON data
14
+ require 'active_support/json'
15
+
16
+ module Middleman
17
+ module Util
18
+ module Data
19
+ include Contracts
20
+
21
+ module_function
22
+
23
+ YAML_ERRORS = [StandardError]
24
+
25
+ # https://github.com/tenderlove/psych/issues/23
26
+ if defined?(Psych) && defined?(Psych::SyntaxError)
27
+ YAML_ERRORS << Psych::SyntaxError
28
+ end
29
+
30
+ # Get the frontmatter and plain content from a file
31
+ # @param [String] path
32
+ # @return [Array<Hash, String>]
33
+ Contract Pathname, Maybe[Symbol] => [Hash, Maybe[String]]
34
+ def parse(full_path, known_type=nil)
35
+ data = {}
36
+
37
+ return [data, nil] if ::Middleman::Util.binary?(full_path)
38
+
39
+ # Avoid weird race condition when a file is renamed.
40
+ content = begin
41
+ File.read(full_path)
42
+ rescue ::EOFError
43
+ rescue ::IOError
44
+ rescue ::Errno::ENOENT
45
+ ''
46
+ end
47
+
48
+ begin
49
+ if content =~ /\A.*coding:/
50
+ lines = content.split(/\n/)
51
+ lines.shift
52
+ content = lines.join("\n")
53
+ end
54
+
55
+ if known_type
56
+ if known_type == :yaml
57
+ result = parse_yaml(content, full_path, true)
58
+ elsif known_type == :json
59
+ result = parse_json(content, full_path)
60
+ end
61
+ else
62
+ result = parse_yaml(content, full_path, false)
63
+ end
64
+
65
+ return result if result
66
+ rescue
67
+ # Probably a binary file, move on
68
+ end
69
+
70
+ [data, content]
71
+ end
72
+
73
+ # Parse YAML frontmatter out of a string
74
+ # @param [String] content
75
+ # @return [Array<Hash, String>]
76
+ Contract String, Pathname, Bool => Maybe[[Hash, String]]
77
+ def parse_yaml(content, full_path, require_yaml=false)
78
+ total_delims = content.scan(/^(?:---|\.\.\.)\s*(?:\n|$)/).length
79
+ has_first_line_delim = !content.match(/\A(---\s*(?:\n|$))/).nil?
80
+ # has_closing_delim = (total_delims > 1 && has_first_line_delim) || (!has_first_line_delim && total_delims == 1)
81
+
82
+ parts = content.split(/^(?:---|\.\.\.)\s*(?:\n|$)/)
83
+ parts.shift if parts[0].empty?
84
+
85
+ yaml_string = nil
86
+ additional_content = nil
87
+
88
+ if require_yaml
89
+ yaml_string = parts[0]
90
+ additional_content = parts[1]
91
+ else
92
+ if total_delims > 1
93
+ if has_first_line_delim
94
+ yaml_string = parts[0]
95
+ additional_content = parts[1]
96
+ else
97
+ additional_content = content
98
+ end
99
+ else
100
+ additional_content = parts[0]
101
+ end
102
+ end
103
+
104
+ return [{}, additional_content] if yaml_string.nil?
105
+
106
+ begin
107
+ data = map_value(::YAML.load(yaml_string) || {})
108
+ rescue *YAML_ERRORS => e
109
+ $stderr.puts "YAML Exception parsing #{full_path}: #{e.message}"
110
+ return nil
111
+ end
112
+
113
+ [data, additional_content]
114
+ rescue
115
+ [{}, additional_content]
116
+ end
117
+
118
+ # Parse JSON frontmatter out of a string
119
+ # @param [String] content
120
+ # @return [Array<Hash, String>]
121
+ Contract String, Pathname => Maybe[[Hash, String]]
122
+ def parse_json(content, full_path)
123
+ begin
124
+ data = map_value(::ActiveSupport::JSON.decode(content))
125
+ rescue => e
126
+ $stderr.puts "JSON Exception parsing #{full_path}: #{e.message}"
127
+ return nil
128
+ end
129
+
130
+ [data, nil]
131
+ rescue
132
+ [{}, nil]
133
+ end
134
+
135
+ def symbolize_recursive(hash)
136
+ {}.tap do |h|
137
+ hash.each { |key, value| h[key.to_sym] = map_value(value) }
138
+ end
139
+ end
140
+
141
+ def map_value(thing)
142
+ case thing
143
+ when Hash
144
+ symbolize_recursive(thing)
145
+ when Array
146
+ thing.map { |v| map_value(v) }
147
+ else
148
+ thing
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
@@ -11,8 +11,8 @@ require 'rack/mime'
11
11
  # DbC
12
12
  require 'middleman-core/contracts'
13
13
 
14
- # Immutable Data
15
- require 'hamster'
14
+ # Indifferent Access
15
+ require 'hashie'
16
16
 
17
17
  # For URI templating
18
18
  require 'addressable/uri'
@@ -76,56 +76,25 @@ module Middleman
76
76
  end
77
77
  end
78
78
 
79
- class IndifferentHash < ::Hamster::Hash
80
- def [](key)
81
- if key?(key.to_sym)
82
- super(key.to_sym)
83
- elsif key?(key.to_s)
84
- super(key.to_s)
85
- else
86
- super
87
- end
88
- end
89
-
90
- def method_missing(key, *_args)
91
- if key?(key.to_sym)
92
- self[key.to_sym]
93
- elsif key?(key.to_s)
94
- self[key.to_s]
95
- end
96
- end
79
+ class EnhancedHash < ::Hashie::Mash
80
+ # include ::Hashie::Extensions::MergeInitializer
81
+ # include ::Hashie::Extensions::MethodReader
82
+ # include ::Hashie::Extensions::IndifferentAccess
97
83
  end
98
84
 
99
- # Recursively convert a normal Hash into a IndifferentHash
85
+ # Recursively convert a normal Hash into a EnhancedHash
100
86
  #
101
87
  # @private
102
88
  # @param [Hash] data Normal hash
103
- # @return [Middleman::Util::IndifferentHash]
104
- FrozenDataStructure = Frozen[Or[IndifferentHash, Array, String, TrueClass, FalseClass, Fixnum, NilClass]]
105
- Contract Maybe[Or[String, Array, Hash, IndifferentHash]] => Maybe[FrozenDataStructure]
89
+ # @return [Hash]
90
+ Contract Maybe[Hash] => Maybe[Or[Array, EnhancedHash]]
106
91
  def recursively_enhance(obj)
107
- case obj
108
- when ::Hash
109
- res = obj.map { |key, value| [recursively_enhance(key), recursively_enhance(value)] }
110
- IndifferentHash.new(res)
111
- when IndifferentHash
112
- obj.map { |key, value| [recursively_enhance(key), recursively_enhance(value)] }
113
- when ::Array
114
- res = obj.map { |element| recursively_enhance(element) }
115
- Hamster::Vector.new(res)
116
- when ::SortedSet
117
- # This clause must go before ::Set clause, since ::SortedSet is a ::Set.
118
- res = obj.map { |element| recursively_enhance(element) }
119
- Hamster::SortedSet.new(res)
120
- when ::Set
121
- res = obj.map { |element| recursively_enhance(element) }
122
- Hamster::Set.new(res)
123
- when Hamster::Vector, Hamster::Set, Hamster::SortedSet
124
- obj.map { |element| recursively_enhance(element) }
125
- when ::TrueClass, ::FalseClass, ::Fixnum, ::Symbol, ::NilClass
126
- obj
92
+ if obj.is_a? ::Array
93
+ obj.map { |e| recursively_enhance(e) }
94
+ elsif obj.is_a? ::Hash
95
+ ::Hashie::Mash.new(obj)
127
96
  else
128
- obj.dup.freeze
97
+ obj
129
98
  end
130
99
  end
131
100
 
@@ -1,5 +1,5 @@
1
1
  module Middleman
2
2
  # Current Version
3
3
  # @return [String]
4
- VERSION = '4.0.0.beta.2' unless const_defined?(:VERSION)
4
+ VERSION = '4.0.0.rc.1' unless const_defined?(:VERSION)
5
5
  end