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

Sign up to get free protection for your applications and to get access to all the features.
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