terse_ruby 0.1.0 → 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 +4 -4
- data/README.txt +55 -3
- data/build.rb +50 -0
- data/lib/{terse_ruby → terse}/format.rb +16 -15
- data/lib/terse/keyword.rb +166 -0
- data/lib/terse/keyword_java.rb +103 -0
- data/lib/{terse_ruby/keyword.rb → terse/keyword_ruby.rb} +10 -17
- data/lib/terse/scan.rb +294 -0
- data/lib/terse/settings.rb +20 -0
- data/lib/terse_java.rb +24 -0
- data/lib/terse_ruby.rb +35 -2
- data/terse_ruby.gemspec +3 -1
- data/test/expected_test_file_java.java +22 -0
- data/test/{expected_test_file.rb → expected_test_file_ruby.rb} +4 -4
- data/test/test_expansion_java.rb +75 -0
- data/test/{test_expansion.rb → test_expansion_ruby.rb} +16 -5
- data/test/test_file_java.txt +14 -0
- data/test/{test_file.txt → test_file_ruby.txt} +0 -0
- metadata +60 -12
- data/bin/terse_ruby.rb +0 -59
- data/lib/terse_ruby/scan.rb +0 -147
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 79aecaa694ade661bca00b2cd64fdf3704212b7f
|
4
|
+
data.tar.gz: 1dff71c71f0a27f982394d3d30cfc6949bd17000
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0e8abe0b27382b2aac3a15aba0a12c6c901e9c78b2c7dea75d80888e7066417ec763f3459261a8d04ee4194ea78b3306d86db62d07bed40c4fadfd83ef1d6da4
|
7
|
+
data.tar.gz: 064b6152d319c606beaad32edeaa616e9a6a561f331c7ca104a968963fb3811b2ab2ff5cb78cce68f5fa33097866fba1995ac045c201c06bbb94108a4dac7d85
|
data/README.txt
CHANGED
@@ -18,7 +18,7 @@
|
|
18
18
|
# end
|
19
19
|
#
|
20
20
|
#
|
21
|
-
#
|
21
|
+
# Keywords :
|
22
22
|
# Terse Ruby will look through the supplied files and convert certain keywords at the start of lines :
|
23
23
|
# c -> class
|
24
24
|
# m -> module
|
@@ -49,7 +49,59 @@
|
|
49
49
|
#
|
50
50
|
# Alternatively, the below two lines are all you need to run terse_ruby within your own Ruby file :
|
51
51
|
#
|
52
|
-
#
|
52
|
+
# require "terse_ruby"
|
53
|
+
#
|
54
|
+
# # (optionally add to ARGV here)
|
53
55
|
# TerseRuby.scan_files ARGV
|
54
56
|
#
|
55
|
-
#
|
57
|
+
# If ARGV have not been supplied, you'll need to add your args to ARGV, e.g. ARGV << "-v"; ARGV << "terse_file.txt"
|
58
|
+
# Note that ARGV is not any ordinary array; terse_ruby uses the 'argv' gem which adds methods to the ARGV object;
|
59
|
+
# therefore we must supply the ARGV object to TerseRuby.scan_files
|
60
|
+
|
61
|
+
|
62
|
+
# Terse Java
|
63
|
+
#
|
64
|
+
# Invoke in the same way as TerseRuby, but instead using the module name TerseJava; e.g.
|
65
|
+
# ruby -e "require 'terse_ruby'; TerseRuby.scan_files ARGV" [one or more files] [flags]
|
66
|
+
# or
|
67
|
+
# TerseJava.scan_files ARGV
|
68
|
+
#
|
69
|
+
# Keywords :
|
70
|
+
#
|
71
|
+
# imp -> import
|
72
|
+
# pk -> package
|
73
|
+
# c -> class
|
74
|
+
# impl -> implements
|
75
|
+
# ex -> extends
|
76
|
+
# int -> interface
|
77
|
+
# ab -> abstract
|
78
|
+
# e -> enum
|
79
|
+
# st -> static
|
80
|
+
# v -> void
|
81
|
+
# m -> main
|
82
|
+
# p -> public
|
83
|
+
# pt -> protected
|
84
|
+
# pv -> private
|
85
|
+
# r -> return
|
86
|
+
# s -> String
|
87
|
+
# i -> Integer
|
88
|
+
# b -> Boolean
|
89
|
+
# f -> Float
|
90
|
+
# fn -> final
|
91
|
+
#
|
92
|
+
#
|
93
|
+
# Flags :
|
94
|
+
#
|
95
|
+
# The same flags may be used as with Terse Ruby
|
96
|
+
#
|
97
|
+
# Notes :
|
98
|
+
#
|
99
|
+
# Constructors will not be made, as it is impossble to 100% accurately distinguish between a variable and a method in terse form.
|
100
|
+
# Getters and setters will not be made, as it is impossble to 100% accurately distinguish between a variable and a method in terse form.
|
101
|
+
# This is acceptable, as most IDEs are able to generate constructors and getters-and-setters.
|
102
|
+
#
|
103
|
+
# Primitive types (int, float, etc.) will never be used; i -> Integer, f -> Float, etc.
|
104
|
+
#
|
105
|
+
# Obvious class-beginnings will be given { (and } at the end of the file); obvious method endings (i.e. the 'return' keyword) will be given }
|
106
|
+
# Because in terse form, there is ambiguity between methods and variables, some method starts will not gain { and some regular code lines will not gain ;
|
107
|
+
# You should expect Java expanded from terse-form to contain compilation errors of this kind.
|
data/build.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
# Simple build script
|
2
|
+
# Deletes the existing gem file (if present)
|
3
|
+
# Uninstalls the gem
|
4
|
+
# Builds the gem
|
5
|
+
# Installs the new gem
|
6
|
+
|
7
|
+
# Note : when calling system (or backticks, etc.) th enew process starts at the system default, not the current working directory.
|
8
|
+
# Therefore we need to use a syntax of : system "cd #{gem_dir} && #{i_cmd}"
|
9
|
+
|
10
|
+
# Run from this directory!
|
11
|
+
gem_dir = Dir.getwd
|
12
|
+
|
13
|
+
# Delete existing .gem files in the dir
|
14
|
+
|
15
|
+
gemfiles = Dir.entries(gem_dir).collect {|q| q if q =~ /.gem$/}.compact
|
16
|
+
|
17
|
+
gemfiles.each do |q|
|
18
|
+
File.delete q
|
19
|
+
puts "Deleted #{q}"
|
20
|
+
end
|
21
|
+
|
22
|
+
gemfiles = Dir.entries(gem_dir).collect {|q| q if q =~ /.gem$/}.compact
|
23
|
+
raise "Gem has not been deleted" unless gemfiles.size == 0
|
24
|
+
|
25
|
+
# Uninstall, build, install
|
26
|
+
gemspecs = Dir.entries(gem_dir).collect {|q| q if q =~ /.gemspec$/}.compact
|
27
|
+
|
28
|
+
raise "Did not find a .gemspec in #{gem_dir}" if gemspecs.size < 1
|
29
|
+
raise "Found more than one .gemspec in #{gem_dir}" if gemspecs.size > 1
|
30
|
+
|
31
|
+
gemspec = gemspecs[0]
|
32
|
+
|
33
|
+
gemname = File.basename(gemspec, File.extname(gemspec))
|
34
|
+
|
35
|
+
u_cmd = "gem uninstall #{gemname}"
|
36
|
+
system u_cmd
|
37
|
+
|
38
|
+
b_cmd = "gem build #{gemspec}"
|
39
|
+
system "cd #{gem_dir} && #{b_cmd}"
|
40
|
+
|
41
|
+
gemfiles = Dir.entries(gem_dir).collect {|q| q if q =~ /.gem$/}.compact
|
42
|
+
raise "Gem was not built" unless gemfiles.size == 1
|
43
|
+
|
44
|
+
gemfile = gemfiles[0]
|
45
|
+
raise "Gem file is not for the expected gem, expected a #{gemname} gem but found #{gemfile}" unless gemfile =~ /^#{gemname}/
|
46
|
+
|
47
|
+
i_cmd = "gem install #{gemfile}"
|
48
|
+
system "cd #{gem_dir} && #{i_cmd}"
|
49
|
+
|
50
|
+
puts "Gem #{gemname} built & installed"
|
@@ -1,38 +1,39 @@
|
|
1
|
-
module
|
1
|
+
module Terse
|
2
2
|
|
3
3
|
# Simple class for formatting the list of lines
|
4
4
|
class Format
|
5
5
|
|
6
6
|
# Apply indentation
|
7
7
|
# It is easier to do this once the full list of newlines is assembled
|
8
|
-
def self.indent lines
|
8
|
+
def self.indent lines, settings
|
9
9
|
indented_lines = []
|
10
10
|
indent_level = 0
|
11
|
-
keys = %w{ class module def if when case for }
|
12
|
-
first_word_regex = /^([a-zA-Z0-9_]+)(\s+|$)/
|
13
11
|
lines.each do |l|
|
14
12
|
l.strip!
|
15
|
-
l =~
|
16
|
-
|
17
|
-
|
18
|
-
|
13
|
+
l =~ settings.indent_out_regex
|
14
|
+
|
15
|
+
# check for a loop-ending before indenting
|
16
|
+
indent_level -= 1 if settings.indent_out.include?($1.to_s.strip) # must to_s in case $1 is nil
|
19
17
|
indent_level.times do
|
20
18
|
l.insert(0, "\t")
|
21
19
|
end
|
22
|
-
indented_lines << l
|
23
|
-
# puts indented_lines
|
20
|
+
indented_lines << l.dup # need to call .dup ; it will otherwise mess up indentation of loop-ending lines
|
24
21
|
|
25
22
|
# check for indent-increasing keywords after indenting
|
26
|
-
|
23
|
+
l =~ settings.indent_in_regex
|
24
|
+
indent_level += 1 if settings.indent_in.include?($1.to_s.strip) # must to_s in case $1 is nil
|
27
25
|
end
|
28
26
|
indented_lines
|
29
27
|
end
|
30
28
|
|
31
29
|
# Insert empty lines in between the end of a method and the start of the next (same for classes & modules)
|
32
|
-
def self.space_lines lines
|
30
|
+
def self.space_lines lines, keywords, settings
|
33
31
|
spaced_lines = []
|
34
32
|
|
35
|
-
|
33
|
+
# Filter out keywords that do not trigger some sort of indent-change
|
34
|
+
keys = keywords.dup
|
35
|
+
keys = keys.delete_if {|k| !(k.needs_top_level_end || k.needs_inner_end)}
|
36
|
+
keys.collect! {|k| k.substitute}
|
36
37
|
first_word_regex = /^\s*([a-zA-Z0-9_]+)(\s+|$)/
|
37
38
|
for i in 0 ... lines.size - 1
|
38
39
|
first_line = lines[i]
|
@@ -42,9 +43,9 @@ module TerseRuby
|
|
42
43
|
|
43
44
|
first_line =~ first_word_regex # set the $ variables for inspection
|
44
45
|
# We only care about line-pairs where the first line is an end, and the next one matches one of our keys
|
45
|
-
if $1 ==
|
46
|
+
if $1.to_s.strip == settings.loop_ending.strip
|
46
47
|
next_line =~ first_word_regex
|
47
|
-
spaced_lines << "" if keys.include?($1)
|
48
|
+
spaced_lines << "" if keys.include?($1.to_s.strip)
|
48
49
|
end
|
49
50
|
end
|
50
51
|
# Remember to add the last line!
|
@@ -0,0 +1,166 @@
|
|
1
|
+
module Terse
|
2
|
+
|
3
|
+
# A class storing all information about a keyword & what it should be transformed into
|
4
|
+
class Keyword
|
5
|
+
attr_accessor :keyword
|
6
|
+
attr_accessor :substitute
|
7
|
+
attr_accessor :regex
|
8
|
+
attr_accessor :follow_on_regex
|
9
|
+
attr_accessor :follow_on_substitute
|
10
|
+
attr_accessor :try_follow_on_regex
|
11
|
+
attr_accessor :needs_top_level_start
|
12
|
+
attr_accessor :needs_inner_start
|
13
|
+
attr_accessor :needs_top_level_end
|
14
|
+
attr_accessor :needs_inner_end
|
15
|
+
attr_accessor :is_end
|
16
|
+
|
17
|
+
# some keywords naturally imply that the line should have a line-ending, e.g. ; for Java
|
18
|
+
# line-endings should not be applied if the keyword indicates the start/end of a class/method/block, etc.
|
19
|
+
# Internally, :needs_top_level_end and :needs_inner_end take priority over :needs_line_ending
|
20
|
+
attr_accessor :needs_line_ending
|
21
|
+
|
22
|
+
def initialize(keyword)
|
23
|
+
@keyword = keyword
|
24
|
+
@regex = gen_regex_from_keyword keyword
|
25
|
+
@follow_on_regex = gen_regex_from_keyword_including_follow_on keyword
|
26
|
+
end
|
27
|
+
|
28
|
+
# # TODO this doesn't work, needs to process the thing following the keyword, not the keyword itself
|
29
|
+
# def set_substitute
|
30
|
+
# case @keyword
|
31
|
+
# when "a", "r", "w"
|
32
|
+
# @substitute = ":" + @substitute
|
33
|
+
# when "req", "reqr"
|
34
|
+
# @substitute = "\"" + @substitute + "\"" unless ["\"", "'"].include? @substitute[0]
|
35
|
+
# end
|
36
|
+
# end
|
37
|
+
|
38
|
+
|
39
|
+
|
40
|
+
# # @substitutes = %W{ require require_relative include class method def end attr_accesor attr_reader attr_writer }
|
41
|
+
|
42
|
+
|
43
|
+
# # def generate_regexes
|
44
|
+
# # r = []
|
45
|
+
# # for i in 0 ... @keywords.size
|
46
|
+
# # r << Keyword.new(@keywords[i], @substitutes[i], gen_regex_from_keyword(@keywords[i]))
|
47
|
+
# # end
|
48
|
+
# # r
|
49
|
+
# # end
|
50
|
+
|
51
|
+
# # Simple regex to match a keyword at the start of a line
|
52
|
+
# def gen_regex_from_keyword keyword
|
53
|
+
# return /(^\s*)(#{keyword})(\s+|$)/
|
54
|
+
# end
|
55
|
+
|
56
|
+
# # Regex to match a keyword at the start of a line, and the next word on that line
|
57
|
+
# # E.g. this will also catch (in $3) the item following the "require" keyword
|
58
|
+
# # This enables the routine to format this follow-on word, e.g. to turn 'req a_gem' into
|
59
|
+
# # 'require "a_gem"'
|
60
|
+
# def gen_regex_from_keyword_including_follow_on keyword
|
61
|
+
# return /(^\s*)(#{keyword})\s+([a-zA-Z0-9_]+)/
|
62
|
+
# end
|
63
|
+
|
64
|
+
# def self.generate_ruby_keywords
|
65
|
+
# keywords = []
|
66
|
+
# keys = [:req, :reqr, :i, :c, :m, :d, :e, :a, :r, :w]
|
67
|
+
# keys.each do |key|
|
68
|
+
# k = Keyword.new key
|
69
|
+
# # k.keyword = key
|
70
|
+
# case key
|
71
|
+
# when :req
|
72
|
+
# k.substitute = "require"
|
73
|
+
# k.try_follow_on_regex = true
|
74
|
+
# k.follow_on_substitute = "\"\#{$3}\""
|
75
|
+
# when :reqr
|
76
|
+
# k.substitute = "require_relative"
|
77
|
+
# k.try_follow_on_regex = true
|
78
|
+
# k.follow_on_substitute = "\"\#{$3}\""
|
79
|
+
# when :i
|
80
|
+
# k.substitute = "include"
|
81
|
+
# when :c
|
82
|
+
# k.substitute = "class"
|
83
|
+
# k.needs_top_level_end = true
|
84
|
+
# when :m
|
85
|
+
# k.substitute = "module"
|
86
|
+
# k.needs_top_level_end = true
|
87
|
+
# when :d
|
88
|
+
# k.substitute = "def"
|
89
|
+
# k.needs_inner_end = true
|
90
|
+
# when :e
|
91
|
+
# k.substitute = "end"
|
92
|
+
# k.is_end = true
|
93
|
+
# when :a
|
94
|
+
# k.substitute = "attr_accessor"
|
95
|
+
# k.try_follow_on_regex = true
|
96
|
+
# k.follow_on_substitute = "$3.to_sym"#"\":\#{$3}\""
|
97
|
+
# when :r
|
98
|
+
# k.substitute = "attr_reader"
|
99
|
+
# k.try_follow_on_regex = true
|
100
|
+
# k.follow_on_substitute = "$3.to_sym"
|
101
|
+
# when :w
|
102
|
+
# k.substitute = "attr_writer"
|
103
|
+
# k.try_follow_on_regex = true
|
104
|
+
# k.follow_on_substitute = "$3.to_sym"
|
105
|
+
|
106
|
+
# else
|
107
|
+
# # raise "Unknown keyword #{key}"
|
108
|
+
# puts "Unknown keyword #{key}"
|
109
|
+
# end
|
110
|
+
# keywords << k
|
111
|
+
# end # end keys.each
|
112
|
+
# keywords
|
113
|
+
# end
|
114
|
+
|
115
|
+
# def self.generate_java_keywords
|
116
|
+
# keywords = []
|
117
|
+
# keys = [:imp, :c, :ex, :inc, :ab, :e, :st, :v, :m, :p, :pt, :pv, :r, :s, :i, :b, :f, :fn]
|
118
|
+
# keys.each do |key|
|
119
|
+
# k = Keyword.new key
|
120
|
+
# # k.keyword = key
|
121
|
+
# case key
|
122
|
+
# when :imp
|
123
|
+
# k.substitute = "import"
|
124
|
+
# k.try_follow_on_regex = true
|
125
|
+
# k.follow_on_substitute = "\"\#{$3}\""
|
126
|
+
# when :reqr
|
127
|
+
# k.substitute = "require_relative"
|
128
|
+
# k.try_follow_on_regex = true
|
129
|
+
# k.follow_on_substitute = "\"\#{$3}\""
|
130
|
+
# when :i
|
131
|
+
# k.substitute = "include"
|
132
|
+
# when :c
|
133
|
+
# k.substitute = "class"
|
134
|
+
# k.needs_top_level_end = true
|
135
|
+
# when :m
|
136
|
+
# k.substitute = "module"
|
137
|
+
# k.needs_top_level_end = true
|
138
|
+
# when :d
|
139
|
+
# k.substitute = "def"
|
140
|
+
# k.needs_inner_end = true
|
141
|
+
# when :e
|
142
|
+
# k.substitute = "end"
|
143
|
+
# k.is_end = true
|
144
|
+
# when :a
|
145
|
+
# k.substitute = "attr_accessor"
|
146
|
+
# k.try_follow_on_regex = true
|
147
|
+
# k.follow_on_substitute = "$3.to_sym"#"\":\#{$3}\""
|
148
|
+
# when :r
|
149
|
+
# k.substitute = "attr_reader"
|
150
|
+
# k.try_follow_on_regex = true
|
151
|
+
# k.follow_on_substitute = "$3.to_sym"
|
152
|
+
# when :w
|
153
|
+
# k.substitute = "attr_writer"
|
154
|
+
# k.try_follow_on_regex = true
|
155
|
+
# k.follow_on_substitute = "$3.to_sym"
|
156
|
+
|
157
|
+
# else
|
158
|
+
# # raise "Unknown keyword #{key}"
|
159
|
+
# puts "Unknown keyword #{key}"
|
160
|
+
# end
|
161
|
+
# keywords << k
|
162
|
+
# end # end keys.each
|
163
|
+
# keywords
|
164
|
+
# end
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require_relative "keyword"
|
2
|
+
|
3
|
+
module Terse
|
4
|
+
|
5
|
+
# A class storing all information about a keyword & what it should be transformed into
|
6
|
+
class KeywordJava < Keyword
|
7
|
+
|
8
|
+
def initialize(keyword)
|
9
|
+
super(keyword)
|
10
|
+
@loop_ending = "}"
|
11
|
+
@line_ending = ";"
|
12
|
+
end
|
13
|
+
|
14
|
+
# Simple regex to match a keyword
|
15
|
+
def gen_regex_from_keyword keyword
|
16
|
+
# return /(^|\s+)(#{keyword})(\s+|$)/
|
17
|
+
return /(^|\s+|\w*\()(#{keyword})(\s+|$)/
|
18
|
+
end
|
19
|
+
|
20
|
+
# Regex to match a keyword at the start of a line, and the next word on that line
|
21
|
+
# E.g. this will also catch (in $3) the item following the "require" keyword
|
22
|
+
# This enables the routine to format this follow-on word, e.g. to turn 'req a_gem' into
|
23
|
+
# 'require "a_gem"'
|
24
|
+
def gen_regex_from_keyword_including_follow_on keyword
|
25
|
+
return /(^|\s+)(#{keyword})\s+([a-zA-Z0-9_]+)/
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.generate_keywords
|
29
|
+
keywords = []
|
30
|
+
keys = [:imp, :pk, :c, :ex, :impl, :int, :ab, :e, :st, :v, :m, :p, :pt, :pv, :r, :s, :i, :b, :f, :fn]
|
31
|
+
keys.each do |key|
|
32
|
+
k = KeywordJava.new key
|
33
|
+
# k.keyword = key
|
34
|
+
case key
|
35
|
+
when :imp
|
36
|
+
k.substitute = "import"
|
37
|
+
k.needs_line_ending = true
|
38
|
+
when :pk
|
39
|
+
k.substitute = "package"
|
40
|
+
k.needs_line_ending = true
|
41
|
+
when :c
|
42
|
+
k.substitute = "class"
|
43
|
+
k.needs_top_level_start = true
|
44
|
+
k.needs_top_level_end = true
|
45
|
+
when :ex
|
46
|
+
k.substitute = "extends"
|
47
|
+
when :impl
|
48
|
+
k.substitute = "implements"
|
49
|
+
when :int
|
50
|
+
k.substitute = "interface"
|
51
|
+
when :ab
|
52
|
+
k.substitute = "abstract"
|
53
|
+
when :e
|
54
|
+
k.substitute = "enum"
|
55
|
+
k.needs_top_level_start = true
|
56
|
+
k.needs_top_level_end = true
|
57
|
+
when :st
|
58
|
+
k.substitute = "static"
|
59
|
+
k.needs_line_ending = true
|
60
|
+
when :v
|
61
|
+
k.substitute = "void"
|
62
|
+
k.needs_inner_start = true
|
63
|
+
k.needs_inner_end = true
|
64
|
+
when :m
|
65
|
+
k.substitute = "main"
|
66
|
+
when :p
|
67
|
+
k.substitute = "public"
|
68
|
+
k.needs_line_ending = true
|
69
|
+
when :pt
|
70
|
+
k.substitute = "protected"
|
71
|
+
k.needs_line_ending = true
|
72
|
+
when :pv
|
73
|
+
k.substitute = "private"
|
74
|
+
k.needs_line_ending = true
|
75
|
+
when :r
|
76
|
+
k.substitute = "return"
|
77
|
+
k.needs_inner_end = true
|
78
|
+
k.needs_line_ending = true
|
79
|
+
when :s
|
80
|
+
k.substitute = "String"
|
81
|
+
k.needs_line_ending = true
|
82
|
+
when :i
|
83
|
+
k.substitute = "Integer"
|
84
|
+
k.needs_line_ending = true
|
85
|
+
when :b
|
86
|
+
k.substitute = "Boolean"
|
87
|
+
k.needs_line_ending = true
|
88
|
+
when :f
|
89
|
+
k.substitute = "Float"
|
90
|
+
k.needs_line_ending = true
|
91
|
+
when :fn
|
92
|
+
k.substitute = "final"
|
93
|
+
k.needs_line_ending = true
|
94
|
+
else
|
95
|
+
# raise "Unknown keyword #{key}"
|
96
|
+
puts "Unknown keyword #{key}"
|
97
|
+
end
|
98
|
+
keywords << k
|
99
|
+
end # end keys.each
|
100
|
+
keywords
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|