syclink 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e589b7afc92a5b81224d34df04f9aa8de84d4e66
4
- data.tar.gz: e50dbfae41eef5d476fb3e9c95b8505401d49c4a
3
+ metadata.gz: 757f2615ff72bd3c25ef404172faa9308e2a0c7f
4
+ data.tar.gz: a0a0ccac857393f6a4beae8a50a0f356e5faf578
5
5
  SHA512:
6
- metadata.gz: 7cdd7ddbcd51e14acacb585bdbfe4f6760c970d63de348200194d5fd222ebe78e6a816d59bdea44db064119ee879f0354b7656099be74f3bfdaedf0a4f78e548
7
- data.tar.gz: 4fd0b2f5036a5884a8cb8fdd3991d6058c044e04b828957aa89b43ea594c68bdd3823fa31a65d9b4a15f98756e6fc22a99e9f210bec092ae3e3c21a63205586c
6
+ metadata.gz: 737469dcc6463c9e13dc21b8cc310c93206bdd18f3a8a9a6ecaf5e164930a48291c4d42ecab3bbee81cc0e7a854cb6a66e27034cf73d14df068b4a16eb9ab48f
7
+ data.tar.gz: 0dadc454dcba9d28a8b4123a680a109dc860b216274e3800ce6c94259b3f088f3b86e1f6a3e36fb415a8980f2248568b0fbc6607cbdb6af119aaa69a3cd74fe5
data/Gemfile.lock CHANGED
@@ -1,8 +1,9 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- syclink (0.0.1)
4
+ syclink (0.0.2)
5
5
  gli (= 2.13.1)
6
+ sqlite3 (= 1.3.10)
6
7
 
7
8
  GEM
8
9
  remote: https://rubygems.org/
@@ -23,11 +24,12 @@ GEM
23
24
  rspec-support (~> 3.3.0)
24
25
  rspec-support (3.3.0)
25
26
  sass (3.4.15)
27
+ sqlite3 (1.3.10)
26
28
 
27
29
  PLATFORMS
28
30
  ruby
29
31
 
30
32
  DEPENDENCIES
31
- rspec
32
- sass
33
+ rspec (= 3.3.0)
34
+ sass (= 3.4.15)
33
35
  syclink!
data/README.md CHANGED
@@ -27,14 +27,29 @@ _Website commands_
27
27
  * website show - show all websites or search for websites
28
28
  * website remove - Remove website
29
29
  * website create - Create a HTML representation of the website
30
+ * website check - Check from links that are not active anymore (pending)
31
+ * website edit - Open the YAML file for editing (pending)
30
32
 
31
33
  _Link commands_
32
- * add - Add a link
33
- * file - Add links from a file
34
- * update - Update a link
35
- * delete - Delete a link
36
- * list - List all links with an optional filter
37
- * find - Find links based on a search string
34
+ * add link - Add a link
35
+ * add file - Add links from a file
36
+ * update link - Update a link
37
+ * update file - Update links saved to a file
38
+ * delete - Delete one or more links
39
+ * list - List all links with an optional filter
40
+ * find - Find links based on a search string
41
+ * merge - Merge multiple links with same URL
42
+
43
+ _Import commands_
44
+ * import mf - Import Mozilla Firefox bookmarks
45
+ * import gc - Import Google Chrome bookmarks
46
+ * import ie - Import Internet Explorer bookmarks
47
+ * import dir - Import links to files from a directory
48
+
49
+ _Export commands_
50
+ * export xml - Export links in XML fromat (pending)
51
+ * export json - Export links in JSON format (pending)
52
+ * export csv - Export links in csv format
38
53
 
39
54
  Commands
40
55
  ========
@@ -189,3 +204,66 @@ Following is showing the above sequence in commands
189
204
  $ syclink add "http://github.com" --tag DEVELOPMENT
190
205
  $ syclink website create
191
206
 
