middleman-core 3.3.12 → 3.4.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 (93) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -0
  3. data/cucumber.yml +2 -0
  4. data/features/asset_hash.feature +9 -0
  5. data/features/chained_templates.feature +4 -1
  6. data/features/cli/preview_server.feature +532 -0
  7. data/features/i18n_link_to.feature +209 -0
  8. data/features/javascript-testing.feature +18 -0
  9. data/features/redirects.feature +1 -0
  10. data/features/relative_assets.feature +11 -0
  11. data/features/support/env.rb +3 -0
  12. data/fixtures/asset-hash-app/source/fonts/fontawesome-webfont.woff +0 -0
  13. data/fixtures/asset-hash-app/source/fonts/fontawesome-webfont.woff2 +0 -0
  14. data/fixtures/asset-hash-app/source/stylesheets/uses_fonts.css +4 -0
  15. data/fixtures/chained-app/source/test.erb.combobreaker.str.erb +8 -0
  16. data/fixtures/javascript-app/config.rb +0 -0
  17. data/fixtures/javascript-app/source/index.html +17 -0
  18. data/fixtures/preview-server-app/bin/dns_server.rb +33 -0
  19. data/fixtures/preview-server-app/config-complications.rb +11 -0
  20. data/fixtures/preview-server-app/config-empty.rb +0 -0
  21. data/fixtures/preview-server-app/config.rb +11 -0
  22. data/fixtures/preview-server-app/source/index.html.erb +1 -0
  23. data/fixtures/preview-server-app/source/layout.erb +9 -0
  24. data/fixtures/preview-server-app/source/layouts/custom.erb +8 -0
  25. data/fixtures/preview-server-app/source/real.html +1 -0
  26. data/fixtures/preview-server-app/source/real/index.html.erb +5 -0
  27. data/fixtures/preview-server-app/source/should_be_ignored.html +1 -0
  28. data/fixtures/preview-server-app/source/should_be_ignored2.html +1 -0
  29. data/fixtures/preview-server-app/source/should_be_ignored3.html +1 -0
  30. data/fixtures/preview-server-app/source/static.html +1 -0
  31. data/fixtures/relative-assets-app/source/absolute_image_relative_css.html.erb +9 -0
  32. data/fixtures/relative-assets-app/source/javascripts/app.js +3 -0
  33. data/fixtures/relative-assets-app/source/relative_image.html.erb +1 -0
  34. data/fixtures/relative-assets-app/source/relative_image_absolute_css.html.erb +9 -0
  35. data/lib/middleman-core/application.rb +11 -5
  36. data/lib/middleman-core/cli/build.rb +3 -1
  37. data/lib/middleman-core/cli/bundler.rb +1 -1
  38. data/lib/middleman-core/cli/extension.rb +2 -2
  39. data/lib/middleman-core/cli/init.rb +3 -3
  40. data/lib/middleman-core/cli/server.rb +9 -1
  41. data/lib/middleman-core/configuration.rb +0 -2
  42. data/lib/middleman-core/core_extensions/data.rb +2 -2
  43. data/lib/middleman-core/core_extensions/external_helpers.rb +2 -1
  44. data/lib/middleman-core/core_extensions/file_watcher.rb +7 -0
  45. data/lib/middleman-core/core_extensions/front_matter.rb +13 -4
  46. data/lib/middleman-core/core_extensions/rendering.rb +20 -6
  47. data/lib/middleman-core/core_extensions/request.rb +2 -2
  48. data/lib/middleman-core/dns_resolver.rb +73 -0
  49. data/lib/middleman-core/dns_resolver/basic_network_resolver.rb +52 -0
  50. data/lib/middleman-core/dns_resolver/hosts_resolver.rb +63 -0
  51. data/lib/middleman-core/dns_resolver/local_link_resolver.rb +44 -0
  52. data/lib/middleman-core/dns_resolver/network_resolver.rb +42 -0
  53. data/lib/middleman-core/extension.rb +0 -1
  54. data/lib/middleman-core/meta_pages.rb +1 -1
  55. data/lib/middleman-core/preview_server.rb +94 -37
  56. data/lib/middleman-core/preview_server/checks.rb +81 -0
  57. data/lib/middleman-core/preview_server/information.rb +273 -0
  58. data/lib/middleman-core/preview_server/network_interface_inventory.rb +65 -0
  59. data/lib/middleman-core/preview_server/server_hostname.rb +39 -0
  60. data/lib/middleman-core/preview_server/server_information.rb +144 -0
  61. data/lib/middleman-core/preview_server/server_information_validator.rb +18 -0
  62. data/lib/middleman-core/preview_server/server_ip_address.rb +55 -0
  63. data/lib/middleman-core/preview_server/server_url.rb +50 -0
  64. data/lib/middleman-core/preview_server/tcp_port_prober.rb +29 -0
  65. data/lib/middleman-core/sitemap.rb +4 -4
  66. data/lib/middleman-core/sitemap/extensions/ignores.rb +5 -1
  67. data/lib/middleman-core/sitemap/extensions/proxies.rb +0 -1
  68. data/lib/middleman-core/sitemap/extensions/redirects.rb +2 -1
  69. data/lib/middleman-core/sitemap/queryable.rb +2 -1
  70. data/lib/middleman-core/sitemap/store.rb +3 -3
  71. data/lib/middleman-core/step_definitions.rb +1 -0
  72. data/lib/middleman-core/step_definitions/builder_steps.rb +7 -7
  73. data/lib/middleman-core/step_definitions/commandline_steps.rb +88 -0
  74. data/lib/middleman-core/step_definitions/server_steps.rb +25 -28
  75. data/lib/middleman-core/templates/extension/Gemfile +0 -1
  76. data/lib/middleman-core/templates/extension/Rakefile +1 -1
  77. data/lib/middleman-core/util.rb +24 -0
  78. data/lib/middleman-core/version.rb +1 -1
  79. data/lib/middleman-more/core_extensions/default_helpers.rb +49 -8
  80. data/lib/middleman-more/core_extensions/i18n.rb +101 -27
  81. data/lib/middleman-more/extensions/asset_hash.rb +1 -1
  82. data/lib/middleman-more/extensions/asset_host.rb +1 -1
  83. data/lib/middleman-more/extensions/automatic_image_sizes.rb +7 -0
  84. data/lib/middleman-more/extensions/cache_buster.rb +1 -1
  85. data/lib/middleman-more/extensions/lorem.rb +2 -2
  86. data/lib/middleman-more/extensions/minify_javascript.rb +6 -4
  87. data/lib/middleman-more/extensions/relative_assets.rb +4 -8
  88. data/middleman-core.gemspec +3 -3
  89. data/spec/middleman-core/dns_resolver_spec.rb +118 -0
  90. data/spec/middleman-core/preview_server/server_hostname_spec.rb +39 -0
  91. data/spec/middleman-core/preview_server/server_ip_address_spec.rb +43 -0
  92. data/spec/spec_helper.rb +26 -0
  93. metadata +81 -17
