middleman-core 4.0.0 → 4.1.0.rc.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/features/asset_hash.feature +63 -0
  3. data/features/asset_host.feature +14 -0
  4. data/features/cache_buster.feature +15 -1
  5. data/features/clean_build.feature +16 -0
  6. data/features/cli/preview_server.feature +6 -4
  7. data/features/cli_extension.feature +1 -1
  8. data/features/directory_index.feature +3 -1
  9. data/features/encoding_option.feature +28 -0
  10. data/features/i18n_mixed_sources.feature +39 -0
  11. data/features/ignore.feature +33 -4
  12. data/features/relative_assets.feature +14 -0
  13. data/features/support/env.rb +1 -1
  14. data/fixtures/asset-hash-app/source/index.html.erb +3 -0
  15. data/fixtures/asset-hash-app/source/layout.erb +3 -3
  16. data/fixtures/asset-hash-app/source/other.html.erb +3 -0
  17. data/fixtures/asset-hash-app/source/stylesheets/fragment.css.scss +7 -0
  18. data/fixtures/asset-hash-host-app/source/index.html.erb +3 -0
  19. data/fixtures/asset-hash-host-app/source/layout.erb +3 -3
  20. data/fixtures/asset-hash-host-app/source/other.html.erb +3 -0
  21. data/fixtures/asset-hash-host-app/source/stylesheets/fragment.css.scss +3 -0
  22. data/fixtures/i-8859-1-app/config.rb +1 -0
  23. data/fixtures/i-8859-1-app/source/index.html.erb +1 -0
  24. data/fixtures/i18n-mixed-sources/config.rb +1 -0
  25. data/fixtures/i18n-mixed-sources/locales/en.yml +4 -0
  26. data/fixtures/i18n-mixed-sources/locales/es.yml +4 -0
  27. data/fixtures/i18n-mixed-sources/source/a/sub.html.erb +9 -0
  28. data/fixtures/i18n-mixed-sources/source/b/index.html.erb +9 -0
  29. data/fixtures/i18n-mixed-sources/source/index.html.erb +9 -0
  30. data/fixtures/i18n-mixed-sources/source/localizable/a/index.html.erb +9 -0
  31. data/fixtures/i18n-mixed-sources/source/localizable/b/sub.html.erb +9 -0
  32. data/fixtures/i18n-mixed-sources/source/localizable/index.html.erb +9 -0
  33. data/fixtures/import-app/config.rb +1 -1
  34. data/fixtures/indexable-app/config.rb +1 -0
  35. data/fixtures/indexable-app/source/regex_leave_me_alone2.html +1 -0
  36. data/lib/middleman-core/application.rb +6 -5
  37. data/lib/middleman-core/builder.rb +7 -3
  38. data/lib/middleman-core/core_extensions/collections.rb +3 -1
  39. data/lib/middleman-core/core_extensions/collections/step_context.rb +6 -7
  40. data/lib/middleman-core/core_extensions/default_helpers.rb +2 -2
  41. data/lib/middleman-core/core_extensions/front_matter.rb +1 -0
  42. data/lib/middleman-core/core_extensions/i18n.rb +16 -9
  43. data/lib/middleman-core/core_extensions/routing.rb +1 -1
  44. data/lib/middleman-core/extension.rb +34 -0
  45. data/lib/middleman-core/extensions/asset_hash.rb +2 -0
  46. data/lib/middleman-core/extensions/asset_host.rb +2 -0
  47. data/lib/middleman-core/extensions/cache_buster.rb +2 -0
  48. data/lib/middleman-core/extensions/external_pipeline.rb +0 -2
  49. data/lib/middleman-core/extensions/gzip.rb +1 -1
  50. data/lib/middleman-core/extensions/relative_assets.rb +2 -0
  51. data/lib/middleman-core/load_paths.rb +1 -1
  52. data/lib/middleman-core/meta_pages/sitemap_resource.rb +1 -1
  53. data/lib/middleman-core/middleware/inline_url_rewriter.rb +10 -2
  54. data/lib/middleman-core/sitemap/extensions/ignores.rb +37 -39
  55. data/lib/middleman-core/sitemap/extensions/import.rb +24 -66
  56. data/lib/middleman-core/sitemap/extensions/move_file.rb +9 -38
  57. data/lib/middleman-core/sitemap/extensions/proxies.rb +20 -52
  58. data/lib/middleman-core/sitemap/extensions/traversal.rb +13 -1
  59. data/lib/middleman-core/sitemap/resource.rb +4 -9
  60. data/lib/middleman-core/sitemap/store.rb +10 -9
  61. data/lib/middleman-core/step_definitions/server_steps.rb +4 -0
  62. data/lib/middleman-core/util.rb +22 -24
  63. data/lib/middleman-core/version.rb +1 -1
  64. data/spec/middleman-core/util_spec.rb +10 -1
  65. metadata +34 -4
