davetron5000-gliffy 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.
- data/README.rdoc +52 -0
- data/bin/gliffy +8 -0
- data/ext/array_has_response.rb +10 -0
- data/lib/gliffy.rb +1 -0
- data/lib/gliffy/account.rb +55 -0
- data/lib/gliffy/cli.rb +260 -0
- data/lib/gliffy/commands/commands.rb +149 -0
- data/lib/gliffy/config.rb +55 -0
- data/lib/gliffy/diagram.rb +100 -0
- data/lib/gliffy/folder.rb +63 -0
- data/lib/gliffy/gliffy.rb +367 -0
- data/lib/gliffy/response.rb +127 -0
- data/lib/gliffy/rest.rb +110 -0
- data/lib/gliffy/url.rb +67 -0
- data/lib/gliffy/user.rb +72 -0
- metadata +81 -0
data/README.rdoc
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
<em>The Gliffy API is currently in private beta. This will be a Ruby implementation to access your Gliffy account.</em>
|
2
|
+
|
3
|
+
This consists of a Ruby client for Gliffy as well as a simple command line tool to access your Gliffy account using that client.
|
4
|
+
|
5
|
+
== Client Library
|
6
|
+
|
7
|
+
Main entry point is Gliffy::Handle, though you should look at Gliffy::Config for configuration options. Here's an example of getting the list of diagrams and then downloading the first one as a JPEG
|
8
|
+
|
9
|
+
require 'gliffy'
|
10
|
+
|
11
|
+
Gliffy::Config.config.api_key='aa9d0e8d-449b-44ad-904a-bf87deb7c403'
|
12
|
+
Gliffy::Config.config.secret_key='a52585f7-5952-4229-8bbe-b5787521b571'
|
13
|
+
Gliffy::Config.config.account_name='Initech'
|
14
|
+
|
15
|
+
handle = Gliffy::Handle.new('lumberg@initech.com')
|
16
|
+
|
17
|
+
diagrams = handle.get_diagrams
|
18
|
+
diagram_name_safe = diagrams[0].name.gsub(/ /,'_')
|
19
|
+
handle.get_diagram_as_image(diagrams[0].id,:mime_type => :jpeg,:file=>"#{diagram_name_safe}.jpg")
|
20
|
+
|
21
|
+
The Gliffy::Handle is a <b>user-session based connection</b> to Gliffy. You shoudl therefore engineer your application to keep one of these per user who will access Gliffy and *not* make a global instance.
|
22
|
+
|
23
|
+
* RDoc is available a http://davetron5000.github.com/gliffy
|
24
|
+
* Source is available at http://github.com/davetron5000/gliffy/tree/master
|
25
|
+
|
26
|
+
== Command Line Client
|
27
|
+
|
28
|
+
+gliffy+ +help+ shows a list of commands. Currently, there are:
|
29
|
+
|
30
|
+
[+delete+] Delete a diagram (also: del,rm)
|
31
|
+
[+edit+] Edit a diagram
|
32
|
+
[+get+] Download a diagram as an image to a file
|
33
|
+
[+help+] Show commands
|
34
|
+
[+list+] List all diagrams in the account (also: ls)
|
35
|
+
[+new+] Create a new diagram
|
36
|
+
[+url+] Get the URL for an image
|
37
|
+
|
38
|
+
=== Configuration
|
39
|
+
|
40
|
+
The first time you run the client, it will create a base configuration in your home directory called <tt>.gliffyrc</tt>, and will ask you to provide it with four pieces of information:
|
41
|
+
|
42
|
+
[API Key] This is the API Key given to you by Gliffy
|
43
|
+
[Secret Key] This is the Secret Key given to you by Gliffy. <b>Do not check this value into source control</b>
|
44
|
+
[Account Name] Your account's name. This should've been provided with your API Key
|
45
|
+
[Username] The username to work under. The command line client only allows single-user access to your Gliffy account. If you just signed up for Gliffy, this is probably the email address you used to sign up
|
46
|
+
|
47
|
+
If you are not on OS X, you will want to configure two other options, and you can do this by editing your <tt>.gliffyrc</tt> directly:
|
48
|
+
|
49
|
+
[<tt>open_image</tt>] Configures the command for opening an image from the command line. This should be a string of the form <tt>command %s</tt> where <tt>%s</tt> will be replaced with the name of the image file to open.
|
50
|
+
[<tt>open_url</tt>] Configures the command for opening an url from the command line. This should be a string of the form <tt>command %s</tt> where <tt>%s</tt> will be replaced with the name of the url to open.
|
51
|
+
|
52
|
+
The other options are all documented in Gliffy::Config. Note that your most previously received user token is stored here as well, to keep from getting it every time the client is run.
|
data/bin/gliffy
ADDED
data/lib/gliffy.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'gliffy/gliffy'
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'rexml/document'
|
2
|
+
require 'array_has_response'
|
3
|
+
require 'gliffy/rest'
|
4
|
+
|
5
|
+
include REXML
|
6
|
+
|
7
|
+
module Gliffy
|
8
|
+
|
9
|
+
# Represents on account
|
10
|
+
class Account < Response
|
11
|
+
|
12
|
+
attr_reader :name
|
13
|
+
attr_reader :id
|
14
|
+
# Either :basic or :premium
|
15
|
+
attr_reader :type
|
16
|
+
attr_reader :max_users
|
17
|
+
# A Time representing the date on which this account expires
|
18
|
+
attr_reader :expiration_date
|
19
|
+
|
20
|
+
attr_reader :users
|
21
|
+
|
22
|
+
def self.from_xml(element)
|
23
|
+
id = element.attributes['id'].to_i
|
24
|
+
type = element.attributes['account-type']
|
25
|
+
if type == 'Basic'
|
26
|
+
type = :basic
|
27
|
+
elsif type == 'Premium'
|
28
|
+
type = :premium
|
29
|
+
else
|
30
|
+
raise "Unknown type #{type}"
|
31
|
+
end
|
32
|
+
max_users = element.attributes['max-users'].to_i
|
33
|
+
expiration_date = Time.at(element.elements['expiration-date'].text.to_i / 1000)
|
34
|
+
name = element.elements['name'].text
|
35
|
+
users = Users.from_xml(element.elements['users'])
|
36
|
+
|
37
|
+
Account.new(id,type,name,max_users,expiration_date,users)
|
38
|
+
end
|
39
|
+
|
40
|
+
protected
|
41
|
+
|
42
|
+
def initialize(id,type,name,max_users,expiration_date,users=nil)
|
43
|
+
super()
|
44
|
+
@id = id
|
45
|
+
@type = type
|
46
|
+
@name = name
|
47
|
+
@max_users = max_users
|
48
|
+
@expiration_date = expiration_date
|
49
|
+
@users = users
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
class Accounts < ArrayResponseParser; end
|
55
|
+
end
|
data/lib/gliffy/cli.rb
ADDED
@@ -0,0 +1,260 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__)
|
2
|
+
require 'fileutils'
|
3
|
+
require 'yaml'
|
4
|
+
require 'gliffy'
|
5
|
+
require 'logger'
|
6
|
+
|
7
|
+
module Gliffy
|
8
|
+
extend self
|
9
|
+
|
10
|
+
# A command line option to the gliffy command line client
|
11
|
+
class Command
|
12
|
+
|
13
|
+
# Global flags
|
14
|
+
GLOBAL_FLAGS = {
|
15
|
+
'-v' => 'be more verbose (overrides config)',
|
16
|
+
}
|
17
|
+
|
18
|
+
# Global access to all configured commands
|
19
|
+
def self.commands
|
20
|
+
@@commands ||= {}
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.aliases
|
24
|
+
@@aliases ||= {}
|
25
|
+
end
|
26
|
+
|
27
|
+
# The name of the command
|
28
|
+
attr_reader :name
|
29
|
+
# a short description
|
30
|
+
attr_reader :description
|
31
|
+
# a usage statement
|
32
|
+
attr_reader :usage
|
33
|
+
|
34
|
+
# Create a new command
|
35
|
+
#
|
36
|
+
# [+name+] the name of the command (should be short, no spaces)
|
37
|
+
# [+description+] short description of the command
|
38
|
+
# [+offline+] true if this command doesn't require a connection to Gliffy
|
39
|
+
# [+usage+] usage statement
|
40
|
+
# [+block+] A block that represents the command itself. This block will take the gliffy handle and the args array as arguments *or* just the arguments if it is an "offline"
|
41
|
+
# command
|
42
|
+
def initialize(name,description,offline,usage,block)
|
43
|
+
@name = name
|
44
|
+
@description = description
|
45
|
+
@block = block
|
46
|
+
@offline = offline
|
47
|
+
@usage = usage ? usage : ""
|
48
|
+
end
|
49
|
+
|
50
|
+
# Runs the command with the given arguments
|
51
|
+
def run(args)
|
52
|
+
if (@offline)
|
53
|
+
@block.call(args)
|
54
|
+
else
|
55
|
+
previous_token = CLIConfig.instance.config[:current_token]
|
56
|
+
handle = Gliffy::Handle.new(CLIConfig.instance.config[:username],previous_token)
|
57
|
+
@block.call(handle,args)
|
58
|
+
CLIConfig.instance.config[:current_token] = handle.current_token
|
59
|
+
CLIConfig.instance.save
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Executes the command line that was given
|
64
|
+
def self.execute(argv)
|
65
|
+
globals = Hash.new
|
66
|
+
command = argv.shift
|
67
|
+
while !command.nil? && (command =~ /^-/) && !argv.empty?
|
68
|
+
globals[command] = true
|
69
|
+
command = argv.shift
|
70
|
+
end
|
71
|
+
Gliffy::Config.config.log_level = Logger::DEBUG if globals['-v']
|
72
|
+
cmd = Gliffy::Command.commands[command.to_sym]
|
73
|
+
if cmd
|
74
|
+
cmd.run argv
|
75
|
+
else
|
76
|
+
puts "Unknown command #{command}"
|
77
|
+
Gliffy::Command.commands[:help].run []
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Represents the configuration for the command line client.
|
83
|
+
# This is a singleton
|
84
|
+
class CLIConfig
|
85
|
+
|
86
|
+
@@instance = nil
|
87
|
+
|
88
|
+
# Access to the singleton
|
89
|
+
def self.instance
|
90
|
+
@@instance = CLIConfig.new if !@@instance
|
91
|
+
@@instance
|
92
|
+
end
|
93
|
+
|
94
|
+
# Returns the config hash
|
95
|
+
attr_reader :config
|
96
|
+
|
97
|
+
# Loads the user's rc file if it exists, creating it if not
|
98
|
+
def load
|
99
|
+
if !File.exist?(@config_file_name)
|
100
|
+
puts "#{@config_file_name} not found. Create? [y/n]"
|
101
|
+
answer = STDIN.gets
|
102
|
+
if answer =~ /^[Yy]/
|
103
|
+
save
|
104
|
+
else
|
105
|
+
puts "Aborting..."
|
106
|
+
return false
|
107
|
+
end
|
108
|
+
end
|
109
|
+
yaml_config = File.open(@config_file_name) { |file| YAML::load(file) }
|
110
|
+
if (yaml_config)
|
111
|
+
@config = yaml_config
|
112
|
+
end
|
113
|
+
@config[:open_url] = default_open_url if !@config[:open_url]
|
114
|
+
@config[:open_image] = default_open_image if !@config[:open_image]
|
115
|
+
read_config_from_user('API Key',:api_key)
|
116
|
+
read_config_from_user('Secret Key',:secret_key)
|
117
|
+
read_config_from_user('Account Name',:account_name)
|
118
|
+
read_config_from_user('Username',:username)
|
119
|
+
save
|
120
|
+
config = Gliffy::Config.config
|
121
|
+
@config.each() do |key,value|
|
122
|
+
method = key.to_s + "="
|
123
|
+
if config.respond_to? method.to_sym
|
124
|
+
if (method == 'log_device=')
|
125
|
+
if (value == 'STDERR')
|
126
|
+
config.log_device = STDERR
|
127
|
+
elsif (value == 'STDOUT')
|
128
|
+
config.log_device = STDOUT
|
129
|
+
else
|
130
|
+
config.log_device = value
|
131
|
+
end
|
132
|
+
else
|
133
|
+
config.send(method.to_sym,value)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# Saves the configration to the user's config file name as YAML
|
140
|
+
def save
|
141
|
+
fp = File.open(@config_file_name,'w') { |out| YAML::dump(@config,out) }
|
142
|
+
end
|
143
|
+
|
144
|
+
private
|
145
|
+
def read_config_from_user(name,symbol)
|
146
|
+
if (!@config[symbol])
|
147
|
+
puts "No #{name} configured. Enter #{name}"
|
148
|
+
@config[symbol] = STDIN.gets.chomp!
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def initialize
|
153
|
+
@config_file_name = File.expand_path("~/.gliffyrc")
|
154
|
+
@config = {
|
155
|
+
:log_level => Gliffy::Config.config.log_level,
|
156
|
+
:log_device => Gliffy::Config.config.log_device.to_s,
|
157
|
+
:gliffy_app_root => Gliffy::Config.config.gliffy_app_root,
|
158
|
+
:gliffy_rest_context => Gliffy::Config.config.gliffy_rest_context,
|
159
|
+
:protocol => Gliffy::Config.config.protocol,
|
160
|
+
}
|
161
|
+
end
|
162
|
+
|
163
|
+
def default_open_url
|
164
|
+
# Cheesy defaults
|
165
|
+
if RUBY_PLATFORM =~ /win32/
|
166
|
+
# not sure, acutally
|
167
|
+
nil
|
168
|
+
elsif RUBY_PLATFORM =~ /linux/
|
169
|
+
'firefox "%s"'
|
170
|
+
elsif RUBY_PLATFORM =~ /darwin/
|
171
|
+
'open "%s"'
|
172
|
+
else
|
173
|
+
nil
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def default_open_image
|
178
|
+
# Cheesy defaults
|
179
|
+
if RUBY_PLATFORM =~ /win32/
|
180
|
+
# not sure, acutally
|
181
|
+
nil
|
182
|
+
elsif RUBY_PLATFORM =~ /linux/
|
183
|
+
# not sure
|
184
|
+
nil
|
185
|
+
elsif RUBY_PLATFORM =~ /darwin/
|
186
|
+
'open "%s"'
|
187
|
+
else
|
188
|
+
nil
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
|
193
|
+
end
|
194
|
+
|
195
|
+
# For defining commands, this specifies the description of the next command defined
|
196
|
+
def desc(description)
|
197
|
+
@next_desc = description
|
198
|
+
end
|
199
|
+
|
200
|
+
# For defining commands, this specifies if the next defined command is "offline"
|
201
|
+
def offline(bool)
|
202
|
+
@next_offline = bool
|
203
|
+
end
|
204
|
+
|
205
|
+
# For defining commands, specifies the usage statement of the next command
|
206
|
+
def usage(usage='')
|
207
|
+
@next_usage = usage
|
208
|
+
end
|
209
|
+
|
210
|
+
# defines a command. The only options supported is ":aliases" which is an array of aliases
|
211
|
+
# for the command
|
212
|
+
def command(name,options={},&block)
|
213
|
+
Command.commands[name] = Command.new(name,@next_desc,@next_offline,@next_usage,block)
|
214
|
+
if options[:aliases]
|
215
|
+
options[:aliases].each() do |a|
|
216
|
+
Command.commands[a] = Command.commands[name]
|
217
|
+
Command.aliases[a] = true
|
218
|
+
end
|
219
|
+
end
|
220
|
+
@next_usage = nil
|
221
|
+
@next_offline = false
|
222
|
+
@next_desc = nil
|
223
|
+
end
|
224
|
+
|
225
|
+
def parse_options(args)
|
226
|
+
options = Hash.new
|
227
|
+
return options if !args
|
228
|
+
i = 0
|
229
|
+
while i < args.length
|
230
|
+
inc = 2
|
231
|
+
if args[i] =~ /^-/
|
232
|
+
arg = args[i].gsub(/^-/,'')
|
233
|
+
if !args[i+1] || (args[i+1] =~ /^-/)
|
234
|
+
inc = 1
|
235
|
+
options[arg] = true
|
236
|
+
else
|
237
|
+
options[arg] = args[i+1]
|
238
|
+
end
|
239
|
+
else
|
240
|
+
break
|
241
|
+
end
|
242
|
+
i += inc
|
243
|
+
end
|
244
|
+
i.times { args.shift }
|
245
|
+
options
|
246
|
+
end
|
247
|
+
|
248
|
+
def format_date(date)
|
249
|
+
date.strftime('%m/%d/%y %H:%M')
|
250
|
+
end
|
251
|
+
|
252
|
+
end
|
253
|
+
|
254
|
+
include Gliffy
|
255
|
+
require 'gliffy/commands/commands'
|
256
|
+
if !CLIConfig.instance.load
|
257
|
+
exit -1
|
258
|
+
end
|
259
|
+
|
260
|
+
|
@@ -0,0 +1,149 @@
|
|
1
|
+
desc 'List all diagrams in the account'
|
2
|
+
usage <<eos
|
3
|
+
[-l]
|
4
|
+
|
5
|
+
-l - show all information
|
6
|
+
eos
|
7
|
+
command :list, :aliases => [:ls] do |gliffy,args|
|
8
|
+
diagrams = gliffy.get_diagrams
|
9
|
+
options = parse_options(args)
|
10
|
+
if options['l']
|
11
|
+
max = diagrams.inject(0) { |max,diagram| diagram.name.length > max ? diagram.name.length : max }
|
12
|
+
diagrams.sort.each do |diagram|
|
13
|
+
printf "%8d %s %-#{max}s %-3d %s %s %s\n",
|
14
|
+
diagram.id,
|
15
|
+
diagram.is_public? ? "P" : "-",
|
16
|
+
diagram.name,
|
17
|
+
diagram.num_versions,
|
18
|
+
format_date(diagram.create_date),
|
19
|
+
format_date(diagram.mod_date),
|
20
|
+
diagram.owner_username
|
21
|
+
end
|
22
|
+
else
|
23
|
+
printf_string = "%d %s\n"
|
24
|
+
diagrams.sort.each { |diagram| printf printf_string,diagram.id,diagram.name }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
desc 'Delete a diagram'
|
29
|
+
usage 'diagram_id'
|
30
|
+
command :delete, :aliases => [:del,:rm] do |gliffy,args|
|
31
|
+
gliffy.delete_diagram(args[0])
|
32
|
+
end
|
33
|
+
|
34
|
+
desc 'Get the URL for an image'
|
35
|
+
usage <<eos
|
36
|
+
[-o] diagram_id
|
37
|
+
|
38
|
+
-o - open diagram's URL with configured :open_url command
|
39
|
+
eos
|
40
|
+
command :url do |gliffy,args|
|
41
|
+
open = args[0] == '-o'
|
42
|
+
args.shift if open
|
43
|
+
url = gliffy.get_diagram_as_url(args[0])
|
44
|
+
if open
|
45
|
+
if CLIConfig.instance.config[:open_image]
|
46
|
+
system(sprintf(CLIConfig.instance.config[:open_image],url))
|
47
|
+
else
|
48
|
+
puts "Nothing configured for #{:open_image.to_s} to open the image"
|
49
|
+
puts url
|
50
|
+
end
|
51
|
+
else
|
52
|
+
puts url
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
desc 'Download a diagram as an image to a file'
|
57
|
+
usage <<eos
|
58
|
+
[-v version_num] [-f filename] [-t image_type] diagram_id
|
59
|
+
|
60
|
+
image_type can be :jpeg, :png, :svg, or :xml
|
61
|
+
eos
|
62
|
+
command :get do |gliffy,args|
|
63
|
+
|
64
|
+
options = parse_options(args)
|
65
|
+
diagram_id = args.shift
|
66
|
+
|
67
|
+
version_number = options['v'].to_i if options['v']
|
68
|
+
filename = options['f'] if options['f']
|
69
|
+
type = options['t'].to_sym if options['t']
|
70
|
+
type = :jpeg if !type
|
71
|
+
if !filename
|
72
|
+
if version_number
|
73
|
+
filename = "#{diagram_id}_v#{version_number}.#{type.to_s}"
|
74
|
+
else
|
75
|
+
filename = "#{diagram_id}.#{type.to_s}"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
get_options = { :mime_type => type, :file => filename }
|
80
|
+
get_options[:version] = version_number if version_number
|
81
|
+
gliffy.get_diagram_as_image(diagram_id,get_options)
|
82
|
+
puts filename
|
83
|
+
end
|
84
|
+
|
85
|
+
desc 'Edit a diagram'
|
86
|
+
usage 'diagram_id'
|
87
|
+
command :edit do |gliffy,args|
|
88
|
+
return_link = gliffy.get_diagram_as_url(args[0])
|
89
|
+
link = gliffy.get_edit_diagram_link(args[0],return_link,"Done")
|
90
|
+
if CLIConfig.instance.config[:open_url]
|
91
|
+
system(sprintf(CLIConfig.instance.config[:open_url],link.full_url))
|
92
|
+
else
|
93
|
+
puts "Nothing configured for #{:open_url.to_s} to open the url"
|
94
|
+
puts link.full_url
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
desc 'Create a new diagram'
|
99
|
+
usage <<eos
|
100
|
+
[-e] diagram_name
|
101
|
+
|
102
|
+
-e edit the diagram after creating it
|
103
|
+
eos
|
104
|
+
command :new do |gliffy,args|
|
105
|
+
edit = args[0] == '-e'
|
106
|
+
args.shift if edit
|
107
|
+
diagram = gliffy.create_diagram(args[0])
|
108
|
+
if edit
|
109
|
+
Gliffy::Command.commands[:edit].run [diagram.id]
|
110
|
+
else
|
111
|
+
puts "#{diagram.name} created with id #{diagram.id}"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
desc 'Show commands'
|
116
|
+
offline true
|
117
|
+
usage 'command'
|
118
|
+
command :help do |args|
|
119
|
+
if args[0]
|
120
|
+
command = Command.commands[args[0].to_sym]
|
121
|
+
if command
|
122
|
+
printf "%s - %s\n",args[0],command.description
|
123
|
+
printf "usage: %s %s\n",args[0],command.usage
|
124
|
+
else
|
125
|
+
puts "No such command #{args[0]}"
|
126
|
+
end
|
127
|
+
else
|
128
|
+
command_string = " %-6s %s %s\n"
|
129
|
+
puts 'usage: gliffy [global_options] command [command_options]'
|
130
|
+
puts 'global_options:'
|
131
|
+
Command::GLOBAL_FLAGS.keys.sort.each do |flag|
|
132
|
+
printf command_string,flag,Command::GLOBAL_FLAGS[flag],''
|
133
|
+
end
|
134
|
+
puts
|
135
|
+
puts 'command:'
|
136
|
+
command_names = Command.commands.keys.sort { |a,b| a.to_s <=> b.to_s }
|
137
|
+
command_names.each() do |name|
|
138
|
+
command = Command.commands[name]
|
139
|
+
if !Command.aliases[name]
|
140
|
+
aliases = Array.new
|
141
|
+
Command.aliases.keys.each() do |a|
|
142
|
+
aliases << a if Command.commands[a] == command
|
143
|
+
end
|
144
|
+
alias_string = '(also: ' + aliases.join(',') + ')' if aliases.length > 0
|
145
|
+
printf command_string,name.to_s, command.description,alias_string
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|