frontman-ssg 0.0.2

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 (141) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +42 -0
  3. data/.github/CODE_OF_CONDUCT.md +9 -0
  4. data/.github/ISSUE_TEMPLATE/bug_report.md +25 -0
  5. data/.github/ISSUE_TEMPLATE/feature_request.md +22 -0
  6. data/.github/PULL_REQUEST_TEMPLATE.md +22 -0
  7. data/.gitignore +5 -0
  8. data/.rubocop.yml +88 -0
  9. data/CHANGELOG.md +11 -0
  10. data/CONTRIBUTING.md +42 -0
  11. data/Gemfile +5 -0
  12. data/LICENSE.md +21 -0
  13. data/Rakefile +94 -0
  14. data/SECURITY.md +6 -0
  15. data/bin/frontman +6 -0
  16. data/frontman-ssg.gemspec +48 -0
  17. data/frontman.svg +2 -0
  18. data/lib/frontman.rb +15 -0
  19. data/lib/frontman/app.rb +175 -0
  20. data/lib/frontman/bootstrapper.rb +70 -0
  21. data/lib/frontman/builder/asset_pipeline.rb +55 -0
  22. data/lib/frontman/builder/builder.rb +193 -0
  23. data/lib/frontman/builder/file.rb +55 -0
  24. data/lib/frontman/builder/mapping.rb +54 -0
  25. data/lib/frontman/builder/statistics_collector.rb +37 -0
  26. data/lib/frontman/cli.rb +6 -0
  27. data/lib/frontman/commands/build.rb +76 -0
  28. data/lib/frontman/commands/init.rb +58 -0
  29. data/lib/frontman/commands/serve.rb +110 -0
  30. data/lib/frontman/concerns/dispatch_events.rb +56 -0
  31. data/lib/frontman/concerns/forward_calls_to_app.rb +28 -0
  32. data/lib/frontman/config.rb +52 -0
  33. data/lib/frontman/context.rb +125 -0
  34. data/lib/frontman/custom_struct.rb +44 -0
  35. data/lib/frontman/data_store.rb +106 -0
  36. data/lib/frontman/data_store_file.rb +60 -0
  37. data/lib/frontman/helpers/app_helper.rb +18 -0
  38. data/lib/frontman/helpers/link_helper.rb +35 -0
  39. data/lib/frontman/helpers/render_helper.rb +76 -0
  40. data/lib/frontman/helpers/url_helper.rb +11 -0
  41. data/lib/frontman/iterator.rb +48 -0
  42. data/lib/frontman/process/chain.rb +43 -0
  43. data/lib/frontman/process/processor.rb +11 -0
  44. data/lib/frontman/renderers/erb_renderer.rb +21 -0
  45. data/lib/frontman/renderers/haml_renderer.rb +22 -0
  46. data/lib/frontman/renderers/markdown_renderer.rb +26 -0
  47. data/lib/frontman/renderers/renderer.rb +26 -0
  48. data/lib/frontman/renderers/renderer_resolver.rb +26 -0
  49. data/lib/frontman/resource.rb +279 -0
  50. data/lib/frontman/sitemap_tree.rb +211 -0
  51. data/lib/frontman/toolbox/timer.rb +49 -0
  52. data/lib/frontman/version.rb +6 -0
  53. data/project-templates/default/.gitignore +2 -0
  54. data/project-templates/default/Gemfile +3 -0
  55. data/project-templates/default/config.rb +17 -0
  56. data/project-templates/default/data/site.yml +4 -0
  57. data/project-templates/default/helpers/site_helper.rb +7 -0
  58. data/project-templates/default/public/code.css +77 -0
  59. data/project-templates/default/public/frontman-logo.svg +2 -0
  60. data/project-templates/default/public/main.css +27 -0
  61. data/project-templates/default/public/main.js +1 -0
  62. data/project-templates/default/source/index.html.md.erb +7 -0
  63. data/project-templates/default/source/sitemap.xml.erb +11 -0
  64. data/project-templates/default/views/layouts/main.erb +19 -0
  65. data/project-templates/default/views/layouts/main.haml +15 -0
  66. data/project-templates/default/views/partials/menu.erb +7 -0
  67. data/project-templates/webpack/.gitignore +4 -0
  68. data/project-templates/webpack/Gemfile +3 -0
  69. data/project-templates/webpack/README.md +54 -0
  70. data/project-templates/webpack/assets/css/code.css +77 -0
  71. data/project-templates/webpack/assets/css/style.css +27 -0
  72. data/project-templates/webpack/assets/images/.gitkeep +0 -0
  73. data/project-templates/webpack/assets/images/frontman_logo.svg +2 -0
  74. data/project-templates/webpack/assets/js/index.js +1 -0
  75. data/project-templates/webpack/config.rb +24 -0
  76. data/project-templates/webpack/data/site.yml +4 -0
  77. data/project-templates/webpack/helpers/assets_helper.rb +24 -0
  78. data/project-templates/webpack/helpers/site_helper.rb +7 -0
  79. data/project-templates/webpack/package-lock.json +7603 -0
  80. data/project-templates/webpack/package.json +34 -0
  81. data/project-templates/webpack/source/index.html.md.erb +7 -0
  82. data/project-templates/webpack/source/sitemap.xml.erb +11 -0
  83. data/project-templates/webpack/views/layouts/main.erb +20 -0
  84. data/project-templates/webpack/views/layouts/main.haml +14 -0
  85. data/project-templates/webpack/views/partials/menu.erb +7 -0
  86. data/project-templates/webpack/views/partials/script_with_vendors.haml +5 -0
  87. data/project-templates/webpack/webpack/base.config.js +51 -0
  88. data/project-templates/webpack/webpack/dev.config.js +6 -0
  89. data/project-templates/webpack/webpack/prod.config.js +30 -0
  90. data/readme.md +80 -0
  91. data/sorbet/config +2 -0
  92. data/sorbet/rbi/hidden-definitions/errors.txt +27259 -0
  93. data/sorbet/rbi/hidden-definitions/hidden.rbi +45122 -0
  94. data/sorbet/rbi/sorbet-typed/lib/rainbow/all/rainbow.rbi +276 -0
  95. data/sorbet/rbi/todo.rbi +6 -0
  96. data/spec/frontman/app_spec.rb +48 -0
  97. data/spec/frontman/bootstrapper_spec.rb +26 -0
  98. data/spec/frontman/builder/builder_spec.rb +79 -0
  99. data/spec/frontman/builder/file_spec.rb +45 -0
  100. data/spec/frontman/builder/mapping_spec.rb +8 -0
  101. data/spec/frontman/concerns/dispatch_events_spec.rb +70 -0
  102. data/spec/frontman/concerns/forward_calls_to_app_spec.rb +21 -0
  103. data/spec/frontman/config_spec.rb +54 -0
  104. data/spec/frontman/context_spec.rb +48 -0
  105. data/spec/frontman/custom_struct_spec.rb +51 -0
  106. data/spec/frontman/data_store_file_spec.rb +9 -0
  107. data/spec/frontman/data_store_spec.rb +36 -0
  108. data/spec/frontman/frontman_ssg_spec.rb +7 -0
  109. data/spec/frontman/helpers/app_helper_spec.rb +24 -0
  110. data/spec/frontman/helpers/link_helper_spec.rb +37 -0
  111. data/spec/frontman/helpers/render_helper_spec.rb +55 -0
  112. data/spec/frontman/helpers/url_helper_spec.rb +21 -0
  113. data/spec/frontman/iterator_spec.rb +47 -0
  114. data/spec/frontman/mocks/asset.css +3 -0
  115. data/spec/frontman/mocks/config.rb +0 -0
  116. data/spec/frontman/mocks/helpers/formatting_helper.rb +5 -0
  117. data/spec/frontman/mocks/helpers/language_helper.rb +5 -0
  118. data/spec/frontman/mocks/helpers/link_helper.rb +5 -0
  119. data/spec/frontman/mocks/helpers/test_command.rb +5 -0
  120. data/spec/frontman/mocks/html_file.html +8 -0
  121. data/spec/frontman/mocks/html_file.html.md.erb +9 -0
  122. data/spec/frontman/mocks/html_file.md.html +8 -0
  123. data/spec/frontman/mocks/info.yml +4 -0
  124. data/spec/frontman/mocks/layouts/raw_without_body.haml +1 -0
  125. data/spec/frontman/mocks/nested/data.yml +4 -0
  126. data/spec/frontman/mocks/nested/more_data.yml +4 -0
  127. data/spec/frontman/mocks/partials/paragraph.haml +2 -0
  128. data/spec/frontman/mocks/snippet/html_file.html +8 -0
  129. data/spec/frontman/mocks/snippet/html_file.yml +670 -0
  130. data/spec/frontman/mocks/test.html +8 -0
  131. data/spec/frontman/mocks/wrap.haml +3 -0
  132. data/spec/frontman/process/chain_spec.rb +56 -0
  133. data/spec/frontman/renderers/erb_renderer_spec.rb +22 -0
  134. data/spec/frontman/renderers/haml_renderer_spec.rb +12 -0
  135. data/spec/frontman/renderers/markdown_renderer_spec.rb +12 -0
  136. data/spec/frontman/renderers/renderer_spec.rb +16 -0
  137. data/spec/frontman/resource_spec.rb +151 -0
  138. data/spec/frontman/sitemap_tree_spec.rb +128 -0
  139. data/spec/frontman/toolbox/timer_spec.rb +34 -0
  140. data/spec/spec_setup.rb +19 -0
  141. metadata +507 -0