@@ -10,7 +10,7 @@
10
10
  # to .html, .htm, .js and .css.
11
11
  #
12
12
  class Middleman::Extensions::Gzip < ::Middleman::Extension
13
- option :exts, %w(.js .css .html .htm), 'File extensions to Gzip when building.'
13
+ option :exts, %w(.js .css .html .htm .svg), 'File extensions to Gzip when building.'
14
14
  option :ignore, [], 'Patterns to avoid gzipping'
15
15
  option :overwrite, false, 'Overwrite original files instead of adding .gz extension.'
16
16
 
@@ -5,6 +5,7 @@ class Middleman::Extensions::RelativeAssets < ::Middleman::Extension
5
5
  option :exts, %w(.css .png .jpg .jpeg .webp .svg .svgz .js .gif .ttf .otf .woff .woff2 .eot), 'List of extensions that get cache busters strings appended to them.'
6
6
  option :sources, %w(.htm .html .css), 'List of extensions that are searched for relative assets.'
7
7
  option :ignore, [], 'Regexes of filenames to skip adding query strings to'
8
+ option :rewrite_ignore, [], 'Regexes of filenames to skip processing for path rewrites'
8
9
 
9
10
  def initialize(app, options_hash={}, &block)
10
11
  super
@@ -18,6 +19,7 @@ class Middleman::Extensions::RelativeAssets < ::Middleman::Extension
18
19
  url_extensions: options.exts,
19
20
  source_extensions: options.sources,
20
21
  ignore: options.ignore,
22
+ rewrite_ignore: options.rewrite_ignore,
21
23
  middleman_app: app,
22
24
  proc: method(:rewrite_url)
23
25
  end
@@ -22,7 +22,7 @@ module Middleman
22
22
 
23
23
  # Set BUNDLE_GEMFILE and run Bundler setup. Raises an exception if there is no Gemfile
24
24
  def setup_bundler
25
- ENV['BUNDLE_GEMFILE'] ||= findup('Gemfile', ENV['MM_ROOT'])
25
+ ENV['BUNDLE_GEMFILE'] ||= File.join(findup('Gemfile', ENV['MM_ROOT']), 'Gemfile')
26
26
 
27
27
  unless File.exist?(ENV['BUNDLE_GEMFILE'])
28
28
  ENV['BUNDLE_GEMFILE'] = File.expand_path('../../../../Gemfile', __FILE__)