@@ -0,0 +1,273 @@
1
+ require 'ipaddr'
2
+ require 'active_support/core_ext/object/blank'
3
+ require 'middleman-core/preview_server/checks'
4
+ require 'middleman-core/preview_server/server_hostname'
5
+ require 'middleman-core/preview_server/server_ip_address'
6
+
7
+ module Middleman
8
+ class PreviewServer
9
+ # Basic information class to wrap common behaviour
10
+ class BasicInformation
11
+ private
12
+
13
+ attr_reader :checks, :network_interfaces_inventory
14
+
15
+ public
16
+
17
+ attr_accessor :bind_address, :server_name, :port, :reason, :valid
18
+ attr_reader :listeners, :site_addresses
19
+
20
+ # Create instance
21
+ #
22
+ # @param [String] bind_address
23
+ # The bind address of the server
24
+ #
25
+ # @param [String] server_name
26
+ # The name of the server
27
+ #
28
+ # @param [Integer] port
29
+ # The port to listen on
30
+ def initialize(opts={})
31
+ @bind_address = ServerIpAddress.new(opts[:bind_address])
32
+ @server_name = ServerHostname.new(opts[:server_name])
33
+ @port = opts[:port]
34
+ @valid = true
35
+
36
+ @site_addresses = []
37
+ @listeners = []
38
+ @checks = []
39
+
40
+ # This needs to be check for each use case. Otherwise `Webrick` will
41
+ # complain about that.
42
+ @checks << Checks::InterfaceIsAvailableOnSystem.new
43
+ end
44
+
45
+ # Is the given information valid?
46
+ def valid?
47
+ valid == true
48
+ end
49
+
50
+ # Pass "self" to validator
51
+ #
52
+ # @param [#validate] validator
53
+ # The validator
54
+ def validate_me(validator)
55
+ validator.validate self, checks
56
+ end
57
+
58
+ def resolve_me(*)
59
+ fail NoMethodError
60
+ end
61
+
62
+ # Get network information
63
+ #
64
+ # @param [#network_interfaces] inventory
65
+ # Get list of available network interfaces
66
+ def show_me_network_interfaces(inventory)
67
+ @network_interfaces_inventory = inventory
68
+ end
69
+
70
+ # Default is to get all network interfaces
71
+ def local_network_interfaces
72
+ network_interfaces_inventory.nil? ? [] : network_interfaces_inventory.network_interfaces(:all)
73
+ end
74
+ end
75
+
76
+ # This only is used if no other parser is available
77
+ #
78
+ # The "default" behaviour is to fail because of "Checks::DenyAnyAny"
79
+ class DefaultInformation < BasicInformation
80
+ def initialize(*args)
81
+ super
82
+
83
+ # Make this fail
84
+ @checks << Checks::DenyAnyAny.new
85
+ end
86
+
87
+ def resolve_me(*); end
88
+
89
+ # Always true
90
+ def self.matches?(*)
91
+ true
92
+ end
93
+ end
94
+
95
+ # This one is used if no bind address and no server name is given
96
+ class AllInterfaces < BasicInformation
97
+ def initialize(*args)
98
+ super
99
+
100
+ after_init
101
+ end
102
+
103
+ def self.matches?(opts={})
104
+ opts[:bind_address].blank? && opts[:server_name].blank?
105
+ end
106
+
107
+ # Resolve ips
108
+ def resolve_me(resolver)
109
+ hostname = ServerHostname.new(Socket.gethostname)
110
+ hostname_ips = resolver.ips_for(hostname)
111
+ network_interface = ServerIpAddress.new(Array(local_network_interfaces).first)
112
+ resolved_name = ServerHostname.new(resolver.names_for(network_interface).first)
113
+
114
+ if includes_array? local_network_interfaces, hostname_ips
115
+ @server_name = hostname
116
+ @site_addresses << hostname
117
+
118
+ network_interface = ServerIpAddress.new((local_network_interfaces & hostname_ips).first)
119
+ elsif RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/
120
+ @server_name = hostname
121
+ @site_addresses << hostname
122
+ elsif !resolved_name.blank?
123
+ @server_name = resolved_name
124
+ @site_addresses << resolved_name
125
+ else
126
+ @server_name = network_interface
127
+ end
128
+
129
+ @site_addresses << network_interface
130
+
131
+ self
132
+ end
133
+
134
+ private
135
+
136
+ def includes_array?(a, b)
137
+ !(a & b).empty?
138
+ end
139
+
140
+ def after_init
141
+ @listeners << ServerIpAddress.new('::')
142
+ @listeners << ServerIpAddress.new('0.0.0.0')
143
+ end
144
+ end
145
+
146
+ # This is used if bind address is 0.0.0.0, the server name needs to be
147
+ # blank
148
+ class AllIpv4Interfaces < AllInterfaces
149
+ def self.matches?(opts={})
150
+ opts[:bind_address] == '0.0.0.0' && opts[:server_name].blank?
151
+ end
152
+
153
+ # Use only ipv4 interfaces
154
+ def local_network_interfaces
155
+ network_interfaces_inventory.nil? ? [] : network_interfaces_inventory.network_interfaces(:ipv4)
156
+ end
157
+
158
+ private
159
+
160
+ def after_init
161
+ @listeners << ServerIpAddress.new('0.0.0.0')
162
+ end
163
+ end
164
+
165
+ # This is used if bind address is ::, the server name needs to be blank
166
+ class AllIpv6Interfaces < AllInterfaces
167
+ def self.matches?(opts={})
168
+ opts[:bind_address] == '::' && opts[:server_name].blank?
169
+ end
170
+
171
+ # Use only ipv6 interfaces
172
+ def local_network_interfaces
173
+ network_interfaces_inventory.nil? ? [] : network_interfaces_inventory.network_interfaces(:ipv6)
174
+ end
175
+
176
+ private
177
+
178
+ def after_init
179
+ @listeners << ServerIpAddress.new('::')
180
+ end
181
+ end
182
+
183
+ # Used if a bind address is given and the server name is blank
184
+ class BindAddressInformation < BasicInformation
185
+ def initialize(*args)
186
+ super
187
+
188
+ @listeners << bind_address
189
+ @site_addresses << bind_address
190
+ end
191
+
192
+ def self.matches?(opts={})
193
+ !opts[:bind_address].blank? && opts[:server_name].blank?
194
+ end
195
+
196
+ # Resolv
197
+ def resolve_me(resolver)
198
+ @server_name = ServerHostname.new(resolver.names_for(bind_address).first)
199
+ @site_addresses << @server_name unless @server_name.blank?
200
+
201
+ self
202
+ end
203
+ end
204
+
205
+ # Use if server name is given and bind address is blank
206
+ class ServerNameInformation < BasicInformation
207
+ def initialize(*args)
208
+ super
209
+
210
+ @checks << Checks::RequiresBindAddressIfServerNameIsGiven.new
211
+ @site_addresses << server_name
212
+ end
213
+
214
+ def resolve_me(resolver)
215
+ @bind_address = ServerIpAddress.new(resolver.ips_for(server_name).first)
216
+
217
+ unless bind_address.blank?
218
+ @listeners << bind_address
219
+ @site_addresses << bind_address
220
+ end
221
+
222
+ self
223
+ end
224
+
225
+ def self.matches?(opts={})
226
+ opts[:bind_address].blank? && !opts[:server_name].blank?
227
+ end
228
+ end
229
+
230
+ # Only used if bind address and server name are given and bind address is
231
+ # not :: or 0.0.0.0
232
+ class BindAddressAndServerNameInformation < BasicInformation
233
+ def initialize(*args)
234
+ super
235
+
236
+ @listeners << bind_address
237
+ @site_addresses << server_name
238
+ @site_addresses << bind_address
239
+
240
+ @checks << Checks::ServerNameResolvesToBindAddress.new
241
+ end
242
+
243
+ def self.matches?(opts={})
244
+ !opts[:bind_address].blank? && !opts[:server_name].blank? && !%w(:: 0.0.0.0).include?(opts[:bind_address])
245
+ end
246
+
247
+ def resolve_me(*); end
248
+ end
249
+
250
+ # If the server name is either an ipv4 or ipv6 address, e.g. 127.0.0.1 or
251
+ # ::1, use this one
252
+ class ServerNameIsIpInformation < BasicInformation
253
+ def initialize(opts={})
254
+ super
255
+
256
+ ip = ServerIpAddress.new(server_name.to_s)
257
+
258
+ @listeners << ip
259
+ @site_addresses << ip
260
+ end
261
+
262
+ def resolve_me(*); end
263
+
264
+ def self.matches?(opts={})
265
+ ip = IPAddr.new(opts[:server_name])
266
+
267
+ ip.ipv4? || ip.ipv6?
268
+ rescue
269
+ false
270
+ end
271
+ end
272
+ end
273
+ end
@@ -0,0 +1,65 @@
1
+ require 'middleman-core/preview_server/server_ip_address'
2
+
3
+ module Middleman
4
+ class PreviewServer
5
+ # This holds information about local network interfaces on the user systemd
6
+ class NetworkInterfaceInventory
7
+ # Return all ip interfaces
8
+ class All
9
+ def network_interfaces
10
+ ipv4_addresses = Socket.ip_address_list.select(&:ipv4?).map { |ai| ServerIpv4Address.new(ai.ip_address) }
11
+ ipv6_addresses = Socket.ip_address_list.select(&:ipv6?).map { |ai| ServerIpv6Address.new(ai.ip_address) }
12
+
13
+ ipv4_addresses + ipv6_addresses
14
+ end
15
+
16
+ def self.match?(*)
17
+ true
18
+ end
19
+ end
20
+
21
+ # Return all ipv4 interfaces
22
+ class Ipv4
23
+ def network_interfaces
24
+ Socket.ip_address_list.select { |ai| ai.ipv4? && !ai.ipv4_loopback? }.map { |ai| ServerIpv4Address.new(ai.ip_address) }
25
+ end
26
+
27
+ def self.match?(type)
28
+ :ipv4 == type
29
+ end
30
+ end
31
+
32
+ # Return all ipv6 interfaces
33
+ class Ipv6
34
+ def network_interfaces
35
+ Socket.ip_address_list.select { |ai| ai.ipv6? && !ai.ipv6_loopback? }.map { |ai| ServerIpv6Address.new(ai.ip_address) }
36
+ end
37
+
38
+ def self.match?(type)
39
+ :ipv6 == type
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ attr_reader :types
46
+
47
+ public
48
+
49
+ def initialize
50
+ @types = []
51
+ @types << Ipv4
52
+ @types << Ipv6
53
+ @types << All
54
+ end
55
+
56
+ # Return ip interfaces
57
+ #
58
+ # @param [Symbol] type
59
+ # The type of interface which should be returned
60
+ def network_interfaces(type=:all)
61
+ types.find { |t| t.match? type.to_sym }.new.network_interfaces
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,39 @@
1
+ module Middleman
2
+ class PreviewServer
3
+ class ServerHostname
4
+ class ServerFullHostname < SimpleDelegator
5
+ def to_s
6
+ __getobj__.gsub(/\s/, '+')
7
+ end
8
+
9
+ def self.match?(*)
10
+ true
11
+ end
12
+
13
+ alias_method :to_browser, :to_s
14
+ end
15
+
16
+ class ServerPlainHostname < SimpleDelegator
17
+ def to_s
18
+ __getobj__.gsub(/\s/, '+') + '.local'
19
+ end
20
+
21
+ def self.match?(name)
22
+ # rubocop:disable Style/CaseEquality
23
+ name != 'localhost' && /^(?=.{1,255}$)[0-9A-Za-z](?:(?:[0-9A-Za-z]|-){0,61}[0-9A-Za-z])?\.?$/ === name
24
+ # rubocop:enable Style/CaseEquality
25
+ end
26
+
27
+ alias_method :to_browser, :to_s
28
+ end
29
+
30
+ def self.new(string)
31
+ @names = []
32
+ @names << ServerPlainHostname
33
+ @names << ServerFullHostname
34
+
35
+ @names.find { |n| n.match? string }.new(string)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,144 @@
1
+ require 'middleman-core/dns_resolver'
2
+ require 'middleman-core/preview_server/information'
3
+ require 'middleman-core/preview_server/network_interface_inventory'
4
+ require 'middleman-core/preview_server/tcp_port_prober'
5
+ require 'middleman-core/preview_server/server_information_validator'
6
+
7
+ module Middleman
8
+ class PreviewServer
9
+ # This class holds all information which the preview server needs to setup a listener
10
+ #
11
+ # * server name
12
+ # * bind address
13
+ # * port
14
+ #
15
+ # Furthermore it probes for a free tcp port, if the default one 4567 is not available.
16
+ class ServerInformation
17
+ private
18
+
19
+ attr_reader :resolver, :validator, :network_interface_inventory, :informations, :tcp_port_prober
20
+
21
+ public
22
+
23
+ def initialize(opts={})
24
+ @resolver = opts.fetch(:resolver, DnsResolver.new)
25
+ @validator = opts.fetch(:validator, ServerInformationValidator.new)
26
+ @network_interface_inventory = opts.fetch(:network_interface_inventory, NetworkInterfaceInventory.new)
27
+ @tcp_port_prober = opts.fetch(:tcp_port_prober, TcpPortProber.new)
28
+
29
+ @informations = []
30
+ @informations << AllInterfaces
31
+ @informations << AllIpv4Interfaces
32
+ @informations << AllIpv6Interfaces
33
+ @informations << ServerNameIsIpInformation
34
+ @informations << ServerNameInformation
35
+ @informations << BindAddressInformation
36
+ @informations << BindAddressAndServerNameInformation
37
+ @informations << DefaultInformation
38
+ end
39
+
40
+ # The information
41
+ #
42
+ # Is cached
43
+ def information
44
+ return @information if @information
45
+
46
+ # The `DefaultInformation`-class always returns `true`, so there's
47
+ # always a klass available and find will never return nil
48
+ listener_klass = informations.find { |l| l.matches? bind_address: @bind_address, server_name: @server_name }
49
+ @information = listener_klass.new(bind_address: @bind_address, server_name: @server_name)
50
+
51
+ @information.show_me_network_interfaces(network_interface_inventory)
52
+ @information.resolve_me(resolver)
53
+ @information.port = tcp_port_prober.port(@port)
54
+ @information.validate_me(validator)
55
+
56
+ @information
57
+ end
58
+
59
+ # Use a middleman configuration to get information
60
+ #
61
+ # @param [#[]] config
62
+ # The middleman config
63
+ def use(config)
64
+ @bind_address = config[:bind_address]
65
+ @port = config[:port]
66
+ @server_name = config[:server_name]
67
+
68
+ config[:bind_address] = bind_address
69
+ config[:port] = port
70
+ config[:server_name] = server_name
71
+ end
72
+
73
+ # Make information of internal server class avaible to make debugging
74
+ # easier. This can be used to log the class which was used to determine
75
+ # the preview server settings
76
+ #
77
+ # @return [String]
78
+ # The name of the class
79
+ def handler
80
+ information.class.to_s
81
+ end
82
+
83
+ # Is the server information valid?
84
+ #
85
+ # This is used to output a helpful error message, which can be stored in
86
+ # `#reason`.
87
+ #
88
+ # @return [TrueClass, FalseClass]
89
+ # The result
90
+ def valid?
91
+ information.valid?
92
+ end
93
+
94
+ # The reason why the information is NOT valid
95
+ #
96
+ # @return [String]
97
+ # The reason why the information is not valid
98
+ def reason
99
+ information.reason
100
+ end
101
+
102
+ # The server name
103
+ #
104
+ # @return [String]
105
+ # The name of the server
106
+ def server_name
107
+ information.server_name
108
+ end
109
+
110
+ # The bind address of server
111
+ #
112
+ # @return [String]
113
+ # The bind address of the server
114
+ def bind_address
115
+ information.bind_address
116
+ end
117
+
118
+ # The port on which the server should listen
119
+ #
120
+ # @return [Integer]
121
+ # The port number
122
+ def port
123
+ information.port
124
+ end
125
+
126
+ # A list of site addresses
127
+ #
128
+ # @return [Array]
129
+ # A list of addresses which can be used to access the middleman preview
130
+ # server
131
+ def site_addresses
132
+ information.site_addresses
133
+ end
134
+
135
+ # A list of listeners
136
+ #
137
+ # @return [Array]
138
+ # A list of bind address where the
139
+ def listeners
140
+ information.listeners
141
+ end
142
+ end
143
+ end
144
+ end