207
+ Importing Bookmarks from Webrowsers
208
+ ===================================
209
+
210
+ Firefox
211
+ -------
212
+ The bookmarks of _Firefox_ are located in the user's home folder in
213
+ `~/.mozilla/SOME_CRYPTIC_NAME.default/places.sqlite`.
214
+
215
+ The database can be explored with _sqlite3_ from the command line
216
+
217
+ $ cd ~/.mozilla/SOME_CRYPTIC_NAME.default/
218
+ $ sqlite3 places.sqlite
219
+ >
220
+
221
+ We want to retrieve url, title, description, tag, key and bookmark. tag, key
222
+ and bookmark are good candidates for tags for the application.
223
+
224
+ At the command prompt of SQLite3 We can issue the query
225
+
226
+ ````
227
+ sqlite> select p.id, p.url, p.title, b.id, b.fk, b.parent, b.title,
228
+ ...> k.keyword, a.content, b_t.title from moz_bookmarks b
229
+ ...> left outer join moz_keywords k on b.keyword_id = k.id
230
+ ...> left outer join moz_items_annos a on a.item_id = b.id
231
+ ...> left outer join moz_bookmarks b_t on b.parent = b_t.id
232
+ ...> join moz_places p on p.id = b.fk where p.url like "http%";
233
+ 1|https://www.mozilla.org/en-US/firefox/central/||6|1|3|Getting Started|||\
234
+ Bookmarks Toolbar
235
+ 2|http://www.ubuntu.com/||8|2|7|Ubuntu|||Ubuntu and Free Software links
236
+ 3|http://wiki.ubuntu.com/||9|3|7|Ubuntu Wiki (community-edited website)|||\
237
+ Ubuntu and Free Software links
238
+ 4|https://answers.launchpad.net/ubuntu/+addquestion||10|4|7|Make a Support \
239
+ Request to the Ubuntu Community|||Ubuntu and Free Software links
240
+ 5|http://www.debian.org/||11|5|7|Debian (Ubuntu is based on Debian)|||Ubuntu \
241
+ and Free Software links
242
+ 6|https://one.ubuntu.com/||12|6|7|Ubuntu One - The personal cloud that brings\
243
+ your digital life together|||Ubuntu and Free Software links
244
+ 7|https://www.mozilla.org/en-US/firefox/help/||14|7|13|Help and Tutorials|||\
245
+ Mozilla Firefox
246
+ 8|https://www.mozilla.org/en-US/firefox/customize/||15|8|13|Customize \
247
+ Firefox|||Mozilla Firefox
248
+ 9|https://www.mozilla.org/en-US/contribute/||16|9|13|Get Involved|||Mozilla \
249
+ Firefox
250
+ 10|https://www.mozilla.org/en-US/about/||17|10|13|About Us|||Mozilla Firefox
251
+ 4717|http://codekata.com/|CodeKata|30|4717|5|CodeKata|liklo|How do you get \
252
+ to be a great musician? It helps to know the theory,
253
+ and to understand the mechanics of your instrument. It helps to have
254
+ talent. But …|Unsorted Bookmarks
255
+ 399|http://localhost:3000/|Secondhand | Home|34|399|2|Secondhand | Home|||\
256
+ Bookmarks Menu
257
+ 12870|https://www.sqlite.org/cli.html|Command Line Shell For SQLite|35|12870|\
258
+ 5|Command Line Shell For SQLite|dark|What is this sqlite all about?|Unsorted \
259
+ Bookmarks
260
+ 4717|http://codekata.com/|CodeKata|37|4717|36||||wenga
261
+ 12870|https://www.sqlite.org/cli.html|Command Line Shell For SQLite|39|12870|\
262
+ 38||||lite
263
+ 12883|http://ruby.bastardsbook.com/chapters/sql/#h-2-5|SQL | The Bastards \
264
+ Book of Ruby|40|12883|5|SQL | The Bastards Book of Ruby|||Unsorted Bookmarks
265
+ 12883|http://ruby.bastardsbook.com/chapters/sql/#h-2-5|SQL | The Bastards \
266
+ Book of Ruby|42|12883|41||||Ruby
267
+ sqlite>
268
+ ````
269
+
data/bin/syclink CHANGED
@@ -14,7 +14,8 @@ include SycLink::Infrastructure
14
14
  include SycLink::Formatter
15
15
 
16
16
  # Commands that need to have a website and a designer object
17
- WEBSITE_COMMANDS = [ :create, :add, :file, :update, :delete, :list, :find ]
17
+ WEBSITE_COMMANDS = [ :create, :link, :file, :update, :delete, :list, :find,
18
+ :dir, :mf, :gc, :ie, :csv, :merge ]
18
19
 
