ansi 1.0.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.
- data/HISTORY +12 -0
- data/LICENSE +789 -0
- data/MANIFEST +36 -0
- data/README +72 -0
- data/demo/logger.rd +31 -0
- data/demo/progressbar.rd +63 -0
- data/lib/ansi.rb +11 -0
- data/lib/ansi/code.rb +229 -0
- data/lib/ansi/logger.rb +209 -0
- data/lib/ansi/progressbar.rb +268 -0
- data/lib/ansi/string.rb +249 -0
- data/lib/ansi/terminal.rb +43 -0
- data/lib/ansi/terminal/curses.rb +27 -0
- data/lib/ansi/terminal/stty.rb +55 -0
- data/lib/ansi/terminal/termios.rb +63 -0
- data/lib/ansi/terminal/win32.rb +107 -0
- data/meta/abstract +3 -0
- data/meta/authors +3 -0
- data/meta/created +1 -0
- data/meta/homepage +1 -0
- data/meta/license +1 -0
- data/meta/package +1 -0
- data/meta/project +1 -0
- data/meta/released +1 -0
- data/meta/repository +1 -0
- data/meta/summary +1 -0
- data/meta/title +1 -0
- data/meta/version +1 -0
- data/test/test_ansicode.rb +20 -0
- data/test/test_progressbar.rb +18 -0
- metadata +93 -0
data/lib/ansi/string.rb
ADDED
@@ -0,0 +1,249 @@
|
|
1
|
+
require 'ansi/code'
|
2
|
+
#require 'ansi/layout/split'
|
3
|
+
#require 'clio/facets/string'
|
4
|
+
|
5
|
+
# Create a new Ansi::String object.
|
6
|
+
def ANSI.string(str)
|
7
|
+
ANSI::String.new(str)
|
8
|
+
end
|
9
|
+
|
10
|
+
# Clio Strings stores a regular string (@text) and
|
11
|
+
# a Hash mapping character index to ansicodes (@marks).
|
12
|
+
# For example is we have the string:
|
13
|
+
#
|
14
|
+
# "Big Apple"
|
15
|
+
#
|
16
|
+
# And applied the color red to it, the marks hash would be:
|
17
|
+
#
|
18
|
+
# { 0=>[:red] , 9=>[:clear] }
|
19
|
+
#
|
20
|
+
# TODO: In the future we may be able to subclass String,
|
21
|
+
# instead of delegating via @text, but not until it is more compatible.
|
22
|
+
#
|
23
|
+
class ANSI::String
|
24
|
+
|
25
|
+
CLR = ANSI::Code.clear
|
26
|
+
|
27
|
+
attr :text
|
28
|
+
attr :marks
|
29
|
+
|
30
|
+
# New Ansi::String
|
31
|
+
def initialize(text=nil, marks=nil)
|
32
|
+
@text = (text || '').to_s
|
33
|
+
@marks = marks || []
|
34
|
+
yield(self) if block_given?
|
35
|
+
end
|
36
|
+
|
37
|
+
# Convert Ansi::String object to normal String.
|
38
|
+
# This converts the intental markup codes to ANSI codes.
|
39
|
+
def to_s
|
40
|
+
s = text.dup
|
41
|
+
m = marks.sort do |(a,b)|
|
42
|
+
v = b[0] <=> a[0]
|
43
|
+
if v == 0
|
44
|
+
(b[1] == :clear or b[1] == :reset) ? -1 : 1
|
45
|
+
else
|
46
|
+
v
|
47
|
+
end
|
48
|
+
end
|
49
|
+
m.each do |(index, code)|
|
50
|
+
s.insert(index, ANSICode.__send__(code))
|
51
|
+
end
|
52
|
+
#s << CLR unless s =~ /#{Regexp.escape(CLR)}$/ # always end with a clear
|
53
|
+
s
|
54
|
+
end
|
55
|
+
|
56
|
+
# Ansi::String is a type of String.
|
57
|
+
alias_method :to_str, :to_s
|
58
|
+
|
59
|
+
# The size of the base text.
|
60
|
+
def size ; text.size ; end
|
61
|
+
|
62
|
+
# Upcase the string.
|
63
|
+
def upcase ; self.class.new(text.upcase, marks) ; end
|
64
|
+
def upcase! ; text.upcase! ; end
|
65
|
+
|
66
|
+
# Downcase the string.
|
67
|
+
def downcase ; self.class.new(text.upcase, marks) ; end
|
68
|
+
def downcase! ; text.upcase! ; end
|
69
|
+
|
70
|
+
# Add one String to another, or to a regular String.
|
71
|
+
def +(other)
|
72
|
+
case other
|
73
|
+
when String
|
74
|
+
ntext = text + other.text
|
75
|
+
nmarks = marks.dup
|
76
|
+
omarks = shift_marks(0, text.size, other.marks)
|
77
|
+
omarks.each{ |(i, c)| nmarks << [i,c] }
|
78
|
+
else
|
79
|
+
ntext = text + other.to_s
|
80
|
+
nmarks = marks.dup
|
81
|
+
end
|
82
|
+
self.class.new(ntext, nmarks)
|
83
|
+
end
|
84
|
+
|
85
|
+
#
|
86
|
+
#def |(other)
|
87
|
+
# Split.new(self, other)
|
88
|
+
#end
|
89
|
+
|
90
|
+
#
|
91
|
+
#def lr(other, options={})
|
92
|
+
# Split.new(self, other, options)
|
93
|
+
#end
|
94
|
+
|
95
|
+
# slice
|
96
|
+
def slice(*args)
|
97
|
+
if args.size == 2
|
98
|
+
index, len = *args
|
99
|
+
endex = index+len
|
100
|
+
new_text = text[index, len]
|
101
|
+
new_marks = []
|
102
|
+
marks.each do |(i, v)|
|
103
|
+
new_marks << [i, v] if i >= index && i < endex
|
104
|
+
end
|
105
|
+
self.class.new(new_text, new_marks)
|
106
|
+
elsif args.size == 1
|
107
|
+
rng = args.first
|
108
|
+
case rng
|
109
|
+
when Range
|
110
|
+
index, endex = rng.begin, rng.end
|
111
|
+
new_text = text[rng]
|
112
|
+
new_marks = []
|
113
|
+
marks.each do |(i, v)|
|
114
|
+
new_marks << [i, v] if i >= index && i < endex
|
115
|
+
end
|
116
|
+
self.class.new(new_text, new_marks)
|
117
|
+
else
|
118
|
+
nm = marks.select do |(i,c)|
|
119
|
+
marks[0] == rng or ( marks[0] == rng + 1 && [:clear, :reset].include?(marks[1]) )
|
120
|
+
end
|
121
|
+
self.class.new(text[rng,1], nm)
|
122
|
+
end
|
123
|
+
else
|
124
|
+
raise ArgumentError
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
#
|
129
|
+
alias_method :[], :slice
|
130
|
+
|
131
|
+
# This is more limited than the normal String method.
|
132
|
+
# It does not yet support a block, and +replacement+
|
133
|
+
# won't substitue for \1, \2, etc.
|
134
|
+
#
|
135
|
+
# TODO: block support.
|
136
|
+
def sub!(pattern, replacement=nil, &block)
|
137
|
+
mark_changes = []
|
138
|
+
text = @text.sub(pattern) do |s|
|
139
|
+
index = $~.begin(0)
|
140
|
+
replacement = block.call(s) if block_given?
|
141
|
+
delta = (replacement.size - s.size)
|
142
|
+
mark_changes << [index, delta]
|
143
|
+
replacement
|
144
|
+
end
|
145
|
+
marks = @marks
|
146
|
+
mark_changes.each do |index, delta|
|
147
|
+
marks = shift_marks(index, delta, marks)
|
148
|
+
end
|
149
|
+
@text = text
|
150
|
+
@marks = marks
|
151
|
+
self
|
152
|
+
end
|
153
|
+
|
154
|
+
# See #sub!.
|
155
|
+
def sub(pattern,replacement=nil, &block)
|
156
|
+
dup.sub!(pattern, replacement, &block)
|
157
|
+
end
|
158
|
+
|
159
|
+
#
|
160
|
+
def gsub!(pattern, replacement=nil, &block)
|
161
|
+
mark_changes = []
|
162
|
+
mark_additions = []
|
163
|
+
text = @text.gsub(pattern) do |s|
|
164
|
+
index = $~.begin(0)
|
165
|
+
replacement = block.call(self.class.new(s)) if block_given?
|
166
|
+
if self.class===replacement
|
167
|
+
adj_marks = replacement.marks.map{ |(i,c)| [i+index,c] }
|
168
|
+
mark_additions.concat(adj_marks)
|
169
|
+
replacement = replacement.text
|
170
|
+
end
|
171
|
+
delta = (replacement.size - s.size)
|
172
|
+
mark_changes << [index, delta]
|
173
|
+
replacement
|
174
|
+
end
|
175
|
+
marks = @marks
|
176
|
+
mark_changes.each do |(index, delta)|
|
177
|
+
marks = shift_marks(index, delta, marks)
|
178
|
+
end
|
179
|
+
marks.concat(mark_additions)
|
180
|
+
@text = text
|
181
|
+
@marks = marks
|
182
|
+
self
|
183
|
+
end
|
184
|
+
|
185
|
+
# See #gsub!.
|
186
|
+
def gsub(pattern, replacement=nil, &block)
|
187
|
+
dup.gsub!(pattern, replacement, &block)
|
188
|
+
end
|
189
|
+
|
190
|
+
#
|
191
|
+
def ansi(code)
|
192
|
+
m = marks.dup
|
193
|
+
m.unshift([0, code])
|
194
|
+
m.push([size, :clear])
|
195
|
+
self.class.new(text, m)
|
196
|
+
end
|
197
|
+
alias_method :color, :ansi
|
198
|
+
|
199
|
+
#
|
200
|
+
def ansi!(code)
|
201
|
+
marks.unshift([0, ansicolor])
|
202
|
+
marks.push([size, :clear])
|
203
|
+
end
|
204
|
+
alias_method :color!, :ansi!
|
205
|
+
|
206
|
+
def red ; color(:red) ; end
|
207
|
+
def green ; color(:green) ; end
|
208
|
+
def blue ; color(:blue) ; end
|
209
|
+
def black ; color(:black) ; end
|
210
|
+
def magenta ; color(:magenta) ; end
|
211
|
+
def yellow ; color(:yellow) ; end
|
212
|
+
def cyan ; color(:cyan) ; end
|
213
|
+
|
214
|
+
def bold ; ansi(:bold) ; end
|
215
|
+
def underline ; ansi(:underline) ; end
|
216
|
+
|
217
|
+
def red! ; color!(:red) ; end
|
218
|
+
def green! ; color!(:green) ; end
|
219
|
+
def blue! ; color!(:blue) ; end
|
220
|
+
def black! ; color!(:black) ; end
|
221
|
+
def magenta! ; color!(:magenta) ; end
|
222
|
+
def yellow! ; color!(:yellow) ; end
|
223
|
+
def cyan! ; color!(:cyan) ; end
|
224
|
+
|
225
|
+
def bold! ; ansi!(:bold) ; end
|
226
|
+
def underline! ; ansi!(:underline) ; end
|
227
|
+
|
228
|
+
private
|
229
|
+
|
230
|
+
#
|
231
|
+
def shift_marks(index, delta, marks=nil)
|
232
|
+
new_marks = []
|
233
|
+
(marks || @marks).each do |(i, c)|
|
234
|
+
case i <=> index
|
235
|
+
when -1
|
236
|
+
new_marks << [i, c]
|
237
|
+
when 0, 1
|
238
|
+
new_marks << [i+delta, c]
|
239
|
+
end
|
240
|
+
end
|
241
|
+
new_marks
|
242
|
+
end
|
243
|
+
|
244
|
+
#
|
245
|
+
def shift_marks!(index, delta)
|
246
|
+
@marks.replace(shift_marks(index, delta))
|
247
|
+
end
|
248
|
+
|
249
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
#
|
2
|
+
# This library is based of HighLine's SystemExtensions
|
3
|
+
# by James Edward Gray II.
|
4
|
+
#
|
5
|
+
# Copyright 2006 Gray Productions. All rights reserved.
|
6
|
+
#
|
7
|
+
# This is Free Software. See LICENSE and COPYING for details.
|
8
|
+
|
9
|
+
module ANSI
|
10
|
+
|
11
|
+
#
|
12
|
+
module Terminal
|
13
|
+
|
14
|
+
module_function
|
15
|
+
|
16
|
+
modes = %w{win32 termios curses stty}
|
17
|
+
|
18
|
+
#
|
19
|
+
# This section builds character reading and terminal size functions
|
20
|
+
# to suit the proper platform we're running on. Be warned: Here be
|
21
|
+
# dragons!
|
22
|
+
#
|
23
|
+
begin
|
24
|
+
require 'ansi/terminal/' + (mode = modes.pop)
|
25
|
+
CHARACTER_MODE = mode
|
26
|
+
rescue LoadError
|
27
|
+
retry
|
28
|
+
end
|
29
|
+
|
30
|
+
# Get the width of the terminal window.
|
31
|
+
def terminal_width
|
32
|
+
terminal_size[0]
|
33
|
+
end
|
34
|
+
|
35
|
+
# Get the height of the terminal window.
|
36
|
+
def terminal_height
|
37
|
+
terminal_size[1]
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module ANSI
|
2
|
+
|
3
|
+
module Terminal
|
4
|
+
require 'curses'
|
5
|
+
|
6
|
+
module_function
|
7
|
+
|
8
|
+
#CHARACTER_MODE = "curses" # For Debugging purposes only.
|
9
|
+
|
10
|
+
#
|
11
|
+
# Curses savvy getc().
|
12
|
+
#
|
13
|
+
#
|
14
|
+
def get_character(input = STDIN)
|
15
|
+
Curses.getch()
|
16
|
+
end
|
17
|
+
|
18
|
+
def terminal_size
|
19
|
+
Curses.init_screen
|
20
|
+
w, r = Curses.cols, Curses.rows
|
21
|
+
Curses.close_screen
|
22
|
+
return r, w
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module ANSI
|
2
|
+
|
3
|
+
module Terminal
|
4
|
+
|
5
|
+
module_function
|
6
|
+
|
7
|
+
#CHARACTER_MODE = "stty" # For Debugging purposes only.
|
8
|
+
|
9
|
+
#
|
10
|
+
# Unix savvy getc(). (Second choice.)
|
11
|
+
#
|
12
|
+
# *WARNING*: This method requires the external "stty" program!
|
13
|
+
#
|
14
|
+
def get_character( input = STDIN )
|
15
|
+
raw_no_echo_mode
|
16
|
+
|
17
|
+
begin
|
18
|
+
input.getc
|
19
|
+
ensure
|
20
|
+
restore_mode
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
#
|
25
|
+
# Switched the input mode to raw and disables echo.
|
26
|
+
#
|
27
|
+
# *WARNING*: This method requires the external "stty" program!
|
28
|
+
#
|
29
|
+
def raw_no_echo_mode
|
30
|
+
@state = `stty -g`
|
31
|
+
system "stty raw -echo cbreak isig"
|
32
|
+
end
|
33
|
+
|
34
|
+
#
|
35
|
+
# Restores a previously saved input mode.
|
36
|
+
#
|
37
|
+
# *WARNING*: This method requires the external "stty" program!
|
38
|
+
#
|
39
|
+
def restore_mode
|
40
|
+
system "stty #{@state}"
|
41
|
+
end
|
42
|
+
|
43
|
+
# A Unix savvy method to fetch the console columns, and rows.
|
44
|
+
def terminal_size
|
45
|
+
if /solaris/ =~ RUBY_PLATFORM and
|
46
|
+
`stty` =~ /\brows = (\d+).*\bcolumns = (\d+)/
|
47
|
+
[$2, $1].map { |c| x.to_i }
|
48
|
+
else
|
49
|
+
`stty size`.split.map { |x| x.to_i }.reverse
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module ANSI
|
2
|
+
|
3
|
+
module Terminal
|
4
|
+
require "termios" # Unix, first choice.
|
5
|
+
|
6
|
+
module_function
|
7
|
+
|
8
|
+
#CHARACTER_MODE = "termios" # For Debugging purposes only.
|
9
|
+
|
10
|
+
#
|
11
|
+
# Unix savvy getc(). (First choice.)
|
12
|
+
#
|
13
|
+
# *WARNING*: This method requires the "termios" library!
|
14
|
+
#
|
15
|
+
def get_character( input = STDIN )
|
16
|
+
old_settings = Termios.getattr(input)
|
17
|
+
|
18
|
+
new_settings = old_settings.dup
|
19
|
+
new_settings.c_lflag &= ~(Termios::ECHO | Termios::ICANON)
|
20
|
+
new_settings.c_cc[Termios::VMIN] = 1
|
21
|
+
|
22
|
+
begin
|
23
|
+
Termios.setattr(input, Termios::TCSANOW, new_settings)
|
24
|
+
input.getc
|
25
|
+
ensure
|
26
|
+
Termios.setattr(input, Termios::TCSANOW, old_settings)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# A Unix savvy method to fetch the console columns, and rows.
|
31
|
+
def terminal_size
|
32
|
+
if /solaris/ =~ RUBY_PLATFORM and
|
33
|
+
`stty` =~ /\brows = (\d+).*\bcolumns = (\d+)/
|
34
|
+
[$2, $1].map { |c| x.to_i }
|
35
|
+
else
|
36
|
+
`stty size`.split.map { |x| x.to_i }.reverse
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Console screen width (taken from progress bar)
|
41
|
+
#
|
42
|
+
# NOTE: Don't know how portable #screen_width is.
|
43
|
+
# TODO: How to fit in to system?
|
44
|
+
#
|
45
|
+
def screen_width(out=STDERR)
|
46
|
+
default_width = ENV['COLUMNS'] || 76
|
47
|
+
begin
|
48
|
+
tiocgwinsz = 0x5413
|
49
|
+
data = [0, 0, 0, 0].pack("SSSS")
|
50
|
+
if out.ioctl(tiocgwinsz, data) >= 0 then
|
51
|
+
rows, cols, xpixels, ypixels = data.unpack("SSSS")
|
52
|
+
if cols >= 0 then cols else default_width end
|
53
|
+
else
|
54
|
+
default_width
|
55
|
+
end
|
56
|
+
rescue Exception
|
57
|
+
default_width
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
module ANSI
|
2
|
+
|
3
|
+
module Terminal
|
4
|
+
# Cygwin will look like Windows, but we want to treat it like a Posix OS:
|
5
|
+
raise LoadError, "Cygwin is a Posix OS." if RUBY_PLATFORM =~ /\bcygwin\b/i
|
6
|
+
|
7
|
+
require "Win32API" # See if we're on Windows.
|
8
|
+
|
9
|
+
module_function
|
10
|
+
|
11
|
+
#CHARACTER_MODE = "Win32API" # For Debugging purposes only.
|
12
|
+
|
13
|
+
#
|
14
|
+
# Windows savvy getc().
|
15
|
+
#
|
16
|
+
#
|
17
|
+
def get_character( input = STDIN )
|
18
|
+
@stdin_handle ||= GetStdHandle(STD_INPUT_HANDLE)
|
19
|
+
|
20
|
+
begin
|
21
|
+
SetConsoleEcho(@stdin_handle, false)
|
22
|
+
input.getc
|
23
|
+
ensure
|
24
|
+
SetConsoleEcho(@stdin_handle, true)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# A Windows savvy method to fetch the console columns, and rows.
|
29
|
+
def terminal_size
|
30
|
+
stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE)
|
31
|
+
|
32
|
+
bufx, bufy, curx, cury, wattr, left, top, right, bottom, maxx, maxy =
|
33
|
+
GetConsoleScreenBufferInfo(stdout_handle)
|
34
|
+
return right - left + 1, bottom - top + 1
|
35
|
+
end
|
36
|
+
|
37
|
+
# windows savvy console echo toggler
|
38
|
+
def SetConsoleEcho( console_handle, on )
|
39
|
+
mode = GetConsoleMode(console_handle)
|
40
|
+
|
41
|
+
# toggle the console echo bit
|
42
|
+
if on
|
43
|
+
mode |= ENABLE_ECHO_INPUT
|
44
|
+
else
|
45
|
+
mode &= ~ENABLE_ECHO_INPUT
|
46
|
+
end
|
47
|
+
|
48
|
+
ok = SetConsoleMode(console_handle, mode)
|
49
|
+
end
|
50
|
+
|
51
|
+
# win32 console APIs
|
52
|
+
|
53
|
+
STD_INPUT_HANDLE = -10
|
54
|
+
STD_OUTPUT_HANDLE = -11
|
55
|
+
STD_ERROR_HANDLE = -12
|
56
|
+
|
57
|
+
ENABLE_PROCESSED_INPUT = 0x0001
|
58
|
+
ENABLE_LINE_INPUT = 0x0002
|
59
|
+
ENABLE_WRAP_AT_EOL_OUTPUT = 0x0002
|
60
|
+
ENABLE_ECHO_INPUT = 0x0004
|
61
|
+
ENABLE_WINDOW_INPUT = 0x0008
|
62
|
+
ENABLE_MOUSE_INPUT = 0x0010
|
63
|
+
ENABLE_INSERT_MODE = 0x0020
|
64
|
+
ENABLE_QUICK_EDIT_MODE = 0x0040
|
65
|
+
|
66
|
+
@@apiGetStdHandle = nil
|
67
|
+
@@apiGetConsoleMode = nil
|
68
|
+
@@apiSetConsoleMode = nil
|
69
|
+
@@apiGetConsoleScreenBufferInfo = nil
|
70
|
+
|
71
|
+
def GetStdHandle( handle_type )
|
72
|
+
@@apiGetStdHandle ||= Win32API.new( "kernel32", "GetStdHandle",
|
73
|
+
['L'], 'L' )
|
74
|
+
|
75
|
+
@@apiGetStdHandle.call( handle_type )
|
76
|
+
end
|
77
|
+
|
78
|
+
def GetConsoleMode( console_handle )
|
79
|
+
@@apiGetConsoleMode ||= Win32API.new( "kernel32", "GetConsoleMode",
|
80
|
+
['L', 'P'], 'I' )
|
81
|
+
|
82
|
+
mode = ' ' * 4
|
83
|
+
@@apiGetConsoleMode.call(console_handle, mode)
|
84
|
+
mode.unpack('L')[0]
|
85
|
+
end
|
86
|
+
|
87
|
+
def SetConsoleMode( console_handle, mode )
|
88
|
+
@@apiSetConsoleMode ||= Win32API.new( "kernel32", "SetConsoleMode",
|
89
|
+
['L', 'L'], 'I' )
|
90
|
+
|
91
|
+
@@apiSetConsoleMode.call(console_handle, mode) != 0
|
92
|
+
end
|
93
|
+
|
94
|
+
def GetConsoleScreenBufferInfo( console_handle )
|
95
|
+
@@apiGetConsoleScreenBufferInfo ||=
|
96
|
+
Win32API.new( "kernel32", "GetConsoleScreenBufferInfo",
|
97
|
+
['L', 'P'], 'L' )
|
98
|
+
|
99
|
+
format = 'SSSSSssssSS'
|
100
|
+
buf = ([0] * format.size).pack(format)
|
101
|
+
@@apiGetConsoleScreenBufferInfo.call(console_handle, buf)
|
102
|
+
buf.unpack(format)
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|