thredded_create_app 0.1.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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +185 -0
- data/exe/thredded_create_app +8 -0
- data/lib/thredded_create_app.rb +5 -0
- data/lib/thredded_create_app/cli.rb +182 -0
- data/lib/thredded_create_app/command_error.rb +14 -0
- data/lib/thredded_create_app/generator.rb +91 -0
- data/lib/thredded_create_app/logging.rb +36 -0
- data/lib/thredded_create_app/tasks/add_devise.rb +63 -0
- data/lib/thredded_create_app/tasks/add_display_name_to_users.rb +126 -0
- data/lib/thredded_create_app/tasks/add_display_name_to_users/add_display_name_to_users.rb +12 -0
- data/lib/thredded_create_app/tasks/add_simple_form.rb +20 -0
- data/lib/thredded_create_app/tasks/add_thredded.rb +79 -0
- data/lib/thredded_create_app/tasks/add_thredded/_myapp-thredded.scss +18 -0
- data/lib/thredded_create_app/tasks/add_thredded/add_admin_to_users.rb +10 -0
- data/lib/thredded_create_app/tasks/add_thredded/myapp_thredded.js +1 -0
- data/lib/thredded_create_app/tasks/add_thredded/spec/features/thredded_spec.rb +9 -0
- data/lib/thredded_create_app/tasks/add_thredded/thredded.en.yml +8 -0
- data/lib/thredded_create_app/tasks/add_thredded/thredded_initializer_controller.rb +9 -0
- data/lib/thredded_create_app/tasks/base.rb +101 -0
- data/lib/thredded_create_app/tasks/create_rails_app.rb +36 -0
- data/lib/thredded_create_app/tasks/docker.rb +22 -0
- data/lib/thredded_create_app/tasks/docker/Dockerfile.erb +22 -0
- data/lib/thredded_create_app/tasks/docker/docker-compose.yml.erb +22 -0
- data/lib/thredded_create_app/tasks/docker/wait-for-tcp +30 -0
- data/lib/thredded_create_app/tasks/production_configs.rb +21 -0
- data/lib/thredded_create_app/tasks/production_configs/Procfile +1 -0
- data/lib/thredded_create_app/tasks/production_configs/puma.production.rb +18 -0
- data/lib/thredded_create_app/tasks/setup_app_skeleton.rb +160 -0
- data/lib/thredded_create_app/tasks/setup_app_skeleton/_flash-messages.scss +19 -0
- data/lib/thredded_create_app/tasks/setup_app_skeleton/_flash_messages.html.erb +7 -0
- data/lib/thredded_create_app/tasks/setup_app_skeleton/_header.html.erb +13 -0
- data/lib/thredded_create_app/tasks/setup_app_skeleton/_variables.scss.erb +6 -0
- data/lib/thredded_create_app/tasks/setup_app_skeleton/application.body.html.erb +7 -0
- data/lib/thredded_create_app/tasks/setup_app_skeleton/application.scss +83 -0
- data/lib/thredded_create_app/tasks/setup_app_skeleton/application_helper_methods.rb +17 -0
- data/lib/thredded_create_app/tasks/setup_app_skeleton/en.yml.erb +7 -0
- data/lib/thredded_create_app/tasks/setup_app_skeleton/home_show.html.erb.erb +47 -0
- data/lib/thredded_create_app/tasks/setup_app_skeleton/seeds.rb.erb +52 -0
- data/lib/thredded_create_app/tasks/setup_app_skeleton/spec/controllers/users_controller_spec.rb +21 -0
- data/lib/thredded_create_app/tasks/setup_app_skeleton/spec/features/homepage_spec.rb +9 -0
- data/lib/thredded_create_app/tasks/setup_app_skeleton/users_show.html.erb +26 -0
- data/lib/thredded_create_app/tasks/setup_database.rb +42 -0
- data/lib/thredded_create_app/tasks/setup_database/create_postgresql_user.sh +25 -0
- data/lib/thredded_create_app/tasks/setup_database/database.yml.erb +36 -0
- data/lib/thredded_create_app/version.rb +4 -0
- metadata +175 -0
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'term/ansicolor'
|
3
|
+
module ThreddedCreateApp
|
4
|
+
module Logging
|
5
|
+
def log_verbose(message = nil)
|
6
|
+
return unless verbose?
|
7
|
+
log_stderr Term::ANSIColor.bright_magenta(message || yield)
|
8
|
+
end
|
9
|
+
|
10
|
+
def log_command(message)
|
11
|
+
log_stderr Term::ANSIColor.bold message
|
12
|
+
end
|
13
|
+
|
14
|
+
def log_info(message)
|
15
|
+
log_stderr Term::ANSIColor.bright_blue message
|
16
|
+
end
|
17
|
+
|
18
|
+
def log_warn(message)
|
19
|
+
log_stderr Term::ANSIColor.yellow("#{program_name}: [WARN] #{message}")
|
20
|
+
end
|
21
|
+
|
22
|
+
def log_error(message)
|
23
|
+
log_stderr Term::ANSIColor.red Term::ANSIColor.bold(
|
24
|
+
"#{program_name}: #{message}"
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
def log_stderr(*args)
|
29
|
+
STDERR.puts(*args)
|
30
|
+
end
|
31
|
+
|
32
|
+
def program_name
|
33
|
+
@program_name ||= File.basename($PROGRAM_NAME)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'thredded_create_app/tasks/base'
|
3
|
+
module ThreddedCreateApp
|
4
|
+
module Tasks
|
5
|
+
class AddDevise < Base
|
6
|
+
def summary
|
7
|
+
'Add devise with I18n and configure a User model'
|
8
|
+
end
|
9
|
+
|
10
|
+
def before_bundle
|
11
|
+
add_gem 'devise'
|
12
|
+
add_gem 'devise-i18n'
|
13
|
+
end
|
14
|
+
|
15
|
+
def after_bundle
|
16
|
+
replace 'config/initializers/filter_parameter_logging.rb',
|
17
|
+
':password',
|
18
|
+
':password, :password_confirmation'
|
19
|
+
run_generator 'devise:install'
|
20
|
+
run_generator 'devise User'
|
21
|
+
setup_views
|
22
|
+
setup_after_sign_in_behaviour
|
23
|
+
git_commit 'Setup Devise'
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def setup_after_sign_in_behaviour
|
29
|
+
inject_into_file 'app/controllers/application_controller.rb',
|
30
|
+
after: /protect_from_forgery.*\n/,
|
31
|
+
content: <<-'RUBY'
|
32
|
+
|
33
|
+
before_action :store_current_location, unless: :devise_controller?
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def store_current_location
|
38
|
+
store_location_for(:user, request.url)
|
39
|
+
end
|
40
|
+
|
41
|
+
def after_sign_out_path_for(resource)
|
42
|
+
request.referrer || root_path
|
43
|
+
end
|
44
|
+
RUBY
|
45
|
+
end
|
46
|
+
|
47
|
+
def setup_views
|
48
|
+
run_generator 'devise:i18n:views -v sessions registrations'
|
49
|
+
# Make the views render-able outside Devise controllers
|
50
|
+
%w(app/views/devise/registrations/new.html.erb
|
51
|
+
app/views/devise/registrations/edit.html.erb
|
52
|
+
app/views/devise/sessions/new.html.erb
|
53
|
+
app/views/devise/shared/_links.html.erb).each do |path|
|
54
|
+
replace path, 'resource_name', ':user'
|
55
|
+
replace path, 'resource', ':user'
|
56
|
+
replace path, 'resource_class', 'User', optional: true
|
57
|
+
replace path, 'devise_mapping', 'Devise.mappings[:user]',
|
58
|
+
optional: true
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'thredded_create_app/tasks/base'
|
3
|
+
module ThreddedCreateApp
|
4
|
+
module Tasks
|
5
|
+
class AddDisplayNameToUsers < Base
|
6
|
+
def initialize(simple_form: true, **args)
|
7
|
+
super
|
8
|
+
@simple_form = simple_form
|
9
|
+
end
|
10
|
+
|
11
|
+
def summary
|
12
|
+
'Add display_name to the Devise User model'
|
13
|
+
end
|
14
|
+
|
15
|
+
def after_bundle
|
16
|
+
add_display_name
|
17
|
+
configure_devise
|
18
|
+
configure_thredded
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def add_display_name
|
24
|
+
run_generator 'migration add_display_name_to_users'
|
25
|
+
copy 'add_display_name_to_users/add_display_name_to_users.rb',
|
26
|
+
Dir['db/migrate/*_add_display_name_to_users.rb'][0]
|
27
|
+
model_path = 'app/models/user.rb'
|
28
|
+
inject_into_file model_path,
|
29
|
+
after: "ApplicationRecord\n",
|
30
|
+
content: display_name_model_rb
|
31
|
+
inject_into_file model_path,
|
32
|
+
before: /end\n\z/,
|
33
|
+
content: uniq_display_name_method_rb
|
34
|
+
git_commit 'Add a unique display_name User attribute'
|
35
|
+
end
|
36
|
+
|
37
|
+
def configure_devise
|
38
|
+
inject_into_file 'app/controllers/application_controller.rb',
|
39
|
+
after: /protect_from_forgery.*\n/,
|
40
|
+
content: devise_permitted_params_rb
|
41
|
+
%w(app/views/devise/registrations/new.html.erb
|
42
|
+
app/views/devise/registrations/edit.html.erb).each do |path|
|
43
|
+
autofocus = File.read(path).include?(', autofocus: true')
|
44
|
+
replace path, ', autofocus: true', '' if autofocus
|
45
|
+
if @simple_form
|
46
|
+
inject_into_file path,
|
47
|
+
after: %(<div class="form-inputs">\n),
|
48
|
+
content: simple_form_input_html(autofocus)
|
49
|
+
else
|
50
|
+
inject_into_file path,
|
51
|
+
after: /error_messages! %>\n\n/,
|
52
|
+
content: actionview_input_html(autofocus)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
git_commit 'Configure Devise to support display_name in forms'
|
56
|
+
end
|
57
|
+
|
58
|
+
def configure_thredded
|
59
|
+
replace 'config/initializers/thredded.rb',
|
60
|
+
'Thredded.user_name_column = :name',
|
61
|
+
'Thredded.user_name_column = :display_name'
|
62
|
+
git_commit 'Configure Thredded to use display_name as the user name'
|
63
|
+
end
|
64
|
+
|
65
|
+
def display_name_model_rb
|
66
|
+
<<-'RUBY'
|
67
|
+
validates :display_name, presence: true, uniqueness: true
|
68
|
+
before_validation :uniq_display_name!, on: :create
|
69
|
+
|
70
|
+
def display_name=(value)
|
71
|
+
super(value&.strip)
|
72
|
+
end
|
73
|
+
|
74
|
+
RUBY
|
75
|
+
end
|
76
|
+
|
77
|
+
def uniq_display_name_method_rb
|
78
|
+
<<-'RUBY'
|
79
|
+
private
|
80
|
+
|
81
|
+
# Makes the display_name unique by appending a number to it if necessary.
|
82
|
+
# "Gleb" => Gleb 1"
|
83
|
+
def uniq_display_name!
|
84
|
+
if display_name.present?
|
85
|
+
new_display_name = display_name
|
86
|
+
i = 0
|
87
|
+
while User.exists?(display_name: new_display_name)
|
88
|
+
new_display_name = "#{display_name} #{i += 1}"
|
89
|
+
end
|
90
|
+
self.display_name = new_display_name
|
91
|
+
end
|
92
|
+
end
|
93
|
+
RUBY
|
94
|
+
end
|
95
|
+
|
96
|
+
def devise_permitted_params_rb
|
97
|
+
<<-'RUBY'
|
98
|
+
before_action :configure_permitted_parameters, if: :devise_controller?
|
99
|
+
|
100
|
+
protected
|
101
|
+
|
102
|
+
def configure_permitted_parameters
|
103
|
+
devise_parameter_sanitizer.permit(:sign_up, keys: %i(display_name))
|
104
|
+
devise_parameter_sanitizer.permit(:account_update, keys: %i(display_name))
|
105
|
+
end
|
106
|
+
RUBY
|
107
|
+
end
|
108
|
+
|
109
|
+
def simple_form_input_html(autofocus)
|
110
|
+
<<-HTML
|
111
|
+
<%= f.input :display_name, required: true#{', autofocus: true' if autofocus} %>
|
112
|
+
HTML
|
113
|
+
end
|
114
|
+
|
115
|
+
def actionview_input_html(autofocus)
|
116
|
+
<<-HTML
|
117
|
+
<div class="field">
|
118
|
+
<%= f.label :display_name %><br />
|
119
|
+
<%= f.text_field :display_name#{', autofocus: true' if autofocus} %>
|
120
|
+
</div>
|
121
|
+
|
122
|
+
HTML
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
class AddDisplayNameToUsers < ActiveRecord::Migration[5.0]
|
3
|
+
def up
|
4
|
+
add_column :users, :display_name, :text, null: false
|
5
|
+
DbTextSearch::CaseInsensitive.add_index connection, :users, :display_name,
|
6
|
+
unique: true
|
7
|
+
end
|
8
|
+
|
9
|
+
def down
|
10
|
+
remove_column :users, :display_name
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'thredded_create_app/tasks/base'
|
3
|
+
module ThreddedCreateApp
|
4
|
+
module Tasks
|
5
|
+
class AddSimpleForm < Base
|
6
|
+
def summary
|
7
|
+
'Add the simple_form gem'
|
8
|
+
end
|
9
|
+
|
10
|
+
def before_bundle
|
11
|
+
add_gem 'simple_form'
|
12
|
+
end
|
13
|
+
|
14
|
+
def after_bundle
|
15
|
+
run_generator 'simple_form:install'
|
16
|
+
git_commit 'Install SimpleForm (rails g simple_form:install)'
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'thredded_create_app/tasks/base'
|
3
|
+
module ThreddedCreateApp
|
4
|
+
module Tasks
|
5
|
+
class AddThredded < Base
|
6
|
+
def summary
|
7
|
+
'Add and setup Thredded with a User model'
|
8
|
+
end
|
9
|
+
|
10
|
+
def before_bundle
|
11
|
+
add_gem 'thredded'
|
12
|
+
end
|
13
|
+
|
14
|
+
def after_bundle
|
15
|
+
install_thredded
|
16
|
+
git_commit 'Install thredded (rails g thredded:install)'
|
17
|
+
add_thredded_routes
|
18
|
+
copy 'add_thredded/thredded.en.yml', 'config/locales/thredded.en.yml'
|
19
|
+
set_thredded_layout
|
20
|
+
configure_thredded_controller
|
21
|
+
add_thredded_styles
|
22
|
+
add_thredded_javascripts
|
23
|
+
copy 'add_thredded/spec/features/thredded_spec.rb',
|
24
|
+
'spec/features/thredded_spec.rb'
|
25
|
+
git_commit 'Configure Thredded (routes, assets, behaviour, tests)'
|
26
|
+
add_admin_column_to_users
|
27
|
+
git_commit 'Add the admin column to users'
|
28
|
+
run 'bundle exec rails thredded:install:emoji'
|
29
|
+
git_commit 'Copied emoji to public/emoji'
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def install_thredded
|
35
|
+
run_generator 'thredded:install'
|
36
|
+
run 'bundle exec rails thredded:install:migrations' \
|
37
|
+
"#{' --quiet' unless verbose?}"
|
38
|
+
end
|
39
|
+
|
40
|
+
def add_thredded_routes
|
41
|
+
add_route "mount Thredded::Engine => '/forum'"
|
42
|
+
end
|
43
|
+
|
44
|
+
def set_thredded_layout
|
45
|
+
replace 'config/initializers/thredded.rb',
|
46
|
+
"Thredded.layout = 'thredded/application'",
|
47
|
+
"Thredded.layout = 'application'"
|
48
|
+
end
|
49
|
+
|
50
|
+
def configure_thredded_controller
|
51
|
+
copy 'add_thredded/thredded_initializer_controller.rb',
|
52
|
+
'config/initializers/thredded.rb',
|
53
|
+
mode: 'a'
|
54
|
+
end
|
55
|
+
|
56
|
+
def add_thredded_styles
|
57
|
+
copy 'add_thredded/_myapp-thredded.scss',
|
58
|
+
"app/assets/stylesheets/_#{app_name}-thredded.scss"
|
59
|
+
if File.file? 'app/assets/stylesheets/application.css'
|
60
|
+
File.delete 'app/assets/stylesheets/application.css'
|
61
|
+
end
|
62
|
+
File.write 'app/assets/stylesheets/application.scss',
|
63
|
+
"@import \"#{app_name}-thredded\";\n",
|
64
|
+
mode: 'a'
|
65
|
+
end
|
66
|
+
|
67
|
+
def add_thredded_javascripts
|
68
|
+
copy 'add_thredded/myapp_thredded.js',
|
69
|
+
"app/assets/javascripts/#{app_name}_thredded.js"
|
70
|
+
end
|
71
|
+
|
72
|
+
def add_admin_column_to_users
|
73
|
+
run_generator 'migration add_admin_to_users'
|
74
|
+
copy 'add_thredded/add_admin_to_users.rb',
|
75
|
+
Dir['db/migrate/*_add_admin_to_users.rb'][0]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
@import "variables";
|
2
|
+
|
3
|
+
$thredded-brand: $brand-primary;
|
4
|
+
$thredded-text-color: $text-color;
|
5
|
+
$thredded-base-font-family: $base-font-family;
|
6
|
+
$thredded-base-font-size: $base-font-size;
|
7
|
+
$thredded-base-line-height: $base-line-height;
|
8
|
+
$thredded-grid-container-max-width: $grid-container-max-width;
|
9
|
+
@import "thredded";
|
10
|
+
|
11
|
+
.thredded--main-container {
|
12
|
+
// The padding and max-width are handled by the app's container.
|
13
|
+
max-width: none;
|
14
|
+
padding: 0;
|
15
|
+
@include thredded-media-tablet-and-up {
|
16
|
+
padding: 0;
|
17
|
+
}
|
18
|
+
}
|
@@ -0,0 +1 @@
|
|
1
|
+
//= require thredded
|
@@ -0,0 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
Rails.application.config.to_prepare do
|
3
|
+
Thredded::ApplicationController.module_eval do
|
4
|
+
rescue_from Thredded::Errors::LoginRequired do |exception|
|
5
|
+
flash.now[:notice] = exception.message
|
6
|
+
render template: 'devise/sessions/new', status: :forbidden
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'shellwords'
|
3
|
+
require 'fileutils'
|
4
|
+
require 'erb'
|
5
|
+
require 'thredded_create_app/logging'
|
6
|
+
require 'thredded_create_app/command_error'
|
7
|
+
module ThreddedCreateApp
|
8
|
+
module Tasks
|
9
|
+
# @abstract
|
10
|
+
class Base
|
11
|
+
include ThreddedCreateApp::Logging
|
12
|
+
|
13
|
+
def initialize(app_path:, verbose: false, **_args)
|
14
|
+
@app_path = app_path
|
15
|
+
@app_name = File.basename(File.expand_path(app_path))
|
16
|
+
@verbose = verbose
|
17
|
+
@gems = []
|
18
|
+
end
|
19
|
+
|
20
|
+
def summary
|
21
|
+
self.class.name
|
22
|
+
end
|
23
|
+
|
24
|
+
def before_bundle
|
25
|
+
end
|
26
|
+
|
27
|
+
def after_bundle
|
28
|
+
end
|
29
|
+
|
30
|
+
protected
|
31
|
+
|
32
|
+
def add_gem(gem_name, version: nil, groups: nil)
|
33
|
+
log_verbose "+ gem #{gem_name}"
|
34
|
+
@gems << [gem_name, version, groups]
|
35
|
+
end
|
36
|
+
|
37
|
+
def git_commit(message)
|
38
|
+
log_info "Commiting: #{message}"
|
39
|
+
system 'git add -A .'
|
40
|
+
system(*['git', 'commit', '-m', "[thredded_create_app] #{message}",
|
41
|
+
('--quiet' unless verbose?)].compact)
|
42
|
+
end
|
43
|
+
|
44
|
+
attr_reader :app_name, :app_path, :gems
|
45
|
+
|
46
|
+
def copy(src_path, target_path, mode: 'w')
|
47
|
+
copy_template src_path, target_path, process_erb: false, mode: mode
|
48
|
+
end
|
49
|
+
|
50
|
+
def copy_template(src_path, target_path, process_erb: true, mode: 'w')
|
51
|
+
src = File.read(File.expand_path(src_path, File.dirname(__FILE__)))
|
52
|
+
src = ERB.new(src).result(binding) if process_erb
|
53
|
+
FileUtils.mkdir_p(File.dirname(target_path))
|
54
|
+
File.write target_path, src, mode: mode
|
55
|
+
end
|
56
|
+
|
57
|
+
def replace(path, pattern, replacement = nil, optional: false, &block)
|
58
|
+
src = File.read(path)
|
59
|
+
unless src.gsub!(pattern, replacement, &block) || optional
|
60
|
+
raise ThreddedCreateApp::CommandError,
|
61
|
+
"No match found for #{pattern} in #{path}"
|
62
|
+
end
|
63
|
+
File.write path, src
|
64
|
+
end
|
65
|
+
|
66
|
+
def add_route(route_str, append: false)
|
67
|
+
log_verbose "Add route: #{route_str}"
|
68
|
+
inject_into_file 'config/routes.rb',
|
69
|
+
content: " #{route_str}\n",
|
70
|
+
**(if append
|
71
|
+
{ after: /\.routes\.draw do\s*\n/m }
|
72
|
+
else
|
73
|
+
{ before: /end\n\z/ }
|
74
|
+
end)
|
75
|
+
end
|
76
|
+
|
77
|
+
def inject_into_file(path, content:, after: nil, before: nil)
|
78
|
+
replace path, (after || before), after ? '\0' + content : content + '\0'
|
79
|
+
end
|
80
|
+
|
81
|
+
def indent(n, s)
|
82
|
+
s.gsub(/^/, ' ' * n)
|
83
|
+
end
|
84
|
+
|
85
|
+
def run_generator(generate_args)
|
86
|
+
run "bundle exec rails g #{generate_args}#{' --quiet' unless verbose?}"
|
87
|
+
end
|
88
|
+
|
89
|
+
def run(*args, log: true)
|
90
|
+
if log
|
91
|
+
log_command args.length == 1 ? args[0] : Shellwords.shelljoin(args)
|
92
|
+
end
|
93
|
+
exit 1 unless system(*args)
|
94
|
+
end
|
95
|
+
|
96
|
+
def verbose?
|
97
|
+
@verbose
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|