@@ -0,0 +1,55 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require 'sorbet-runtime'
5
+
6
+ module Frontman
7
+ module Builder
8
+ class File
9
+ extend T::Sig
10
+
11
+ attr_accessor :path, :status
12
+
13
+ sig { params(path: String, status: T.any(String, Symbol)).void }
14
+ def initialize(path, status)
15
+ unless valid_status?(status)
16
+ raise "#{status} is not a valid file status!"
17
+ end
18
+
19
+ @path = path
20
+ @status = status.to_sym
21
+ end
22
+
23
+ class << self
24
+ extend T::Sig
25
+
26
+ sig { params(path: String).returns(Frontman::Builder::File) }
27
+ def unchanged(path)
28
+ new(path, :unchanged)
29
+ end
30
+
31
+ sig { params(path: String).returns(Frontman::Builder::File) }
32
+ def deleted(path)
33
+ new(path, :deleted)
34
+ end
35
+
36
+ sig { params(path: String).returns(Frontman::Builder::File) }
37
+ def created(path)
38
+ new(path, :created)
39
+ end
40
+
41
+ sig { params(path: String).returns(Frontman::Builder::File) }
42
+ def updated(path)
43
+ new(path, :updated)
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ sig { params(status: T.any(String, Symbol)).returns(T::Boolean) }
50
+ def valid_status?(status)
51
+ %i[updated deleted created unchanged].include?(status.to_sym)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,54 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require 'fileutils'
5
+ require 'json'
6
+ require 'sorbet-runtime'
7
+
8
+ module Frontman
9
+ module Builder
10
+ class Mapping
11
+ extend T::Sig
12
+
13
+ attr_reader :output_path
14
+
15
+ sig { params(output_path: String).void }
16
+ def initialize(output_path)
17
+ @output_path = output_path
18
+ @mapping = {
19
+ updated: [],
20
+ created: [],
21
+ unchanged: [],
22
+ deleted: []
23
+ }
24
+ end
25
+
26
+ sig { returns(T::Hash[T.any(String, Symbol), T::Array[String]]) }
27
+ def all
28
+ @mapping
29
+ end
30
+
31
+ sig { params(build_file: Frontman::Builder::File).void }
32
+ def add_from_build_file(build_file)
33
+ add(build_file.status, build_file.path)
34
+ end
35
+
36
+ sig { params(status: T.any(String, Symbol), path: String).void }
37
+ def add(status, path)
38
+ @mapping[status.to_sym].push(path)
39
+ end
40
+
41
+ sig { void }
42
+ def save_file
43
+ ::File.open(output_path, 'w') do |f|
44
+ f.write(JSON.pretty_generate(@mapping))
45
+ end
46
+ end
47
+
48
+ sig { void }
49
+ def delete_file
50
+ FileUtils.rm_f(output_path)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,37 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require 'frontman/builder/builder'
5
+ require 'frontman/builder/mapping'
6
+ require 'frontman/toolbox/timer'
7
+ require 'sorbet-runtime'
8
+
9
+ module Frontman
10
+ module Builder
11
+ class StatisticsCollector
12
+ extend T::Sig
13
+
14
+ sig do
15
+ params(
16
+ builder: Frontman::Builder::Builder,
17
+ mapping: Frontman::Builder::Mapping,
18
+ timer: Frontman::Toolbox::Timer,
19
+ new_files: T::Array[String]
20
+ ).void
21
+ end
22
+ def self.output(builder, mapping, timer, new_files = [])
23
+ puts JSON.pretty_generate(mapping.all)
24
+ puts '================================================================='
25
+ puts "Previous build size : #{builder.current_build_files.size} files"
26
+ puts "Current build size : #{new_files.size} files"
27
+
28
+ %i[updated created deleted unchanged].each do |status|
29
+ puts "#{status} : #{Array(mapping.all[status]).length} files"
30
+ end
31
+
32
+ timer.stop
33
+ puts timer.output
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,6 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require 'frontman/commands/serve'
5
+ require 'frontman/commands/build'
6
+ require 'frontman/commands/init'
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: false
2
+
3
+ require 'thor'
4
+ require 'frontman/app'
5
+ require 'frontman/bootstrapper'
6
+ require 'frontman/builder/asset_pipeline'
7
+ require 'frontman/builder/builder'
8
+ require 'frontman/builder/mapping'
9
+ require 'frontman/builder/statistics_collector'
10
+ require 'frontman/config'
11
+ require 'frontman/toolbox/timer'
12
+ require 'frontman/sitemap_tree'
13
+
14
+ module Frontman
15
+ class CLI < Thor
16
+ option :parallel, type: :boolean
17
+ option :verbose, type: :boolean
18
+ desc 'build', 'Generate the HTML for your website'
19
+ def build
20
+ Frontman::Config.set(:mode, 'build')
21
+ Frontman::Bootstrapper.bootstrap_app(Frontman::App.instance)
22
+
23
+ assets_pipeline = Frontman::Builder::AssetPipeline.new(
24
+ Frontman::App.instance
25
+ .asset_pipelines
26
+ .filter { |p| %i[all build].include?(p[:mode]) }
27
+ )
28
+
29
+ assets_pipeline.run!(:before)
30
+
31
+ enable_parallel = options[:parallel]
32
+
33
+ Frontman::Config.set(:parallel, enable_parallel)
34
+
35
+ timer = Frontman::Toolbox::Timer.start
36
+
37
+ current_build_files = Dir.glob(Dir.pwd + '/build/**/*').reject do |f|
38
+ File.directory? f
39
+ end
40
+
41
+ public_dir = Frontman::Config.get(:public_dir, fallback: 'public/')
42
+ assets_to_build = Dir.glob(File.join(public_dir, '**/*')).reject do |f|
43
+ File.directory? f
44
+ end
45
+
46
+ mapping_path = Dir.pwd + '/_build.json'
47
+ mapping = Frontman::Builder::Mapping.new(mapping_path)
48
+ mapping.delete_file
49
+
50
+ build_directory = Dir.pwd + '/build/'
51
+ builder = Frontman::Builder::Builder.new
52
+ builder.build_directory = build_directory
53
+ builder.current_build_files = current_build_files
54
+
55
+ builder.on('created, updated, deleted, unchanged', lambda { |build_file|
56
+ mapping.add_from_build_file(build_file)
57
+ })
58
+
59
+ assets = builder.build_assets(assets_to_build)
60
+ redirects = builder.build_redirects
61
+
62
+ resources_paths = builder.build_from_resources(
63
+ Frontman::SitemapTree.resources
64
+ )
65
+
66
+ new_files = assets + redirects + resources_paths
67
+
68
+ builder.delete_files(current_build_files - new_files)
69
+ mapping.save_file
70
+
71
+ assets_pipeline.run!(:after)
72
+
73
+ Builder::StatisticsCollector.output(builder, mapping, timer, new_files)
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: false
2
+
3
+ require 'fileutils'
4
+ require 'thor'
5
+
6
+ module Frontman
7
+ class CLI < Thor
8
+ option :template
9
+ desc 'init', 'Bootstrap a new Frontman project'
10
+ def init(path)
11
+ template = options[:template] || 'default'
12
+ unless template_exists?(template)
13
+ raise "Template #{template} does not exist!"
14
+ end
15
+
16
+ target_dir = File.join(Dir.pwd, path == '.' ? '' : path)
17
+
18
+ unless allowed_to_modify_dir?(target_dir)
19
+ say 'Not bootstrapping new Frontman project'
20
+ return
21
+ end
22
+
23
+ copy_template(template, target_dir)
24
+
25
+ command = path == '.' ? '' : "cd #{path} && "
26
+ command += 'bundle exec frontman serve'
27
+
28
+ say "Your project is ready. Run `#{command}` and start developing!"
29
+ end
30
+
31
+ private
32
+
33
+ def copy_template(template, dest)
34
+ FileUtils.cp_r(
35
+ "#{path_to_template(template)}/.",
36
+ dest
37
+ )
38
+ end
39
+
40
+ def allowed_to_modify_dir?(dir)
41
+ return true if !Dir.exist?(dir) || Dir.empty?(dir)
42
+
43
+ say 'This folder already contains files. '
44
+ say 'Initializing a new Frontman project here may override these files.'
45
+ answer = ask('Are you sure you want to continue? [y/N]')
46
+
47
+ answer.to_s.downcase == 'y'
48
+ end
49
+
50
+ def template_exists?(template)
51
+ Dir.exist?(path_to_template(template))
52
+ end
53
+
54
+ def path_to_template(template)
55
+ File.join(__dir__, '../../../project-templates', template)
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: false
2
+
3
+ require 'thor'
4
+ require 'sinatra/base'
5
+ require 'better_errors'
6
+ require 'listen'
7
+ require 'frontman/app'
8
+ require 'frontman/bootstrapper'
9
+ require 'frontman/builder/asset_pipeline'
10
+ require 'frontman/config'
11
+ require 'frontman/resource'
12
+
13
+ module Frontman
14
+ class CLI < Thor
15
+ desc 'serve', 'Serve your application'
16
+ def serve
17
+ Frontman::Config.set(:mode, 'serve')
18
+ app = Frontman::App.instance
19
+ Frontman::Bootstrapper.bootstrap_app(app)
20
+
21
+ assets_pipeline = Frontman::Builder::AssetPipeline.new(
22
+ app
23
+ .asset_pipelines
24
+ .filter { |p| %i[all serve].include?(p[:mode]) }
25
+ )
26
+ processes = assets_pipeline.run_in_background!(:before)
27
+
28
+ helpers_dir = Frontman::Config.get(:helpers_dir, fallback: 'helpers')
29
+ content_dir = Frontman::Config.get(:content_dir, fallback: 'source/')
30
+ listen_to_dirs = Frontman::Config.get(:observe_dirs, fallback:
31
+ [
32
+ Frontman::Config.get(:layout_dir, fallback: 'views/layouts'),
33
+ Frontman::Config.get(:partial_dir, fallback: 'views/partials'),
34
+ content_dir,
35
+ helpers_dir
36
+ ]).filter { |dir| Dir.exist?(dir) }
37
+ app.refresh_data_files = true
38
+
39
+ listener = Listen.to(*listen_to_dirs) do |modified, added|
40
+ (added + modified).each do |m|
41
+ resource_path = m.sub("#{Dir.pwd}/", '')
42
+ begin
43
+ if resource_path.start_with?(helpers_dir)
44
+ helper_name = File.basename(resource_path).gsub('.rb', '')
45
+ load("./#{resource_path}")
46
+ app.register_helpers(
47
+ [{
48
+ path: File.join(Dir.pwd, resource_path),
49
+ name: helper_name.split('_').collect(&:capitalize).join
50
+ }]
51
+ )
52
+ elsif resource_path.start_with?(*listen_to_dirs)
53
+ r = Frontman::Resource.from_path(resource_path)
54
+
55
+ if resource_path.start_with?(content_dir)
56
+ exists = app.sitemap_tree.from_resource(r)
57
+ app.sitemap_tree.add(r) unless exists
58
+ end
59
+
60
+ r&.parse_resource(true)
61
+ elsif resource_path.end_with?('.rb')
62
+ load("./#{resource_path}")
63
+ end
64
+ rescue Error
65
+ # We ignore all errors to prevent the listener from crashing.
66
+ # Errors will be surfaced by the server instead.
67
+ end
68
+ end
69
+ end
70
+
71
+ listener.start
72
+
73
+ FrontManServer.set :public_folder, Frontman::Config.get(
74
+ :public_dir, fallback: 'public'
75
+ )
76
+ FrontManServer.run! do
77
+ host = "http://localhost:#{FrontManServer.settings.port}"
78
+ print "== View your site at \"#{host}/\"\n"
79
+ processes += assets_pipeline.run_in_background!(:after)
80
+ at_exit { processes.each { |pid| Process.kill(0, pid) } }
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ class FrontManServer < Sinatra::Base
87
+ set :port, 4568
88
+ set :server_settings,
89
+ # Avoid having webrick displaying logs for every requests to the serve
90
+ AccessLog: [],
91
+ # Remove logger for WebRick, we have the one of sinatra already
92
+ Logger: Rack::NullLogger.new(self)
93
+
94
+ use BetterErrors::Middleware
95
+ BetterErrors.application_root = Dir.pwd
96
+
97
+ get '*' do |path|
98
+ app = Frontman::App.instance
99
+ return redirect to app.get_redirect(path), 302 if app.get_redirect(path)
100
+
101
+ tree = app.sitemap_tree.from_url(path)
102
+ if tree&.resource
103
+ extension = File.extname(tree.resource.destination_path)
104
+ headers['Content-Type'] = Rack::Mime.mime_type(extension)
105
+ tree.resource.render
106
+ else
107
+ halt 404
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,56 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require 'sorbet-runtime'
5
+
6
+ module Frontman
7
+ module DispatchEvents
8
+ extend T::Sig
9
+
10
+ sig { returns(T::Hash[Symbol, T::Array[T.untyped]]) }
11
+ def listeners
12
+ @listeners ||= {}
13
+ end
14
+
15
+ sig do
16
+ params(events: T.any(Symbol, String), callback: T.untyped)
17
+ .returns(T.self_type)
18
+ end
19
+ def on(events, callback)
20
+ list(events).each do |event_name|
21
+ listeners[event_name.to_sym] ||= []
22
+ T.must(listeners[event_name.to_sym]).push(callback)
23
+ end
24
+
25
+ self
26
+ end
27
+
28
+ # We don't annotate with sig because of bad support for splat arguments
29
+ def emit(events, *arguments)
30
+ list(events).each do |event_name|
31
+ event_listeners = listeners[event_name.to_sym] || []
32
+ event_listeners.each do |listener|
33
+ listener.call(*arguments)
34
+ end
35
+ end
36
+
37
+ self
38
+ end
39
+
40
+ sig { params(events: T.any(Symbol, String)).returns(T.self_type) }
41
+ def off(events)
42
+ list(events).each do |event_name|
43
+ listeners[event_name.to_sym] = []
44
+ end
45
+
46
+ self
47
+ end
48
+
49
+ private
50
+
51
+ sig { params(events: T.any(Symbol, String)).returns(T::Array[String]) }
52
+ def list(events)
53
+ events.to_s.split(',').map(&:strip)
54
+ end
55
+ end
56
+ end