@@ -42,7 +42,7 @@ module Middleman
42
42
  props['Source File'] = @resource.file_descriptor ? @resource.file_descriptor[:full_path].to_s.sub(/^#{Regexp.escape(ENV['MM_ROOT'] + '/')}/, '') : 'Dynamic'
43
43
 
44
44
  data = @resource.data
45
- props['Data'] = data.inspect unless data.empty?
45
+ props['Data'] = data.to_hash(symbolize_keys: true).inspect unless data.empty?
46
46
 
47
47
  options = @resource.options
48
48
  props['Options'] = options.inspect unless options.empty?
@@ -34,13 +34,16 @@ module Middleman
34
34
  @source_exts_regex_text = Regexp.union(@source_exts).to_s
35
35
 
36
36
  @ignore = options.fetch(:ignore)
37
+ @rewrite_ignore = Array(options.fetch(:rewrite_ignore, []))
37
38
  end
38
39
 
39
40
  def call(env)
40
41
  status, headers, response = @rack_app.call(env)
41
42
 
42
- # Allow upstream request to skip all rewriting
43
- return [status, headers, response] if env['bypass_inline_url_rewriter'] == 'true'
43
+ # Allow configuration or upstream request to skip all rewriting
44
+ if rewrite_ignore?(env['PATH_INFO']) || env['bypass_inline_url_rewriter'] == 'true'
45
+ return [status, headers, response]
46
+ end
44
47
 
45
48
  # Allow upstream request to skip this specific rewriting
46
49
  if @uid
@@ -96,6 +99,11 @@ module Middleman
96
99
  false
97
100
  end
98
101
  end
102
+
103
+ Contract String => Bool
104
+ def rewrite_ignore?(path)
105
+ @rewrite_ignore.any? { |ignore| Middleman::Util.path_match(ignore, path) }
106
+ end
99
107
  end
100
108
  end
101
109
  end
@@ -2,58 +2,56 @@ module Middleman
2
2
  module Sitemap
3
3
  module Extensions
4
4
  # Class to handle managing ignores
5
- class Ignores < Extension
5
+ class Ignores < ConfigExtension
6
6
  self.resource_list_manipulator_priority = 0
7
7
 
8
- # Expose `create_ignore` as `app.ignore`
9
- expose_to_application ignore: :create_ignore
10
-
11
- # Expose `create_ignore` to config as `ignore`
12
8
  expose_to_config ignore: :create_ignore
13
9
 
14
- def initialize(app, config={}, &block)
15
- super
16
-
17
- # Array of callbacks which can assign ignored
18
- @ignored_callbacks = Set.new
19
-
20
- @app.sitemap.define_singleton_method(:ignored?, &method(:ignored?))
21
- end
22
-
23
10
  # Ignore a path or add an ignore callback
24
11
  # @param [String, Regexp] path Path glob expression, or path regex
25
- # @return [void]
26
- Contract Maybe[Or[String, Regexp]], Maybe[Proc] => Any
12
+ # @return [IgnoreDescriptor]
13
+ Contract Maybe[Or[String, Regexp]], Maybe[Proc] => RespondTo[:execute_descriptor]
27
14
  def create_ignore(path=nil, &block)
28
- if path.is_a? Regexp
29
- @ignored_callbacks << proc { |p| p =~ path }
30
- elsif path.is_a? String
31
- path_clean = ::Middleman::Util.normalize_path(path)
32
- if path_clean.include?('*') # It's a glob
33
- if defined? File::FNM_EXTGLOB
34
- @ignored_callbacks << proc { |p| File.fnmatch(path_clean, p, File::FNM_EXTGLOB) }
15
+ @app.sitemap.invalidate_resources_not_ignored_cache!
16
+ IgnoreDescriptor.new(path, block)
17
+ end
18
+
19
+ IgnoreDescriptor = Struct.new(:path, :block) do
20
+ def execute_descriptor(_app, resources)
21
+ resources.map do |r|
22
+ # Ignore based on the source path (without template extensions)
23
+ if ignored?(r.path)
24
+ r.ignore!
35
25
  else
36
- @ignored_callbacks << proc { |p| File.fnmatch(path_clean, p) }
26
+ # This allows files to be ignored by their source file name (with template extensions)
27
+ r.ignore! if !r.is_a?(ProxyResource) && r.file_descriptor && ignored?(r.file_descriptor[:relative_path].to_s)
37
28
  end
38
- else
39
- # Add a specific-path ignore unless that path is already covered
40
- return if ignored?(path_clean)
41
- @ignored_callbacks << proc { |p| p == path_clean }
29
+
30
+ r
42
31
  end
43
- elsif block_given?
44
- @ignored_callbacks << block
45
32
  end
46
33
 
47
- @app.sitemap.invalidate_resources_not_ignored_cache!
48
- end
34
+ def ignored?(match_path)
35
+ match_path = ::Middleman::Util.normalize_path(match_path)
36
+
37
+ if path.is_a? Regexp
38
+ match_path =~ path
39
+ elsif path.is_a? String
40
+ path_clean = ::Middleman::Util.normalize_path(path)
49
41
 
50
- # Whether a path is ignored
51
- # @param [String] path
52
- # @return [Boolean]
53
- Contract String => Bool
54
- def ignored?(path)
55
- path_clean = ::Middleman::Util.normalize_path(path)
56
- @ignored_callbacks.any? { |b| b.call(path_clean) }
42
+ if path_clean.include?('*') # It's a glob
43
+ if defined?(::File::FNM_EXTGLOB)
44
+ ::File.fnmatch(path_clean, match_path, ::File::FNM_EXTGLOB)
45
+ else
46
+ ::File.fnmatch(path_clean, match_path)
47
+ end
48
+ else
49
+ match_path == path_clean
50
+ end
51
+ elsif block_given?
52
+ block.call(match_path)
53
+ end
54
+ end
57
55
  end
58
56
  end
59
57
  end
@@ -4,46 +4,45 @@ require 'middleman-core/contracts'
4
4
  module Middleman
5
5
  module Sitemap
6
6
  module Extensions
7
- class Import < Extension
7
+ class Import < ConfigExtension
8
8
  self.resource_list_manipulator_priority = 1
9
9
 
10
- ImportFileDescriptor = Struct.new(:from, :to)
11
- ImportPathDescriptor = Struct.new(:from, :renameProc)
12
-
13
10
  # Expose `create_import_file` to config as `import_file`
14
11
  expose_to_config import_file: :create_import_file
15
12
 
16
13
  # Expose `create_import_path` to config as `import_path`
17
14
  expose_to_config import_path: :create_import_path
18
15
 
19
- def initialize(app, config={}, &block)
20
- super
21
-
22
- @import_file_configs = Set.new
23
- @import_path_configs = Set.new
16
+ ImportFileDescriptor = Struct.new(:from, :to) do
17
+ def execute_descriptor(app, resources)
18
+ resources + [
19
+ ::Middleman::Sitemap::Resource.new(app.sitemap, to, from)
20
+ ]
21
+ end
24
22
  end
25
23
 
26
- def after_configuration
27
- ::Middleman::CoreExtensions::Collections::StepContext.add_to_context(:import_file, &method(:create_import_file))
28
- ::Middleman::CoreExtensions::Collections::StepContext.add_to_context(:import_path, &method(:create_import_path))
29
- end
24
+ ImportPathDescriptor = Struct.new(:from, :renameProc) do
25
+ def execute_descriptor(app, resources)
26
+ resources + ::Middleman::Util.glob_directory(File.join(from, '**/*'))
27
+ .reject { |path| File.directory?(path) }
28
+ .map do |path|
29
+ target_path = Pathname(path).relative_path_from(Pathname(from).parent).to_s
30
30
 
31
- # Import an external file into `source`
32
- # @param [String] from The original path.
33
- # @param [String] to The new path.
34
- # @return [void]
35
- Contract String, String => Any
36
- def create_import_file(from, to)
37
- @import_file_configs << create_anonymous_import_file(from, to)
38
- @app.sitemap.rebuild_resource_list!(:added_import_file)
31
+ ::Middleman::Sitemap::Resource.new(
32
+ app.sitemap,
33
+ renameProc.call(target_path, path),
34
+ path
35
+ )
36
+ end
37
+ end
39
38
  end
40
39
 
41
40
  # Import an external file into `source`
42
41
  # @param [String] from The original path.
43
42
  # @param [String] to The new path.
44
- # @return [ImportFileDescriptor]
43
+ # @return [void]
45
44
  Contract String, String => ImportFileDescriptor
46
- def create_anonymous_import_file(from, to)
45
+ def create_import_file(from, to)
47
46
  ImportFileDescriptor.new(
48
47
  File.expand_path(from, @app.root),
49
48
  ::Middleman::Util.normalize_path(to)
@@ -54,54 +53,13 @@ module Middleman
54
53
  # @param [String] from The original path.
55
54
  # @param [Proc] block Renaming method
56
55
  # @return [void]
57
- Contract String, Maybe[Proc] => Any
56
+ Contract String, Maybe[Proc] => ImportPathDescriptor
58
57
  def create_import_path(from, &block)
59
- rename_proc = block_given? ? block : proc { |path| path }
60
- @import_path_configs << create_anonymous_import_path(from, rename_proc)
61
- @app.sitemap.rebuild_resource_list!(:added_import_path)
62
- end
63
-
64
- # Import an external glob into `source`
65
- # @param [String] from The original path.
66
- # @param [Proc] block Renaming method
67
- # @return [ImportPathDescriptor]
68
- Contract String, Proc => ImportPathDescriptor
69
- def create_anonymous_import_path(from, block)
70
58
  ImportPathDescriptor.new(
71
59
  from,
72
- block
60
+ block_given? ? block : proc { |path| path }
73
61
  )
74
62
  end
75
-
76
- Contract IsA['Middleman::SourceFile'] => Bool
77
- def ignored?(file)
78
- @app.config[:ignored_sitemap_matchers].any? { |_, fn| fn.call(file, @app) }
79
- end
80
-
81
- # Update the main sitemap resource list
82
- # @return Array<Middleman::Sitemap::Resource>
83
- Contract ResourceList => ResourceList
84
- def manipulate_resource_list(resources)
85
- resources + @import_file_configs.map { |c|
86
- ::Middleman::Sitemap::Resource.new(
87
- @app.sitemap,
88
- c[:to],
89
- c[:from]
90
- )
91
- } + @import_path_configs.flat_map { |c|
92
- ::Middleman::Util.glob_directory(File.join(c[:from], '**/*'))
93
- .reject { |path| File.directory?(path) }
94
- .map do |path|
95
- target_path = Pathname(path).relative_path_from(Pathname(c[:from]).parent).to_s
96
-
97
- ::Middleman::Sitemap::Resource.new(
98
- @app.sitemap,
99
- c[:renameProc].call(target_path, path),
100
- path
101
- )
102
- end
103
- }
104
- end
105
63
  end
106
64
  end
107
65
  end
@@ -6,62 +6,33 @@ module Middleman
6
6
  module Extensions
7
7
  # Manages the list of proxy configurations and manipulates the sitemap
8
8
  # to include new resources based on those configurations
9
- class MoveFile < Extension
10
- MoveFileDescriptor = Struct.new(:from, :to)
11
-
9
+ class MoveFile < ConfigExtension
12
10
  self.resource_list_manipulator_priority = 101
13
11
 
14
12
  # Expose `create_move_file` to config as `move_file`
15
13
  expose_to_config move_file: :create_move_file
16
14
 
17
- def initialize(app, config={}, &block)
18
- super
19
-
20
- @move_configs = Set.new
21
- end
15
+ MoveFileDescriptor = Struct.new(:from, :to) do
16
+ def execute_descriptor(_app, resources)
17
+ resources.each do |r|
18
+ r.destination_path = to if from == r.path || from == r.destination_path
19
+ end
22
20
 
23
- def after_configuration
24
- ::Middleman::CoreExtensions::Collections::StepContext.add_to_context(:move_file, &method(:create_move_file))
21
+ resources
22
+ end
25
23
  end
26
24
 
27
25
  # Setup a move from one path to another
28
26
  # @param [String] from The original path.
29
27
  # @param [String] to The new path.
30
28
  # @return [void]
31
- Contract String, String => Any
32
- def create_move_file(from, to)
33
- @move_configs << create_anonymous_move(from, to)
34
- @app.sitemap.rebuild_resource_list!(:added_move_file)
35
- end
36
-
37
- # Setup a move from one path to another
38
- # @param [String] from The original path.
39
- # @param [String] to The new path.
40
- # @return [MoveFileDescriptor]
41
29
  Contract String, String => MoveFileDescriptor
42
- def create_anonymous_move(from, to)
30
+ def create_move_file(from, to)
43
31
  MoveFileDescriptor.new(
44
32
  ::Middleman::Util.normalize_path(from),
45
33
  ::Middleman::Util.normalize_path(to)
46
34
  )
47
35
  end
48
-
49
- # Update the main sitemap resource list
50
- # @return Array<Middleman::Sitemap::Resource>
51
- Contract ResourceList => ResourceList
52
- def manipulate_resource_list(resources)
53
- resources.each do |r|
54
- matches = @move_configs.select do |c|
55
- c.from == r.path || c.from == r.destination_path
56
- end
57
-
58
- if c = matches.last
59
- r.destination_path = c.to
60
- end
61
- end
62
-
63
- resources
64
- end
65
36
  end
66
37
  end
67
38
  end
@@ -6,28 +6,12 @@ module Middleman
6
6
  module Extensions
7
7
  # Manages the list of proxy configurations and manipulates the sitemap
8
8
  # to include new resources based on those configurations
9
- class Proxies < Extension
9
+ class Proxies < ConfigExtension
10
10
  self.resource_list_manipulator_priority = 0
11
11
 
12
- # Expose `create_proxy` as `app.proxy`
13
- expose_to_application proxy: :create_proxy
14
-
15
12
  # Expose `create_proxy` to config as `proxy`
16
13
  expose_to_config proxy: :create_proxy
17
14
 
18
- def initialize(app, config={}, &block)
19
- super
20
-
21
- @proxy_configs = Set.new
22
- @post_config = false
23
- end
24
-
25
- def after_configuration
26
- @post_config = true
27
-
28
- ::Middleman::CoreExtensions::Collections::StepContext.add_to_context(:proxy, &method(:create_anonymous_proxy))
29
- end
30
-
31
15
  # Setup a proxy from a path to a target
32
16
  # @param [String] path The new, proxied path to create
33
17
  # @param [String] target The existing path that should be proxied to. This must be a real resource, not another proxy.
@@ -36,51 +20,35 @@ module Middleman
36
20
  # @option opts [Boolean] directory_indexes Whether or not the `:directory_indexes` extension applies to these paths.
37
21
  # @option opts [Hash] locals Local variables for the template. These will be available when the template renders.
38
22
  # @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}.
39
- # @return [void]
40
- Contract String, String, Maybe[Hash] => Any
23
+ # @return [ProxyDescriptor]
24
+ Contract String, String, Maybe[Hash] => RespondTo[:execute_descriptor]
41
25
  def create_proxy(path, target, opts={})
42
- options = opts.dup
43
- @app.ignore(target) if options.delete(:ignore)
44
-
45
- @proxy_configs << create_anonymous_proxy(path, target, options)
46
- @app.sitemap.rebuild_resource_list!(:added_proxy)
47
- end
48
-
49
- # Setup a proxy from a path to a target
50
- # @param [String] path The new, proxied path to create
51
- # @param [String] target The existing path that should be proxied to. This must be a real resource, not another proxy.
52
- # @option opts [Boolean] ignore Ignore the target from the sitemap (so only the new, proxy resource ends up in the output)
53
- # @option opts [Symbol, Boolean, String] layout The layout name to use (e.g. `:article`) or `false` to disable layout.
54
- # @option opts [Boolean] directory_indexes Whether or not the `:directory_indexes` extension applies to these paths.
55
- # @option opts [Hash] locals Local variables for the template. These will be available when the template renders.
56
- # @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}.
57
- # @return [void]
58
- def create_anonymous_proxy(path, target, options={})
59
26
  ProxyDescriptor.new(
60
27
  ::Middleman::Util.normalize_path(path),
61
28
  ::Middleman::Util.normalize_path(target),
62
- options
29
+ opts.dup
63
30
  )
64
31
  end
65
-
66
- # Update the main sitemap resource list
67
- # @return Array<Middleman::Sitemap::Resource>
68
- Contract ResourceList => ResourceList
69
- def manipulate_resource_list(resources)
70
- resources + @proxy_configs.map { |c| c.to_resource(@app) }
71
- end
72
32
  end
73
33
 
74
34
  ProxyDescriptor = Struct.new(:path, :target, :metadata) do
75
- def to_resource(app)
76
- ProxyResource.new(app.sitemap, path, target).tap do |p|
77
- md = metadata.dup
78
- p.add_metadata(
79
- locals: md.delete(:locals) || {},
80
- page: md.delete(:data) || {},
81
- options: md
82
- )
35
+ def execute_descriptor(app, resources)
36
+ md = metadata.dup
37
+ should_ignore = md.delete(:ignore)
38
+
39
+ r = ProxyResource.new(app.sitemap, path, target)
40
+ r.add_metadata(
41
+ locals: md.delete(:locals) || {},
42
+ page: md.delete(:data) || {},
43
+ options: md
44
+ )
45
+
46
+ if should_ignore
47
+ d = ::Middleman::Sitemap::Extensions::Ignores::IgnoreDescriptor.new(target)
48
+ d.execute_descriptor(app, resources)
83
49
  end
50
+
51
+ resources + [r]
84
52
  end
85
53
  end
86
54
  end