console 0.5 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +15 -0
- data/Gemfile +8 -0
- data/README.md +165 -0
- data/Rakefile +13 -0
- data/console.gemspec +24 -0
- data/lib/console.rb +65 -0
- data/lib/console/buffer.rb +40 -0
- data/lib/console/capture.rb +66 -0
- data/lib/console/error.rb +76 -0
- data/lib/console/filter.rb +119 -0
- data/lib/console/generic.rb +26 -0
- data/lib/console/logger.rb +31 -0
- data/lib/console/serialized/logger.rb +71 -0
- data/lib/console/shell.rb +65 -0
- data/lib/console/split.rb +42 -0
- data/lib/console/terminal.rb +21 -0
- data/lib/console/terminal/logger.rb +150 -0
- data/lib/console/terminal/text.rb +67 -0
- data/lib/console/terminal/xterm.rb +80 -0
- data/lib/console/version.rb +23 -0
- data/proposal.md +84 -0
- metadata +107 -58
- data/README +0 -51
- data/ext/console/console.c +0 -271
- data/ext/console/extconf.rb +0 -2
- data/lib/console/string.rb +0 -37
- data/test/console.rb +0 -126
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: d6d335e8b78f82a73be6a3cf6ab279594277fe4fd492d82db5da34695643219e
|
4
|
+
data.tar.gz: 22dd7d9b66f54a1bd38e563158d5ff58148e9f744d06a4d391ef8ae01676bb9d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9c4947d01b98cf603d14444be28815d4e7b6d3e3b7627be3b6f1d4d4d56f97702e7b61827c7c966533d696b73b9a0138b5a74fb2388fec645b5619b2e741ad2e
|
7
|
+
data.tar.gz: 8ecfe1ee0c30bc99b2662f6095defde0e99c8594a8a49b9c513f14914b80f84c9bc7ee7e13064097ac225a2f9a7a132ebae6b8f31c610cecd4c58e4cbfb77cf7
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,165 @@
|
|
1
|
+
# Console
|
2
|
+
|
3
|
+
Provides beautiful console logging for Ruby applications. Implements fast, buffered log output.
|
4
|
+
|
5
|
+
[![Build Status](https://travis-ci.com/socketry/console.svg)](http://travis-ci.com/socketry/console)
|
6
|
+
[![Coverage Status](https://coveralls.io/repos/socketry/console/badge.svg)](https://coveralls.io/r/socketry/console)
|
7
|
+
|
8
|
+
## Motivation
|
9
|
+
|
10
|
+
When Ruby decided to reverse the order of exception backtraces, I finally gave up using the built in logging and decided restore sanity to the output of my programs once and for all!
|
11
|
+
|
12
|
+
## Installation
|
13
|
+
|
14
|
+
Add this line to your application's Gemfile:
|
15
|
+
|
16
|
+
```ruby
|
17
|
+
gem 'console'
|
18
|
+
```
|
19
|
+
|
20
|
+
And then execute:
|
21
|
+
|
22
|
+
$ bundle
|
23
|
+
|
24
|
+
## Usage
|
25
|
+
|
26
|
+
As your code executes, it generates interesting events which you want to know about. The general approach is to use an `Console::Logger` which outputs text to the terminal. The text is generated by inspecting the console that occurred.
|
27
|
+
|
28
|
+
Capturing structured information allows it to be used in different ways. These events can be sent to a logger or some other system (e.g. web browser, syslog) and analysed in more detail.
|
29
|
+
|
30
|
+
### Default Logger
|
31
|
+
|
32
|
+
Generally speaking, use `Console.logger` which is suitable for logging to the user's terminal.
|
33
|
+
|
34
|
+
### Module Integration
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
require 'console'
|
38
|
+
|
39
|
+
# Set the log level:
|
40
|
+
Console.logger.debug!
|
41
|
+
|
42
|
+
module MyModule
|
43
|
+
extend Console
|
44
|
+
|
45
|
+
def self.test_logger
|
46
|
+
logger.debug "GOTO LINE 1."
|
47
|
+
logger.info "5 things your doctor won't tell you!"
|
48
|
+
logger.warn "Something didn't work as expected!"
|
49
|
+
logger.error "The matrix has two cats!"
|
50
|
+
end
|
51
|
+
|
52
|
+
test_logger
|
53
|
+
end
|
54
|
+
```
|
55
|
+
|
56
|
+
### Class Integration
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
require 'console'
|
60
|
+
|
61
|
+
# Set the log level:
|
62
|
+
Console.logger.debug!
|
63
|
+
|
64
|
+
class MyObject
|
65
|
+
include Console
|
66
|
+
|
67
|
+
def test_logger
|
68
|
+
logger.debug "GOTO LINE 1."
|
69
|
+
logger.info "5 things your doctor won't tell you!"
|
70
|
+
logger.warn "Something didn't work as expected!"
|
71
|
+
logger.error "The matrix has two cats!"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
MyObject.new.test_logger
|
76
|
+
```
|
77
|
+
|
78
|
+
### Console Formatting
|
79
|
+
|
80
|
+
Console classes are used to wrap data which can generate structured log messages:
|
81
|
+
|
82
|
+
```ruby
|
83
|
+
require 'console'
|
84
|
+
|
85
|
+
class MyConsole < Console::Generic
|
86
|
+
def format(output, terminal, verbose)
|
87
|
+
output.puts "My console text!"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
Console.logger.info("My Console", MyConsole.new)
|
92
|
+
```
|
93
|
+
|
94
|
+
#### Error Events
|
95
|
+
|
96
|
+
`Console::Error` represents an error and will log the message and backtrace recursively.
|
97
|
+
|
98
|
+
#### Shell Events
|
99
|
+
|
100
|
+
`Console::Shell` represents the execution of a shell command, and will log the environment, arguments and options used to execute it.
|
101
|
+
|
102
|
+
### Multiple Loggers
|
103
|
+
|
104
|
+
### Custom Log Levels
|
105
|
+
|
106
|
+
`Console::Filter` implements support for multiple log levels.
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
require 'console'
|
110
|
+
|
111
|
+
MyLogger = Console::Filter[noise: 0, stuff: 1, broken: 2]
|
112
|
+
|
113
|
+
logger = MyLogger.new(Console.logger, name: "Java")
|
114
|
+
logger.verbose! # log severity/name/pid etc.
|
115
|
+
|
116
|
+
logger.broken("It's so janky.")
|
117
|
+
```
|
118
|
+
|
119
|
+
### Multiple Outputs
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
require 'console/terminal'
|
123
|
+
require 'console/serialized/logger'
|
124
|
+
require 'console/logger'
|
125
|
+
require 'console/split'
|
126
|
+
|
127
|
+
terminal = Console::Terminal::Logger.new
|
128
|
+
file = Console::Serialized::Logger.new(File.open("/tmp/log.json", "w"))
|
129
|
+
|
130
|
+
logger = Console::Logger.new(Console::Split[terminal, file])
|
131
|
+
|
132
|
+
logger.info "I can go everywhere!"
|
133
|
+
```
|
134
|
+
|
135
|
+
## Contributing
|
136
|
+
|
137
|
+
1. Fork it
|
138
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
139
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
140
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
141
|
+
5. Create new Pull Request
|
142
|
+
|
143
|
+
## License
|
144
|
+
|
145
|
+
Released under the MIT license.
|
146
|
+
|
147
|
+
Copyright, 2019, by [Samuel Williams](https://www.codeotaku.com).
|
148
|
+
|
149
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
150
|
+
of this software and associated documentation files (the "Software"), to deal
|
151
|
+
in the Software without restriction, including without limitation the rights
|
152
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
153
|
+
copies of the Software, and to permit persons to whom the Software is
|
154
|
+
furnished to do so, subject to the following conditions:
|
155
|
+
|
156
|
+
The above copyright notice and this permission notice shall be included in
|
157
|
+
all copies or substantial portions of the Software.
|
158
|
+
|
159
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
160
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
161
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
162
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
163
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
164
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
165
|
+
THE SOFTWARE.
|
data/Rakefile
ADDED
data/console.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
|
2
|
+
require_relative "lib/console/version"
|
3
|
+
|
4
|
+
Gem::Specification.new do |spec|
|
5
|
+
spec.name = "console"
|
6
|
+
spec.version = Console::VERSION
|
7
|
+
spec.authors = ["Samuel Williams"]
|
8
|
+
spec.email = ["samuel.williams@oriontransfer.co.nz"]
|
9
|
+
|
10
|
+
spec.summary = "Beautiful logging for Ruby."
|
11
|
+
spec.homepage = "https://github.com/socketry/console"
|
12
|
+
|
13
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
14
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
15
|
+
end
|
16
|
+
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_development_dependency "covered"
|
21
|
+
spec.add_development_dependency "bundler"
|
22
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
23
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
24
|
+
end
|
data/lib/console.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
# Copyright, 2019, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require_relative 'console/logger'
|
22
|
+
require_relative 'console/terminal/logger'
|
23
|
+
|
24
|
+
module Console
|
25
|
+
class << self
|
26
|
+
attr_accessor :logger
|
27
|
+
|
28
|
+
LEVELS = {
|
29
|
+
'debug' => Logger::DEBUG,
|
30
|
+
'info' => Logger::INFO,
|
31
|
+
}
|
32
|
+
|
33
|
+
# Set the default log level based on `$DEBUG` and `$VERBOSE`.
|
34
|
+
# You can also specify CONSOLE_LOG_LEVEL=debug or CONSOLE_LOG_LEVEL=info in environment.
|
35
|
+
def default_log_level(env = ENV)
|
36
|
+
if level = env['CONSOLE_LOG_LEVEL']
|
37
|
+
LEVELS[level] || Logger.warn
|
38
|
+
elsif $DEBUG
|
39
|
+
Logger::DEBUG
|
40
|
+
elsif $VERBOSE
|
41
|
+
Logger::INFO
|
42
|
+
else
|
43
|
+
Logger::WARN
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Create the logger instance:
|
49
|
+
@logger = Logger.new(
|
50
|
+
Terminal::Logger.new($stderr),
|
51
|
+
level: self.default_log_level,
|
52
|
+
)
|
53
|
+
|
54
|
+
def logger= logger
|
55
|
+
@logger = logger
|
56
|
+
end
|
57
|
+
|
58
|
+
def logger
|
59
|
+
@logger || Console.logger
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.extended(klass)
|
63
|
+
klass.instance_variable_set(:@logger, nil)
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require 'stringio'
|
22
|
+
|
23
|
+
module Console
|
24
|
+
class Buffer < StringIO
|
25
|
+
def initialize(prefix = nil)
|
26
|
+
@prefix = prefix
|
27
|
+
|
28
|
+
super()
|
29
|
+
end
|
30
|
+
|
31
|
+
def puts(*args, prefix: @prefix)
|
32
|
+
args.each do |arg|
|
33
|
+
self.write(prefix) if prefix
|
34
|
+
super(arg)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
alias << puts
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require_relative 'filter'
|
22
|
+
|
23
|
+
module Console
|
24
|
+
class Capture
|
25
|
+
def initialize
|
26
|
+
@events = []
|
27
|
+
end
|
28
|
+
|
29
|
+
attr :events
|
30
|
+
|
31
|
+
def last
|
32
|
+
@events.last
|
33
|
+
end
|
34
|
+
|
35
|
+
def verbose!(value = true)
|
36
|
+
end
|
37
|
+
|
38
|
+
def call(subject = nil, *arguments, severity: UNKNOWN, **options, &block)
|
39
|
+
message = {
|
40
|
+
time: Time.now.iso8601,
|
41
|
+
severity: severity,
|
42
|
+
**options,
|
43
|
+
}
|
44
|
+
|
45
|
+
if subject
|
46
|
+
message[:subject] = subject
|
47
|
+
end
|
48
|
+
|
49
|
+
if arguments.any?
|
50
|
+
message[:arguments] = arguments
|
51
|
+
end
|
52
|
+
|
53
|
+
if block_given?
|
54
|
+
if block.arity.zero?
|
55
|
+
message[:message] = yield
|
56
|
+
else
|
57
|
+
buffer = StringIO.new
|
58
|
+
yield buffer
|
59
|
+
message[:message] = buffer.string
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
@events << message
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# Copyright, 2019, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require_relative 'generic'
|
22
|
+
|
23
|
+
module Console
|
24
|
+
class Error < Generic
|
25
|
+
def self.current_working_directory
|
26
|
+
Dir.getwd
|
27
|
+
rescue # e.g. Errno::EMFILE
|
28
|
+
nil
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.for(exception)
|
32
|
+
self.new(exception, self.current_working_directory)
|
33
|
+
end
|
34
|
+
|
35
|
+
def initialize(exception, root = nil)
|
36
|
+
@exception = exception
|
37
|
+
@root = root
|
38
|
+
end
|
39
|
+
|
40
|
+
attr :exception
|
41
|
+
attr :root
|
42
|
+
|
43
|
+
def self.register(terminal)
|
44
|
+
terminal[:exception_title] ||= terminal.style(:red, nil, :bold)
|
45
|
+
terminal[:exception_detail] ||= terminal.style(:yellow)
|
46
|
+
terminal[:exception_backtrace] ||= terminal.style(:red)
|
47
|
+
end
|
48
|
+
|
49
|
+
def format(output, terminal, verbose)
|
50
|
+
format_exception(@exception, nil, output, terminal, verbose)
|
51
|
+
end
|
52
|
+
|
53
|
+
def format_exception(exception, prefix = nil, output, terminal, verbose)
|
54
|
+
lines = exception.message.lines.map(&:chomp)
|
55
|
+
|
56
|
+
output.puts " #{prefix}#{terminal[:exception_title]}#{exception.class}#{terminal.reset}: #{lines.shift}"
|
57
|
+
|
58
|
+
lines.each do |line|
|
59
|
+
output.puts " #{terminal[:exception_detail]}#{line}#{terminal.reset}"
|
60
|
+
end
|
61
|
+
|
62
|
+
exception.backtrace&.each_with_index do |line, index|
|
63
|
+
path, offset, message = line.split(":")
|
64
|
+
|
65
|
+
# Make the path a bit more readable
|
66
|
+
path.gsub!(/^#{@root}\//, "./") if @root
|
67
|
+
|
68
|
+
output.puts " #{index == 0 ? "→" : " "} #{terminal[:exception_backtrace]}#{path}:#{offset}#{terminal.reset} #{message}"
|
69
|
+
end
|
70
|
+
|
71
|
+
if exception.cause and verbose
|
72
|
+
format_exception(exception.cause, "Caused by ", output, terminal)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|