belajar 0.1.1
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/.gitignore +16 -0
- data/.travis.yml +6 -0
- data/Gemfile +4 -0
- data/Guardfile +5 -0
- data/README.md +5 -0
- data/Rakefile +5 -0
- data/belajar.gemspec +38 -0
- data/bin/belajar +12 -0
- data/lib/belajar.rb +24 -0
- data/lib/belajar/chapter.rb +23 -0
- data/lib/belajar/configuration.rb +89 -0
- data/lib/belajar/congratulator.rb +13 -0
- data/lib/belajar/course.rb +80 -0
- data/lib/belajar/exceptions.rb +19 -0
- data/lib/belajar/generator.rb +60 -0
- data/lib/belajar/github_client.rb +30 -0
- data/lib/belajar/loadable.rb +23 -0
- data/lib/belajar/loading/chapters.rb +9 -0
- data/lib/belajar/loading/courses.rb +9 -0
- data/lib/belajar/loading/units.rb +9 -0
- data/lib/belajar/reference_solution.rb +18 -0
- data/lib/belajar/solution.rb +53 -0
- data/lib/belajar/storeable.rb +32 -0
- data/lib/belajar/task.rb +10 -0
- data/lib/belajar/terminal.rb +12 -0
- data/lib/belajar/terminal/cli.rb +59 -0
- data/lib/belajar/terminal/courses.rb +179 -0
- data/lib/belajar/terminal/output.rb +78 -0
- data/lib/belajar/terminal/setup.rb +115 -0
- data/lib/belajar/terminal/solutions.rb +46 -0
- data/lib/belajar/terminal/texts/about.txt +19 -0
- data/lib/belajar/terminal/texts/congratulations.txt +12 -0
- data/lib/belajar/terminal/texts/courses_empty.txt +3 -0
- data/lib/belajar/terminal/texts/hint_course_download.txt +13 -0
- data/lib/belajar/terminal/texts/welcome.txt +12 -0
- data/lib/belajar/terminal/welcome.rb +98 -0
- data/lib/belajar/test.rb +46 -0
- data/lib/belajar/test_result.rb +86 -0
- data/lib/belajar/unit.rb +28 -0
- data/lib/belajar/version.rb +3 -0
- data/lib/belajar/views.rb +51 -0
- data/lib/belajar/views/chapters_menu.rb +60 -0
- data/lib/belajar/views/courses_menu.rb +52 -0
- data/lib/belajar/views/main_menu.rb +37 -0
- data/lib/belajar/views/menu.rb +89 -0
- data/lib/belajar/views/splash.rb +57 -0
- data/lib/belajar/views/task_view.rb +236 -0
- data/lib/belajar/views/top_bar.rb +40 -0
- data/lib/belajar/views/units_menu.rb +61 -0
- data/lib/belajar/window.rb +215 -0
- data/spec/belajar/chapter_spec.rb +76 -0
- data/spec/belajar/configuration_spec.rb +161 -0
- data/spec/belajar/congratulator_spec.rb +24 -0
- data/spec/belajar/course_spec.rb +201 -0
- data/spec/belajar/generator_spec.rb +82 -0
- data/spec/belajar/github_client_spec.rb +53 -0
- data/spec/belajar/loading/chapters_spec.rb +16 -0
- data/spec/belajar/loading/courses_spec.rb +16 -0
- data/spec/belajar/loading/units_spec.rb +21 -0
- data/spec/belajar/reference_solution_spec.rb +41 -0
- data/spec/belajar/solution_spec.rb +86 -0
- data/spec/belajar/storeable_spec.rb +35 -0
- data/spec/belajar/task_spec.rb +23 -0
- data/spec/belajar/terminal/cli_spec.rb +51 -0
- data/spec/belajar/terminal/courses_spec.rb +293 -0
- data/spec/belajar/terminal/output_spec.rb +151 -0
- data/spec/belajar/terminal/setup_spec.rb +10 -0
- data/spec/belajar/terminal/solutions_spec.rb +8 -0
- data/spec/belajar/terminal/welcome_spec.rb +12 -0
- data/spec/belajar/terminal_spec.rb +24 -0
- data/spec/belajar/test_example_spec.rb +54 -0
- data/spec/belajar/test_result_spec.rb +91 -0
- data/spec/belajar/test_spec.rb +48 -0
- data/spec/belajar/unit_spec.rb +85 -0
- data/spec/belajar/views/chapters_menu_spec.rb +6 -0
- data/spec/belajar/views/courses_menu_spec.rb +6 -0
- data/spec/belajar/views/menu_spec.rb +19 -0
- data/spec/belajar/views/task_view_spec.rb +7 -0
- data/spec/belajar/views/units_menu_spec.rb +6 -0
- data/spec/belajar/views_spec.rb +21 -0
- data/spec/belajar_spec.rb +51 -0
- data/spec/path_helpers_spec.rb +60 -0
- data/spec/resource_helpers_spec.rb +33 -0
- data/spec/spec_helper.rb +28 -0
- data/spec/support/macros/content_helpers.rb +129 -0
- data/spec/support/macros/mock_helpers.rb +25 -0
- data/spec/support/macros/path_helpers.rb +139 -0
- data/spec/support/macros/resource_helpers.rb +157 -0
- data/spec/support/macros/test_helpers.rb +6 -0
- metadata +385 -0
@@ -0,0 +1,23 @@
|
|
1
|
+
module Belajar
|
2
|
+
module Loadable
|
3
|
+
|
4
|
+
require 'active_support/inflector'
|
5
|
+
|
6
|
+
def load(path)
|
7
|
+
if Dir.exist?(path)
|
8
|
+
dirs = Dir.entries(path).select do |entry|
|
9
|
+
!entry.match(/\./)
|
10
|
+
end
|
11
|
+
|
12
|
+
dirs.sort.map do |dir|
|
13
|
+
dir_path = File.join(path, dir)
|
14
|
+
class_name = self.to_s.demodulize.singularize
|
15
|
+
"Belajar::#{class_name}".constantize.new(dir_path)
|
16
|
+
end
|
17
|
+
else
|
18
|
+
Array.new
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Belajar
|
2
|
+
class ReferenceSolution
|
3
|
+
attr_reader :path
|
4
|
+
|
5
|
+
def initialize(path)
|
6
|
+
@path = Dir[File.join(path, '*solution.rb')].first
|
7
|
+
@code = File.read(@path).strip if @path
|
8
|
+
end
|
9
|
+
|
10
|
+
def code
|
11
|
+
@code.to_s
|
12
|
+
end
|
13
|
+
|
14
|
+
def code_lines
|
15
|
+
code.lines.map(&:chomp)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Belajar
|
2
|
+
class Solution
|
3
|
+
|
4
|
+
FILE_SUFFIX = '_solution.rb'
|
5
|
+
|
6
|
+
attr_reader :code, :path, :errors
|
7
|
+
|
8
|
+
def initialize(unit_path)
|
9
|
+
@unit_path = unit_path
|
10
|
+
@path = solution_path(unit_path)
|
11
|
+
@code = File.read(@path).strip if File.file?(@path)
|
12
|
+
@verified = get_store_state
|
13
|
+
end
|
14
|
+
|
15
|
+
def verify!
|
16
|
+
result = Belajar::Test.new(@unit_path).run(self.code)
|
17
|
+
set_store_state(result.passed?)
|
18
|
+
result
|
19
|
+
end
|
20
|
+
|
21
|
+
def verified?
|
22
|
+
!!@verified
|
23
|
+
end
|
24
|
+
|
25
|
+
def store_key
|
26
|
+
unless @store_key
|
27
|
+
part_path = path.split('/')[-3..-1].join('/').gsub(FILE_SUFFIX, '')
|
28
|
+
@store_key = Storeable.key(part_path, prefix: 'verified')
|
29
|
+
end
|
30
|
+
|
31
|
+
@store_key
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def solution_path(path)
|
37
|
+
local_path = Belajar.config.solutions_path
|
38
|
+
sub_dirs = Storeable.key(path.split('/')[-3..-2].join('/').gsub(FILE_SUFFIX, ''))
|
39
|
+
file = Storeable.key(File.basename(path)) + FILE_SUFFIX
|
40
|
+
|
41
|
+
File.join(local_path, sub_dirs, file)
|
42
|
+
end
|
43
|
+
|
44
|
+
def set_store_state(verified)
|
45
|
+
@verified = verified
|
46
|
+
QuickStore.store.set(store_key, verified?)
|
47
|
+
end
|
48
|
+
|
49
|
+
def get_store_state
|
50
|
+
QuickStore.store.get(store_key)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Belajar
|
2
|
+
module Storeable
|
3
|
+
|
4
|
+
LEADING_NUMBERS = /^\d+[\_\-\s]+/
|
5
|
+
PART_JOINTS = /[\_\-\s]+/
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def key(text, options = {})
|
9
|
+
separator = QuickStore.config.key_separator
|
10
|
+
prefix = options[:prefix]
|
11
|
+
suffix = clean(options[:suffix])
|
12
|
+
suffixes = options[:suffixes]
|
13
|
+
suffixes_items = suffixes ? suffixes.map { |s| clean(s) }.compact : nil
|
14
|
+
|
15
|
+
[prefix, clean(text), suffix || suffixes_items].compact.join(separator)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def clean(text)
|
21
|
+
if text
|
22
|
+
parts = text.to_s.split(QuickStore.config.key_separator).map do |key|
|
23
|
+
key.gsub(LEADING_NUMBERS, '').gsub(PART_JOINTS, '_').downcase
|
24
|
+
end
|
25
|
+
|
26
|
+
parts.join(QuickStore.config.key_separator)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
data/lib/belajar/task.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
module Belajar
|
2
|
+
module Terminal
|
3
|
+
|
4
|
+
# text should be of a width of 70 columns or less
|
5
|
+
def self.text(file_name)
|
6
|
+
texts_path = File.expand_path('../terminal/texts', __FILE__)
|
7
|
+
file = File.join(texts_path, "#{file_name.to_s}.txt")
|
8
|
+
(File.exist?(file) ? File.read(file).to_s : '')
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'thor'
|
2
|
+
|
3
|
+
module Belajar
|
4
|
+
module Terminal
|
5
|
+
|
6
|
+
require_relative 'courses'
|
7
|
+
require_relative 'solutions'
|
8
|
+
require_relative 'setup'
|
9
|
+
require_relative 'output'
|
10
|
+
|
11
|
+
class CLI < Thor
|
12
|
+
include Terminal::Output
|
13
|
+
|
14
|
+
desc 'courses [COMMAND]', 'Handle belajar courses'
|
15
|
+
subcommand 'courses', Terminal::Courses
|
16
|
+
|
17
|
+
desc 'solutions [COMMAND]', 'Handle your solutions'
|
18
|
+
subcommand 'solutions', Terminal::Solutions
|
19
|
+
|
20
|
+
desc 'setup [COMMAND]', 'Change belajar setup'
|
21
|
+
subcommand 'setup', Terminal::Setup
|
22
|
+
|
23
|
+
def self.start
|
24
|
+
Belajar.config.import!
|
25
|
+
super
|
26
|
+
end
|
27
|
+
|
28
|
+
desc 'about', 'About belajar'
|
29
|
+
def about
|
30
|
+
Welcome.about
|
31
|
+
end
|
32
|
+
|
33
|
+
desc 'welcome', 'Setup belajar the first time and learn some important commands.'
|
34
|
+
def welcome
|
35
|
+
Welcome.run
|
36
|
+
end
|
37
|
+
|
38
|
+
desc 'scaffold', 'Scaffold solution files for your courses.'
|
39
|
+
def scaffold
|
40
|
+
generator = Generator.new
|
41
|
+
generator.prepare
|
42
|
+
|
43
|
+
courses_path = Belajar.config.courses_path
|
44
|
+
solutions_path = Belajar.config.solutions_path
|
45
|
+
|
46
|
+
generator.scaffold(courses_path, solutions_path)
|
47
|
+
|
48
|
+
say_info "You will find your solution files in\n#{solutions_path}."
|
49
|
+
end
|
50
|
+
|
51
|
+
desc 'learn', 'Go to belajar to learn Ruby!'
|
52
|
+
def learn
|
53
|
+
courses = Loading::Courses.load(Belajar.config.courses_path)
|
54
|
+
courses.empty? ? Courses.new.list : Belajar.start
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,179 @@
|
|
1
|
+
module Belajar
|
2
|
+
module Terminal
|
3
|
+
|
4
|
+
require 'os'
|
5
|
+
require 'open-uri'
|
6
|
+
require 'zip'
|
7
|
+
require_relative 'output'
|
8
|
+
|
9
|
+
class Courses < Thor
|
10
|
+
include Terminal::Output
|
11
|
+
|
12
|
+
desc 'list', 'List your available belajar courses'
|
13
|
+
def list
|
14
|
+
courses = Loading::Courses.load(Belajar.config.courses_path)
|
15
|
+
say_info courses_list_text(courses)
|
16
|
+
end
|
17
|
+
|
18
|
+
method_option :github, type: :string, aliases: '-g', desc: 'Download Github repository'
|
19
|
+
desc 'download [URL] [OPTIONS]', 'Download a new belajar course from [URL]'
|
20
|
+
def download(url = nil, action = 'downloaded')
|
21
|
+
use_initial_course = url.nil? && options[:github].nil?
|
22
|
+
url = GithubClient.master_zip_url(Belajar.config.initial_course) if use_initial_course
|
23
|
+
url = GithubClient.master_zip_url(options[:github]) if options[:github]
|
24
|
+
|
25
|
+
url_given = (url =~ /\A#{URI::regexp(['http', 'https'])}\z/)
|
26
|
+
github = use_initial_course || options[:github] || url.match(/github\.com/)
|
27
|
+
|
28
|
+
raise Download::NoUrlError unless url_given
|
29
|
+
raise Download::NoZipFileUrlError unless File.basename(url) =~ /\.zip/
|
30
|
+
|
31
|
+
courses_path = Belajar.config.courses_path
|
32
|
+
FileUtils.makedirs(courses_path) unless Dir.exist?(courses_path)
|
33
|
+
|
34
|
+
file_name = File.join(courses_path, url.split('/').last)
|
35
|
+
|
36
|
+
File.open(file_name, 'w') { |file| file << open(url).read }
|
37
|
+
course = Course.unzip(file_name, github_repo: github)
|
38
|
+
|
39
|
+
if github
|
40
|
+
user_and_repo = url.match(/github.com\/(.*)\/archive\/master.zip/).captures.first
|
41
|
+
store_repo_data(options[:github] || user_and_repo)
|
42
|
+
end
|
43
|
+
|
44
|
+
QuickStore.store.set(course.key(:url), url)
|
45
|
+
QuickStore.store.set(course.key(:updated_at), Time.now.to_s)
|
46
|
+
scaffold_solutions
|
47
|
+
|
48
|
+
say_info "Successfully #{action} the course \"#{course.title}\"!"
|
49
|
+
rescue Download::NoUrlError => e
|
50
|
+
print_download_warning(url, "\"#{url}\" is not a valid URL!")
|
51
|
+
rescue Download::NoZipFileUrlError => e
|
52
|
+
print_download_warning(url, "\"#{url}\" is not a URL of a *.zip file!")
|
53
|
+
rescue Exception => e
|
54
|
+
print_download_warning(url, e.message)
|
55
|
+
ensure
|
56
|
+
FileUtils.rm(file_name) if File.exist?(file_name.to_s)
|
57
|
+
end
|
58
|
+
|
59
|
+
method_option :all, type: :boolean, aliases: '-a', desc: 'Update all courses'
|
60
|
+
desc 'update [COURSE_NAME] [OPTIONS]', 'Update Daigak courses.'
|
61
|
+
def update(course_name = nil)
|
62
|
+
if options[:all]
|
63
|
+
courses = Loading::Courses.load(Belajar.config.courses_path)
|
64
|
+
courses.each { |course| update_course(course) }
|
65
|
+
elsif course_name
|
66
|
+
path = File.join(Belajar.config.courses_path, course_name)
|
67
|
+
|
68
|
+
unless Dir.exist?(path)
|
69
|
+
print_course_not_available(course_name)
|
70
|
+
return
|
71
|
+
end
|
72
|
+
|
73
|
+
update_course(Course.new(course_name))
|
74
|
+
else
|
75
|
+
system 'belajar course help update'
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
method_option :all, type: :boolean, aliases: '-a', desc: 'Delete all courses'
|
80
|
+
desc 'delete [COURSE_NAME] [OPTIONS]', 'Delete Belajar courses.'
|
81
|
+
def delete(course_name = nil)
|
82
|
+
if options[:all]
|
83
|
+
get_confirm('Are you shure you want to delete all courses?') do
|
84
|
+
course_dirs = Dir[File.join(Belajar.config.courses_path, '*')]
|
85
|
+
|
86
|
+
course_dirs.each do |dir|
|
87
|
+
FileUtils.remove_dir(dir)
|
88
|
+
QuickStore.store.delete(Storeable.key(File.basename(dir), prefix: 'courses'))
|
89
|
+
end
|
90
|
+
|
91
|
+
say_info "All belajar courses were successfully deleted."
|
92
|
+
end
|
93
|
+
elsif course_name
|
94
|
+
path = File.join(Belajar.config.courses_path, course_name)
|
95
|
+
|
96
|
+
unless Dir.exist?(path)
|
97
|
+
print_course_not_available(course_name)
|
98
|
+
return
|
99
|
+
end
|
100
|
+
|
101
|
+
get_confirm("Are you shure you want to delete the course \"#{course_name}\"?") do
|
102
|
+
FileUtils.remove_dir(path)
|
103
|
+
QuickStore.store.delete(Storeable.key(course_name, prefix: 'courses'))
|
104
|
+
say_info "The course \"#{course_name}\" was successfully deleted."
|
105
|
+
end
|
106
|
+
else
|
107
|
+
system 'belajar courses help delete'
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
def courses_list_text(courses)
|
114
|
+
if courses.empty?
|
115
|
+
text = Terminal.text :courses_empty
|
116
|
+
else
|
117
|
+
text = [
|
118
|
+
"Available belajar courses:\n",
|
119
|
+
*courses.map { |course| "* #{File.basename(course.path)}\n" }
|
120
|
+
].join("\n")
|
121
|
+
end
|
122
|
+
|
123
|
+
"#{text}\n#{Terminal.text :hint_course_download}"
|
124
|
+
end
|
125
|
+
|
126
|
+
def store_repo_data(user_and_repo)
|
127
|
+
parts = (user_and_repo ||= Belajar.config.initial_course).split('/')
|
128
|
+
author = parts.first
|
129
|
+
course = parts.second
|
130
|
+
|
131
|
+
course = Course.new(course)
|
132
|
+
QuickStore.store.set(course.key(:author), author)
|
133
|
+
QuickStore.store.set(course.key(:github), user_and_repo)
|
134
|
+
end
|
135
|
+
|
136
|
+
def scaffold_solutions
|
137
|
+
generator = Generator.new
|
138
|
+
generator.prepare
|
139
|
+
generator.scaffold(Belajar.config.courses_path, Belajar.config.solutions_path)
|
140
|
+
end
|
141
|
+
|
142
|
+
def print_download_warning(url, text)
|
143
|
+
message = [
|
144
|
+
"Error while downloading course from URL \"#{url}\"",
|
145
|
+
"#{text}\n",
|
146
|
+
Terminal.text(:hint_course_download)
|
147
|
+
].join("\n")
|
148
|
+
|
149
|
+
say_warning message
|
150
|
+
end
|
151
|
+
|
152
|
+
def update_course(course)
|
153
|
+
url = QuickStore.store.get(course.key(:url))
|
154
|
+
github_repo = QuickStore.store.get(course.key(:github))
|
155
|
+
updated = GithubClient.updated?(github_repo)
|
156
|
+
|
157
|
+
if !github_repo || updated
|
158
|
+
download(url, 'updated') if url
|
159
|
+
else
|
160
|
+
say_info "Course \"#{course.title}\" is still up to date."
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def print_course_not_available(course_name)
|
165
|
+
text = [
|
166
|
+
"The course \"#{course_name}\" is not available in",
|
167
|
+
"\"#{Belajar.config.courses_path}\".\n",
|
168
|
+
]
|
169
|
+
|
170
|
+
say_warning text.join("\n")
|
171
|
+
|
172
|
+
unless Loading::Courses.load(Belajar.config.courses_path).empty?
|
173
|
+
Terminal::Courses.new.list
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
end
|
179
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'active_support/concern'
|
3
|
+
require 'colorize'
|
4
|
+
|
5
|
+
module Belajar
|
6
|
+
module Terminal
|
7
|
+
|
8
|
+
module Output
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
|
11
|
+
included do
|
12
|
+
private
|
13
|
+
|
14
|
+
def say(text)
|
15
|
+
output = text.split("\n").map {|line| "\t#{line}" }.join("\n")
|
16
|
+
$stdout.puts output
|
17
|
+
end
|
18
|
+
|
19
|
+
def empty_line
|
20
|
+
$stdout.puts ''
|
21
|
+
end
|
22
|
+
|
23
|
+
def get(string)
|
24
|
+
$stdout.print "\n\t#{string} "
|
25
|
+
$stdin.gets.strip
|
26
|
+
end
|
27
|
+
|
28
|
+
def say_info(text)
|
29
|
+
say_box(text, ' ℹ', :light_blue)
|
30
|
+
end
|
31
|
+
|
32
|
+
def say_warning(text)
|
33
|
+
say_box(text, '⚠ ', :light_red)
|
34
|
+
end
|
35
|
+
|
36
|
+
def say_box(text, symbol, color)
|
37
|
+
empty_line
|
38
|
+
say line.send(color)
|
39
|
+
empty_line
|
40
|
+
|
41
|
+
indented_text = text.split("\n").join("\n#{' ' * (symbol.length + 1)}")
|
42
|
+
say indented_text.prepend("#{symbol} ").send(color)
|
43
|
+
|
44
|
+
empty_line
|
45
|
+
say line.send(color)
|
46
|
+
empty_line
|
47
|
+
end
|
48
|
+
|
49
|
+
def line(symbol = '-')
|
50
|
+
symbol * 70
|
51
|
+
end
|
52
|
+
|
53
|
+
def get_command(command, description)
|
54
|
+
say description
|
55
|
+
|
56
|
+
loop do
|
57
|
+
cmd = get '>'
|
58
|
+
|
59
|
+
unless cmd == command
|
60
|
+
say "This was something else. Try \"#{command}\"."
|
61
|
+
next
|
62
|
+
end
|
63
|
+
|
64
|
+
system cmd
|
65
|
+
break
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def get_confirm(description)
|
70
|
+
say_warning description
|
71
|
+
confirm = get '(yes|no)'
|
72
|
+
yield if confirm == 'yes' && block_given?
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|