jnv-iruby 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.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +70 -0
- data/Rakefile +1 -0
- data/Ruby Notebook.ipynb +117 -0
- data/bin/iruby_kernel +92 -0
- data/bin/iruby_profile +37 -0
- data/iruby.gemspec +31 -0
- data/lib/iruby.rb +4 -0
- data/lib/iruby/display_hook.rb +33 -0
- data/lib/iruby/kernel.rb +220 -0
- data/lib/iruby/kernel_completer.rb +30 -0
- data/lib/iruby/message.rb +44 -0
- data/lib/iruby/out_stream.rb +95 -0
- data/lib/iruby/output/html.rb +168 -0
- data/lib/iruby/profile.rb +71 -0
- data/lib/iruby/session.rb +329 -0
- data/lib/iruby/version.rb +3 -0
- data/static/base/images/ipynblogo.png +0 -0
- data/static/base/images/ipynblogo.svg +268 -0
- data/static/custom/custom.css +1 -0
- data/static/custom/custom.js +8 -0
- metadata +198 -0
data/lib/iruby/kernel.rb
ADDED
@@ -0,0 +1,220 @@
|
|
1
|
+
require 'ffi-rzmq'
|
2
|
+
require 'json'
|
3
|
+
require 'ostruct'
|
4
|
+
require 'term/ansicolor'
|
5
|
+
|
6
|
+
require 'iruby/kernel_completer'
|
7
|
+
require 'iruby/session'
|
8
|
+
require 'iruby/out_stream'
|
9
|
+
require 'iruby/display_hook'
|
10
|
+
require 'iruby/output/html'
|
11
|
+
|
12
|
+
|
13
|
+
class String
|
14
|
+
include Term::ANSIColor
|
15
|
+
end
|
16
|
+
|
17
|
+
module IRuby
|
18
|
+
|
19
|
+
class ResponseWithMime
|
20
|
+
def initialize(response, mime)
|
21
|
+
@response = response
|
22
|
+
@mime = mime
|
23
|
+
end
|
24
|
+
attr_reader :mime
|
25
|
+
def inspect
|
26
|
+
@response.inspect
|
27
|
+
end
|
28
|
+
def to_s
|
29
|
+
@response.to_s
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class BindingWithMime < OpenStruct
|
34
|
+
def mime_type=(v)
|
35
|
+
@mime_type = v
|
36
|
+
end
|
37
|
+
def mime_type
|
38
|
+
@mime_type || 'text/plain'
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
class Kernel
|
44
|
+
attr_accessor :user_ns
|
45
|
+
|
46
|
+
def self.html(s)
|
47
|
+
@output_mime = "text/html"
|
48
|
+
s
|
49
|
+
end
|
50
|
+
|
51
|
+
def execution_count
|
52
|
+
@execution_count
|
53
|
+
end
|
54
|
+
|
55
|
+
def output_mime
|
56
|
+
@output_mime
|
57
|
+
end
|
58
|
+
|
59
|
+
def initialize session, reply_socket, pub_socket
|
60
|
+
@session = session
|
61
|
+
@reply_socket = reply_socket
|
62
|
+
@pub_socket = pub_socket
|
63
|
+
@user_ns = BindingWithMime.new.send(:binding)
|
64
|
+
@history = []
|
65
|
+
@execution_count = 0
|
66
|
+
#@compiler = CommandCompiler.new()
|
67
|
+
@completer = KernelCompleter.new(@user_ns)
|
68
|
+
|
69
|
+
# Build dict of handlers for message types
|
70
|
+
@handlers = {}
|
71
|
+
['execute_request', 'complete_request', 'kernel_info_request'].each do |msg_type|
|
72
|
+
@handlers[msg_type] = msg_type
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def abort_queue
|
77
|
+
while true
|
78
|
+
#begin
|
79
|
+
ident = @reply_socket.recv(ZMQ::NOBLOCK)
|
80
|
+
#rescue Exception => e
|
81
|
+
#if e.errno == ZMQ::EAGAIN
|
82
|
+
#break
|
83
|
+
#else
|
84
|
+
#assert self.reply_socket.rcvmore(), "Unexpected missing message part."
|
85
|
+
#msg = self.reply_socket.recv_json()
|
86
|
+
#end
|
87
|
+
#end
|
88
|
+
msg_type = msg['header']['msg_type']
|
89
|
+
reply_type = msg_type.split('_')[0] + '_reply'
|
90
|
+
@session.send(@reply_socket, reply_type, {status: 'aborted'}, msg)
|
91
|
+
# reply_msg = @session.msg(reply_type, {status: 'aborted'}, msg)
|
92
|
+
# @reply_socket.send(ident,ZMQ::SNDMORE)
|
93
|
+
# @reply_socket.send(reply_msg.to_json)
|
94
|
+
# We need to wait a bit for requests to come in. This can probably
|
95
|
+
# be set shorter for true asynchronous clients.
|
96
|
+
sleep(0.1)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def kernel_info_request(ident, parent)
|
101
|
+
reply_content = {
|
102
|
+
protocol_version: [4, 0],
|
103
|
+
|
104
|
+
# Language version number (mandatory).
|
105
|
+
# It is Python version number (e.g., [2, 7, 3]) for the kernel
|
106
|
+
# included in IPython.
|
107
|
+
language_version: RUBY_VERSION.split('.').map { |x| x.to_i },
|
108
|
+
|
109
|
+
# Programming language in which kernel is implemented (mandatory).
|
110
|
+
# Kernel included in IPython returns 'python'.
|
111
|
+
language: "ruby"
|
112
|
+
}
|
113
|
+
reply_msg = @session.send(@reply_socket, 'kernel_info_reply',
|
114
|
+
reply_content, parent, ident
|
115
|
+
)
|
116
|
+
end
|
117
|
+
|
118
|
+
def send_status(status, parent)
|
119
|
+
@session.send(@pub_socket, "status", {execution_state: status}, parent)
|
120
|
+
end
|
121
|
+
|
122
|
+
def execute_request(ident, parent)
|
123
|
+
begin
|
124
|
+
code = parent['content']['code']
|
125
|
+
rescue
|
126
|
+
STDERR.puts "Got bad msg: "
|
127
|
+
STDERR.puts parent
|
128
|
+
return
|
129
|
+
end
|
130
|
+
# pyin_msg = @session.msg()
|
131
|
+
if ! parent['content'].fetch('silent', false)
|
132
|
+
@execution_count += 1
|
133
|
+
end
|
134
|
+
self.send_status("busy", parent)
|
135
|
+
@session.send(@pub_socket, 'pyin', {code: code}, parent)
|
136
|
+
reply_content = {status: 'ok',
|
137
|
+
payload: [],
|
138
|
+
user_variables: {},
|
139
|
+
user_expressions: {},
|
140
|
+
}
|
141
|
+
result = nil
|
142
|
+
begin
|
143
|
+
$displayhook.set_parent(parent)
|
144
|
+
$stdout.set_parent(parent)
|
145
|
+
|
146
|
+
eval("@mime_type=nil",@user_ns)
|
147
|
+
result = eval(code, @user_ns)
|
148
|
+
rescue Exception => e
|
149
|
+
# $stderr.puts e.inspect
|
150
|
+
#etype, evalue, tb = sys.exc_info()
|
151
|
+
ename, evalue, tb = e.class.to_s, e.message, e.backtrace
|
152
|
+
tb = format_exception(ename, evalue, tb)
|
153
|
+
#tb = "1, 2, 3"
|
154
|
+
exc_content = {
|
155
|
+
ename: ename,
|
156
|
+
evalue: evalue,
|
157
|
+
traceback: tb,
|
158
|
+
#etype: etype,
|
159
|
+
#status: 'error',
|
160
|
+
}
|
161
|
+
# STDERR.puts exc_content
|
162
|
+
@session.send(@pub_socket, 'pyerr', exc_content, parent)
|
163
|
+
|
164
|
+
reply_content = exc_content
|
165
|
+
end
|
166
|
+
reply_content['execution_count'] = @execution_count
|
167
|
+
|
168
|
+
# reply_msg = @session.msg('execute_reply', reply_content, parent)
|
169
|
+
#$stdout.puts reply_msg
|
170
|
+
#$stderr.puts reply_msg
|
171
|
+
#@session.send(@reply_socket, ident + reply_msg)
|
172
|
+
reply_msg = @session.send(@reply_socket, 'execute_reply', reply_content, parent, ident)
|
173
|
+
if reply_msg['content']['status'] == 'error'
|
174
|
+
abort_queue
|
175
|
+
end
|
176
|
+
if ! result.nil? and ! parent['content']['silent']
|
177
|
+
output = ResponseWithMime.new(result, eval("@mime_type",@user_ns))
|
178
|
+
$displayhook.display(output)
|
179
|
+
end
|
180
|
+
self.send_status("idle", parent)
|
181
|
+
end
|
182
|
+
|
183
|
+
def complete_request(ident, parent)
|
184
|
+
matches = {
|
185
|
+
matches: @completer.complete(parent['content']['line'], parent['content']['text']),
|
186
|
+
status: 'ok',
|
187
|
+
matched_text: parent['content']['line'],
|
188
|
+
}
|
189
|
+
completion_msg = @session.send(@reply_socket, 'complete_reply',
|
190
|
+
matches, parent, ident)
|
191
|
+
return nil
|
192
|
+
end
|
193
|
+
|
194
|
+
def start()
|
195
|
+
self.send_status("starting", nil)
|
196
|
+
while true
|
197
|
+
ident, msg = @session.recv(@reply_socket, 0)
|
198
|
+
begin
|
199
|
+
handler = @handlers[msg['header']['msg_type']]
|
200
|
+
rescue
|
201
|
+
handler = nil
|
202
|
+
end
|
203
|
+
if handler.nil?
|
204
|
+
STDERR.puts "UNKNOWN MESSAGE TYPE: #{msg['header']['msg_type']} #{msg}"
|
205
|
+
else
|
206
|
+
# STDERR.puts 'handling ' + omsg.inspect
|
207
|
+
send(handler, ident, msg)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
private
|
213
|
+
def format_exception(name, value, backtrace)
|
214
|
+
tb = []
|
215
|
+
tb << "#{name.red}: #{value}"
|
216
|
+
tb.concat(backtrace.map { |l| l.white })
|
217
|
+
tb
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'bond'
|
2
|
+
|
3
|
+
module IRuby
|
4
|
+
class KernelCompleter
|
5
|
+
class FakeReadline
|
6
|
+
def self.setup(arg)
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.line_buffer
|
10
|
+
@line_buffer
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(ns)
|
15
|
+
@ns = ns
|
16
|
+
Bond.start(readline: FakeReadline, debug: true)
|
17
|
+
end
|
18
|
+
|
19
|
+
def complete(line, text)
|
20
|
+
tab(line)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
def tab(full_line, last_word=full_line)
|
25
|
+
# TODO use @ns as binding
|
26
|
+
Bond.agent.weapon.instance_variable_set('@line_buffer', full_line)
|
27
|
+
Bond.agent.call(last_word)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module IRuby
|
2
|
+
class Message
|
3
|
+
# A simple message object that maps dict keys to attributes.
|
4
|
+
|
5
|
+
# A Message can be created from a dict and a dict from a Message instance
|
6
|
+
# simply by calling dict(msg_obj)."""
|
7
|
+
|
8
|
+
def initialize msg_dict
|
9
|
+
@dct = {}
|
10
|
+
msg_dict.each_pair do |k, v|
|
11
|
+
if v.is_a?(Hash)
|
12
|
+
v = Message.new(v)
|
13
|
+
end
|
14
|
+
@dct[k] = v
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def method_missing(m, *args, &block)
|
19
|
+
@dct[m.to_s]
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.msg_header(msg_id, username, session)
|
23
|
+
return {
|
24
|
+
msg_id: msg_id,
|
25
|
+
username: username,
|
26
|
+
session: session
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.extract_header(msg_or_header)
|
31
|
+
# Given a message or header, return the header.
|
32
|
+
if msg_or_header.nil?
|
33
|
+
return {}
|
34
|
+
end
|
35
|
+
# See if msg_or_header is the entire message.
|
36
|
+
h = msg_or_header['header']
|
37
|
+
# See if msg_or_header is just the header
|
38
|
+
#h ||= msg_or_header['msg_id']
|
39
|
+
h ||= msg_or_header
|
40
|
+
|
41
|
+
return h
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module IRuby
|
2
|
+
class OutStream
|
3
|
+
#A file like object that publishes the stream to a 0MQ PUB socket.
|
4
|
+
|
5
|
+
def initialize session, pub_socket, name, max_buffer=200
|
6
|
+
@session = session
|
7
|
+
@pub_socket = pub_socket
|
8
|
+
@name = name
|
9
|
+
@_buffer = []
|
10
|
+
@_buffer_len = 0
|
11
|
+
@max_buffer = max_buffer
|
12
|
+
@parent_header = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def set_parent parent
|
16
|
+
header = Message.extract_header(parent)
|
17
|
+
@parent_header = header
|
18
|
+
end
|
19
|
+
|
20
|
+
def close
|
21
|
+
@pub_socket = nil
|
22
|
+
end
|
23
|
+
|
24
|
+
def flush
|
25
|
+
# STDERR.puts("flushing, parent to follow")
|
26
|
+
# STDERR.puts @parent_header.inspect
|
27
|
+
if @pub_socket.nil?
|
28
|
+
raise 'I/O operation on closed file'
|
29
|
+
else
|
30
|
+
if @_buffer
|
31
|
+
data = @_buffer.join('')
|
32
|
+
content = { name: @name, data: data }
|
33
|
+
if ! @session
|
34
|
+
return
|
35
|
+
end
|
36
|
+
# msg = @session.msg('stream', content, @parent_header) if @session
|
37
|
+
# FIXME: Wha?
|
38
|
+
# STDERR.puts msg.to_json
|
39
|
+
#@pub_socket.send(msg.to_json)
|
40
|
+
@session.send(@pub_socket, 'stream', content, @parent_header)
|
41
|
+
@_buffer_len = 0
|
42
|
+
@_buffer = []
|
43
|
+
nil
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def isattr
|
49
|
+
return false
|
50
|
+
end
|
51
|
+
|
52
|
+
def next
|
53
|
+
raise 'Read not supported on a write only stream.'
|
54
|
+
end
|
55
|
+
|
56
|
+
def read size=0
|
57
|
+
raise 'Read not supported on a write only stream.'
|
58
|
+
end
|
59
|
+
alias readline read
|
60
|
+
|
61
|
+
def write s
|
62
|
+
if @pub_socket.nil?
|
63
|
+
raise 'I/O operation on closed file'
|
64
|
+
else
|
65
|
+
s = s.to_s
|
66
|
+
@_buffer << s
|
67
|
+
@_buffer_len += s.length
|
68
|
+
_maybe_send
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def puts str
|
73
|
+
write "#{str}\n"
|
74
|
+
end
|
75
|
+
|
76
|
+
def _maybe_send
|
77
|
+
#if self._buffer[-1].include?('\n')
|
78
|
+
flush
|
79
|
+
#end
|
80
|
+
#if @_buffer_len > @max_buffer
|
81
|
+
#flush
|
82
|
+
#end
|
83
|
+
end
|
84
|
+
|
85
|
+
def writelines sequence
|
86
|
+
if @pub_socket.nil?
|
87
|
+
raise 'I/O operation on closed file'
|
88
|
+
else
|
89
|
+
sequence.each do |s|
|
90
|
+
write(s)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
module IRuby
|
2
|
+
module Output
|
3
|
+
module HTML
|
4
|
+
require "gruff"
|
5
|
+
require "base64"
|
6
|
+
def self.table(data)
|
7
|
+
#
|
8
|
+
# data = {a: 1, b:2}
|
9
|
+
|
10
|
+
if data.respond_to?(:keys)
|
11
|
+
d = data
|
12
|
+
else
|
13
|
+
d = data
|
14
|
+
end
|
15
|
+
|
16
|
+
r = "<table>"
|
17
|
+
if d.respond_to?(:keys) # hash
|
18
|
+
columns = [0,1]
|
19
|
+
else
|
20
|
+
columns = d.first.keys
|
21
|
+
r << "<tr>#{columns.map{|c| "<th>#{c}</th>"}.join("\n")}</tr>"
|
22
|
+
end
|
23
|
+
d.each{|row|
|
24
|
+
r << "<tr>"
|
25
|
+
columns.each{|column|
|
26
|
+
r << "<td>#{row[column]}</td>"
|
27
|
+
}
|
28
|
+
r << "</tr>"
|
29
|
+
}
|
30
|
+
r << "</table>"
|
31
|
+
r
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.image(image)
|
35
|
+
data = image.respond_to?(:to_blob) ? image.to_blob : image
|
36
|
+
"<img src='data:image/png;base64,#{Base64.encode64(data)}'>"
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.chart_pie(o)
|
40
|
+
data=o.delete(:data)
|
41
|
+
title=o.delete(:title)
|
42
|
+
size=o.delete(:size) || 300
|
43
|
+
g = Gruff::Pie.new(size)
|
44
|
+
g.title = title if title
|
45
|
+
data.each do |data|
|
46
|
+
g.data(data[0], data[1])
|
47
|
+
end
|
48
|
+
image(g.to_blob)
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.chart_bar(o)
|
52
|
+
data=o.delete(:data)
|
53
|
+
title=o.delete(:title)
|
54
|
+
size=o.delete(:size) || 300
|
55
|
+
|
56
|
+
klass = o.delete(:stacked) ? Gruff::StackedBar : Gruff::Bar
|
57
|
+
g = klass.new(size)
|
58
|
+
|
59
|
+
if labels=o.delete(:labels)
|
60
|
+
if ! labels.respond_to?(:keys)
|
61
|
+
labels = Hash[labels.map.with_index{|v,k| [k,v]}]
|
62
|
+
end
|
63
|
+
g.labels = labels
|
64
|
+
end
|
65
|
+
|
66
|
+
g.title = title if title
|
67
|
+
data.each do |data|
|
68
|
+
g.data(data[0], data[1])
|
69
|
+
end
|
70
|
+
image(g.to_blob)
|
71
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
module Gmaps
|
75
|
+
def self.points2latlng(points)
|
76
|
+
"[" + points.reject{|p| not p.lat or not p.lon}.map{|p|
|
77
|
+
" {location: new google.maps.LatLng(#{p.lat.to_f}, #{p.lon.to_f}) #{", weight: #{p.weight.to_i}" if p.respond_to?(:weight) and p.weight} } "
|
78
|
+
}.join(',') + "]"
|
79
|
+
end
|
80
|
+
def self.heatmap(o)
|
81
|
+
data = o.delete(:points)
|
82
|
+
raise "Missing :points parameter" if not data
|
83
|
+
|
84
|
+
points = points2latlng(data)
|
85
|
+
zoom = o.delete(:zoom)
|
86
|
+
center = o.delete(:center)
|
87
|
+
map_type = o.delete(:map_type)
|
88
|
+
r = <<E
|
89
|
+
<div id='map-canvas' style='width: 500px; height: 500px;'></div>
|
90
|
+
<script src="https://maps.googleapis.com/maps/api/js?v=3.exp&sensor=false&libraries=visualization&callback=initialize"></script>
|
91
|
+
|
92
|
+
<script>
|
93
|
+
function initialize() {
|
94
|
+
var points = #{points};
|
95
|
+
var latlngbounds = new google.maps.LatLngBounds();
|
96
|
+
var zoom = #{zoom.to_json};
|
97
|
+
var center = #{center.to_json};
|
98
|
+
var map_type = #{map_type.to_json} || google.maps.MapTypeId.SATELLITE;
|
99
|
+
|
100
|
+
var mapOptions = {
|
101
|
+
mapTypeId: map_type
|
102
|
+
};
|
103
|
+
|
104
|
+
if (zoom){
|
105
|
+
mapOptions.zoom = zoom
|
106
|
+
}
|
107
|
+
if (center){
|
108
|
+
mapOptions.center = new google.maps.LatLng(center.lat, center.lon)
|
109
|
+
}
|
110
|
+
|
111
|
+
map = new google.maps.Map(document.getElementById('map-canvas'), mapOptions);
|
112
|
+
|
113
|
+
if (! zoom){
|
114
|
+
for (var i = 0; i < points.length; i++) {
|
115
|
+
latlngbounds.extend(points[i].location);
|
116
|
+
}
|
117
|
+
map.fitBounds(latlngbounds);
|
118
|
+
}
|
119
|
+
|
120
|
+
|
121
|
+
var pointArray = new google.maps.MVCArray(points);
|
122
|
+
|
123
|
+
heatmap = new google.maps.visualization.HeatmapLayer({
|
124
|
+
data: pointArray
|
125
|
+
});
|
126
|
+
|
127
|
+
heatmap.setMap(map);
|
128
|
+
}
|
129
|
+
console.log("finished pre- init!")
|
130
|
+
</script>
|
131
|
+
E
|
132
|
+
STDERR.write("#{r}\n\n")
|
133
|
+
r
|
134
|
+
end
|
135
|
+
end
|
136
|
+
#stolen from https://github.com/Bantik/heatmap/blob/master/lib/heatmap.rb
|
137
|
+
module WordCloud
|
138
|
+
|
139
|
+
def self.wordcloud(histogram={})
|
140
|
+
html = %{<div class="wordcloud">}
|
141
|
+
histogram.keys.sort{|a,b| histogram[a] <=> histogram[b]}.reverse.each do |k|
|
142
|
+
next if histogram[k] < 1
|
143
|
+
_max = histogram_max(histogram) * 2
|
144
|
+
_size = element_size(histogram, k)
|
145
|
+
_heat = element_heat(histogram[k], _max)
|
146
|
+
html << %{
|
147
|
+
<span class="wordcloud_element" style="color: ##{_heat}#{_heat}#{_heat}; font-size: #{_size}px;">#{k}</span>
|
148
|
+
}
|
149
|
+
end
|
150
|
+
html << %{<br style="clear: both;" /></div>}
|
151
|
+
end
|
152
|
+
|
153
|
+
def self.histogram_max(histogram)
|
154
|
+
histogram.map{|k,v| histogram[k]}.max
|
155
|
+
end
|
156
|
+
|
157
|
+
def self.element_size(histogram, key)
|
158
|
+
(((histogram[key] / histogram.map{|k,v| histogram[k]}.reduce(&:+).to_f) * 100) + 5).to_i
|
159
|
+
end
|
160
|
+
|
161
|
+
def self.element_heat(val, max)
|
162
|
+
sprintf("%02x" % (200 - ((200.0 / max) * val)))
|
163
|
+
end
|
164
|
+
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|