19
20
  # syclink's configuration directory
20
21
  syclink_directory = File.expand_path("~/.syc/syclink")
@@ -54,7 +55,7 @@ copy_file_if_missing(File.join(File.dirname(__FILE__),
54
55
 
55
56
  config = load_config(syclink_file)
56
57
 
57
- program_desc 'Create a link list and display it as an html page'
58
+ program_desc 'Create a link-list and display it as an html page'
58
59
 
59
60
  version SycLink::VERSION
60
61
 
@@ -65,63 +66,89 @@ desc 'Website to operate on'
65
66
  arg_name 'WEBSITE'
66
67
  flag [:w,:website]
67
68
 
68
- desc 'Add a link to the website'
69
- arg_name 'URL'
69
+ desc 'Add a link from command line or links from a file to the website'
70
70
  command :add do |c|
71
71
 
72
- c.desc 'Name of the link'
73
- c.arg_name 'NAME'
74
- c.flag [:n, :name]
72
+ c.desc 'Add a link from the command line to the website'
73
+ c.arg_name 'URL'
74
+ c.command :link do |s|
75
+ s.desc 'Name of the link'
76
+ s.arg_name 'NAME'
77
+ s.flag [:n, :name]
75
78
 
76
- c.desc 'Description of the link'
77
- c.arg_name 'DESCRIPTION'
78
- c.flag [:d, :description]
79
+ s.desc 'Description of the link'
80
+ s.arg_name 'DESCRIPTION'
81
+ s.flag [:d, :description]
79
82
 
80
- c.desc 'Tag the link is associated to'
81
- c.arg_name 'TAG'
82
- c.flag [:t, :tag]
83
+ s.desc 'Tag the link is associated to'
84
+ s.arg_name 'TAG'
85
+ s.flag [:t, :tag]
83
86
 
84
- c.action do |global_options,options,args|
87
+ s.action do |global_options,options,args|
85
88
 
86
- @designer.add_link(args[0], options)
89
+ @designer.add_link(args[0], options)
87
90
 
91
+ end
88
92
  end
89
- end
90
93
 
91
- desc 'Add links from a file to the website'
92
- arg_name 'FILE'
93
- command :file do |c|
94
+ c.desc 'Add links from a file to the website'
95
+ c.arg_name 'FILE'
96
+ c.command :file do |s|
94
97
 
95
- c.action do |global_options,options,args|
98
+ s.action do |global_options,options,args|
96
99
 
97
- @designer.add_links_from_file(args[0])
100
+ @designer.add_links_from_file(args[0])
98
101
 
102
+ end
99
103
  end
100
104
  end
101
105
 
102
- desc 'Update a link'
103
- arg_name 'URL'
106
+ desc 'Update a link from command line or links from file'
104
107
  command :update do |c|
105
108
 
106
- c.desc 'Name of the link'
107
- c.arg_name 'NAME'
108
- c.flag [:n, :name]
109
+ c.desc 'Update a link from the command line'
110
+ c.arg_name 'URL'
111
+ c.command :link do |s|
109
112
 
110
- c.desc 'Description of the link'
111
- c.arg_name 'DESCRIPTION'
112
- c.flag [:d, :description]
113
+ s.desc 'Name of the link'
114
+ s.arg_name 'NAME'
115
+ s.flag [:n, :name]
113
116
 
114
- c.desc 'Tag the link is associated to'
115
- c.arg_name 'TAG'
116
- c.flag [:t, :tag]
117
+ s.desc 'Description of the link'
118
+ s.arg_name 'DESCRIPTION'
119
+ s.flag [:d, :description]
117
120
 
118
- c.action do |global_options,options,args|
121
+ s.desc 'Tag the link is associated to'
122
+ s.arg_name 'TAG'
123
+ s.flag [:t, :tag]
124
+
125
+ s.action do |global_options,options,args|
119
126
 
120
- @designer.update_link(args[0], options)
127
+ @designer.update_link(args[0], options)
128
+
129
+ end
130
+ end
131
+
132
+ c.desc "Update links by reading from file"
133
+ c.arg_name 'FILE'
134
+ c.command :file do |s|
135
+
136
+ s.action do |global_options,options,args|
137
+
138
+ @designer.update_links_from_file(args[0])
139
+
140
+ end
121
141
 
122
142
  end
123
143
  end
124
144
 
145
+ desc 'Merge links with same URL'
146
+ command :merge do |c|
147
+ c.action do |global_options,options,args|
148
+ @designer.merge_links
149
+ end
150
+ end
151
+
125
152
  desc 'Find a link'
126
153
  arg_name 'FIND_STRING'
127
154
  command :find do |c|
@@ -131,17 +158,31 @@ command :find do |c|
131
158
  c.arg_name 'URL, NAME, DESCRIPTION, TAG'
132
159
  c.flag [:c, :columns]
133
160
 
161
+ c.desc 'Table width'
162
+ c.arg_name 'WIDTH'
163
+ c.flag [:w, :width], :type => Integer
164
+
165
+ c.desc 'Expand table to full width of specified WIDTH'
166
+ c.default_value true
167
+ c.switch [:e, :expand]
168
+
134
169
  c.action do |global_options,options,args|
135
170
 
136
- print_links(@designer.find_links(args[0].downcase), options[:c])
171
+ if args[0].nil?
172
+ STDERR.puts "Warning: You need to specify a FIND_STRING"
173
+ STDERR.puts "If you want to list all links use 'syclink list'"
174
+ else
175
+ print_links(@designer.find_links(args[0]), options[:c], options)
176
+ end
137
177
 
138
178
  end
139
179
  end
140
180
 
141
181
  desc 'Remove one or more links'
142
- arg_name 'URL[URL,URL]'
182
+ arg_name 'URL [URL URL]'
143
183
  command :delete do |c|
144
184
  c.action do |global_options,options,args|
185
+ p args
145
186
  @designer.remove_links(args)
146
187
  end
147
188
  end
@@ -170,9 +211,17 @@ command :list do |c|
170
211
  c.arg_name 'URL, NAME, DESCRIPTION, TAG'
171
212
  c.flag [:c, :columns]
172
213
 
214
+ c.desc 'Table width'
215
+ c.arg_name 'WIDTH'
216
+ c.flag [:w, :width], :type => Integer
217
+
218
+ c.desc 'Expand table to full width of specified WIDTH'
219
+ c.default_value true
220
+ c.switch [:e, :expand]
221
+
173
222
  c.action do |global_options,options,args|
174
223
 
175
- print_links(@designer.list_links(options), options[:c])
224
+ print_links(@designer.list_links(options), options[:c], options)
176
225
 
177
226
  end
178
227
  end
@@ -232,6 +281,107 @@ command :website do |c|
232
281
  c.default_command :create
233
282
  end
234
283
 
284
+ desc "Import links from Firefox, Chrome, Internet Explorer or directory"
285
+ command :import do |c|
286
+
287
+ c.desc 'Import links from Mozilla Firefox'
288
+ c.arg_name 'PATH_TO_FIREFOX_DATABASE'
289
+ c.command :mf do |s|
290
+
291
+ s.action do |global_options,options,args|
292
+ unless File.exists? args[0]
293
+ STDERR.puts <<-HERE.gsub(/^ {10}/, '')
294
+ Error: #{args[0]} doesn't exist!
295
+ Firefox stores its bookmarks in a SQLite3 database called
296
+ places.sqlite. With Ubuntu this database is usually located in
297
+ '~/.mozilla/firefox/*.default/places.sqlite'.
298
+ If you are on Windows
299
+ the file is located in the user's home directory
300
+ '~/AppData/Roaming/Mozilla/Profiles/*.default/places.sqlite'.
301
+ HERE
302
+ exit(0)
303
+ else
304
+ @designer.import_links(SycLink::Firefox.new(args.shift))
305
+ end
306
+ end
307
+ end
308
+
309
+ c.desc 'Import links from Google Chrome'
310
+ c.arg_name 'PATH_TO_CHROME_BOOKMARKS'
311
+ c.command :gc do |s|
312
+
313
+ s.action do |global_options,options,args|
314
+ unless File.exists? args[0]
315
+ STDERR.puts <<-HERE.gsub(/^ {10}/, '')
316
+ Error: #{args[0]} doesn't exist!
317
+ Google Chrome stores its bookmarks in a JSON file called
318
+ Bookmarks. With Ubuntu this file is usually located in
319
+ '~/.config/chromium/Default/Bookmarks'.
320
+ If you are on Windows
321
+ the file is located in the user's home directory
322
+ '~/AppData/Local/Google/Chrome/User Data/Bookmarks'.
323
+ HERE
324
+ exit(0)
325
+ else
326
+ @designer.import_links(SycLink::Chrome.new(args.shift))
327
+ end
328
+ end
329
+ end
330
+
331
+ c.desc 'Import links from Internet Explorer'
332
+ c.arg_name 'PATH_TO_INTERNET_EXPLORER_BOOKMARKS'
333
+ c.command :ie do |s|
334
+
335
+ s.action do |global_options,options,args|
336
+ unless File.exists? args[0]
337
+ STDERR.puts <<-HERE.gsub(/^ {10}/, '')
338
+ Error: #{args[0]} doesn't exist!
339
+ Internet Explorer stores its bookmarks in a directory structure.
340
+ the bookmarks are located in the user's home directory
341
+ '~/AppData/Favorites'.
342
+ HERE
343
+ exit(0)
344
+ else
345
+ @designer.import_links(SycLink::InternetExplorer.new(args.shift))
346
+ end
347
+ end
348
+ end
349
+
350
+ c.desc 'Import links from a Directory'
351
+ c.long_desc <<-HERE.gsub(/^ {4}/, '')
352
+ The PATH_TO_DIRECTORY can have patterns that allows to import specific
353
+ files.
354
+
355
+ Examples:
356
+
357
+ PATH_TO_DIRECTORY/**/*.pdf will import all pdf-files in the directories and
358
+ sub-directory
359
+
360
+ PATH_TO_DIRECTORY/**/* will import all files and sub-directories
361
+ HERE
362
+
363
+ c.arg_name 'PATH_TO_DIRECTORY'
364
+ c.command :dir do |s|
365
+
366
+ s.action do |global_options,options,args|
367
+ @designer.import_links(SycLink::FileImporter.new(args.shift))
368
+ end
369
+ end
370
+ end
371
+
372
+ desc 'Export to csv'
373
+ command :export do |c|
374
+
375
+ c.desc 'Export links to csv'
376
+ c.command :csv do |s|
377
+
378
+ s.action do |global_options,options,args|
379
+ puts @designer.export(:csv)
380
+ end
381
+ end
382
+
383
+ end
384
+
235
385
  pre do |global,command,options,args|
236
386
 
237
387
  if WEBSITE_COMMANDS.include?(command.name)
@@ -281,10 +431,10 @@ on_error do |exception|
281
431
  end
282
432
  end
283
433
 
284
- def print_links(links, columns)
434
+ def print_links(links, columns, opts = {})
285
435
  allowed_cols = %w{ url name description tag }
286
436
  cols = columns.delete(' ').downcase.split(',') & allowed_cols
287
- SycLink::Formatter.table(links, cols)
437
+ SycLink::Formatter.table(links, cols, opts)
288
438
  end
289
439
 
290
440
  exit run(ARGV)
@@ -0,0 +1,37 @@
1
+ require "json"
2
+
3
+ # Module that creates a link list and generates an html representation
4
+ module SycLink
5
+
6
+ # Importer for Google Chrome links
7
+ class Chrome < Importer
8
+
9
+ # Reads the content of the Google Chrome bookmarks file
10
+ def read
11
+ serialized = File.read(path)
12
+ extract_links(JSON.parse(serialized)).flatten.each_slice(4).to_a
13
+ end
14
+
15
+ private
16
+
17
+ # Extracts the links from the JSON file
18
+ def extract_links(json)
19
+ json["roots"].collect do |key, children|
20
+ extract_children(children["name"], children["children"])
21
+ end
22
+ end
23
+
24
+ # Extracts the children from the JSON file
25
+ def extract_children(tag, children)
26
+ children.map do |child|
27
+ if child["children"]
28
+ extract_children("#{tag},#{child['name']}", child["children"])
29
+ else
30
+ [child["url"], child["name"], "", tag]
31
+ end
32
+ end
33
+ end
34
+
35
+ end
36
+
37
+ end
@@ -29,13 +29,31 @@ module SycLink
29
29
  # are added to the websie
30
30
  def add_links_from_file(file)
31
31
  File.foreach(file) do |line|
32
- url, name, description, tag = line.split(';')
32
+ next if line.chomp.empty?
33
+ url, name, description, tag = line.chomp.split(';')
33
34
  website.add_link(Link.new(url, { name: name,
34
35
  description: description,
35
36
  tag: tag }))
36
37
  end
37
38
  end
38
39
 
40
+ # Accepts and SycLink::Importer to import Links and add them to the website
41
+ def import_links(importer)
42
+ importer.links.each do |link|
43
+ website.add_link(link)
44
+ end
45
+ end
46
+
47
+ # Export links to specified format
48
+ def export(format)
49
+ message = "to_#{format.downcase}"
50
+ if website.respond_to? message
51
+ website.send(message)
52
+ else
53
+ raise "cannot export to #{format}"
54
+ end
55
+ end
56
+
39
57
  # List links contained in the website and optionally filter on attributes
40
58
  def list_links(args = {})
41
59
  website.list_links(args)
@@ -52,6 +70,21 @@ module SycLink
52
70
  website.find_links(url).first.update(args)
53
71
  end
54
72
 
73
+ def update_links_from_file(file)
74
+ File.foreach(file) do |line|
75
+ next if line.chomp.empty?
76
+ url, name, description, tag = line.chomp.split(';')
77
+ website.find_links(url).first.update({ name: name,
78
+ description: description,
79
+ tag: tag })
80
+ end
81
+ end
82
+
83
+ # Merge links with same URL
84
+ def merge_links
85
+ website.merge_links_on(:url)
86
+ end
87
+
55
88
  # Deletes one or more links from the website. Returns the deleted links.
56
89
  # Expects the links provided in an array
57
90
  def remove_links(urls)
@@ -12,6 +12,12 @@ module SycLink
12
12
  renderer.result(binding)
13
13
  end
14
14
 
15
+ # Takes an array of row values and converts them to a csv string. Expects
16
+ # that the importing class is having a method rows.
17
+ def to_csv
18
+ rows.map { |row| row.join(';') }.join("\n")
19
+ end
20
+
15
21
  end
16
22
 
17
23
  end
@@ -0,0 +1,19 @@
1
+ module SycLink
2
+
3
+ class FileImporter < Importer
4
+
5
+ def read
6
+ root_dir = File.dirname(path).scan(/^[^\*|\?]*/).first
7
+ regex = Regexp.new("(?<=#{root_dir}).*")
8
+ Dir.glob(path).map do |file|
9
+ url = file
10
+ name = File.basename(file)
11
+ description = ""
12
+ tags = extract_tags(File.dirname(file).scan(regex))
13
+ [url, name, description, tags]
14
+ end
15
+ end
16
+
17
+ end
18
+
19
+ end
@@ -0,0 +1,36 @@
1
+ require 'sqlite3'
2
+ require_relative 'importer'
3
+
4
+ # Module that creates a link list and generates an html representation
5
+ module SycLink
6
+
7
+ # Importer for Firefox links
8
+ class Firefox < Importer
9
+
10
+ # Query strig to read links from the Firefox database places.sqlite
11
+ QUERY_STRING = "select p.url, p.title, b.title, a.content, k.keyword, b_t.title from moz_bookmarks b left outer join moz_keywords k on b.keyword_id = k.id left outer join moz_items_annos a on a.item_id = b.id left outer join moz_bookmarks b_t on b.parent = b_t.id join moz_places p on p.id = b.fk where p.url like 'http%';"
12
+
13
+ # Reads the links from the Firefox database places.sqlite
14
+ def read
15
+ bookmark_file = Dir.glob(File.expand_path(path)).shift
16
+ raise "Did not find file #{path}" unless bookmark_file
17
+
18
+ db = SQLite3::Database.new(path)
19
+
20
+ import = db.execute(QUERY_STRING)
21
+ end
22
+
23
+ # Returns row values in Arrays
24
+ def rows
25
+ read.map do |row|
26
+ a = row[0]; b = row[1]; c = row[2]; d = row[3]; e = row[4]; f = row[5]
27
+ [a,
28
+ b || c,
29
+ (d || '').gsub("\n", ' '),
30
+ [e, f].join(',').gsub(/^,|,$/, '')]
31
+ end
32
+ end
33
+
34
+ end
35
+
36
+ end