atom-tools 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- data/COPYING +18 -0
- data/README +103 -0
- data/Rakefile +77 -0
- data/bin/atom-client.rb +246 -0
- data/bin/atom-server.rb~ +71 -0
- data/doc/classes/Atom/App.html +217 -0
- data/doc/classes/Atom/Author.html +130 -0
- data/doc/classes/Atom/Category.html +128 -0
- data/doc/classes/Atom/Collection.html +322 -0
- data/doc/classes/Atom/Content.html +129 -0
- data/doc/classes/Atom/Contributor.html +119 -0
- data/doc/classes/Atom/Element.html +325 -0
- data/doc/classes/Atom/Entry.html +365 -0
- data/doc/classes/Atom/Feed.html +585 -0
- data/doc/classes/Atom/HTTP.html +374 -0
- data/doc/classes/Atom/Link.html +137 -0
- data/doc/classes/Atom/Text.html +229 -0
- data/doc/classes/XHTML.html +118 -0
- data/doc/created.rid +1 -0
- data/doc/files/README.html +213 -0
- data/doc/files/lib/atom/app_rb.html +110 -0
- data/doc/files/lib/atom/collection_rb.html +110 -0
- data/doc/files/lib/atom/element_rb.html +109 -0
- data/doc/files/lib/atom/entry_rb.html +111 -0
- data/doc/files/lib/atom/feed_rb.html +112 -0
- data/doc/files/lib/atom/http_rb.html +109 -0
- data/doc/files/lib/atom/text_rb.html +108 -0
- data/doc/files/lib/atom/xml_rb.html +110 -0
- data/doc/files/lib/atom/yaml_rb.html +109 -0
- data/doc/fr_class_index.html +39 -0
- data/doc/fr_file_index.html +36 -0
- data/doc/fr_method_index.html +62 -0
- data/doc/index.html +24 -0
- data/doc/rdoc-style.css +208 -0
- data/lib/atom/app.rb +87 -0
- data/lib/atom/collection.rb +75 -0
- data/lib/atom/element.rb +277 -0
- data/lib/atom/entry.rb +135 -0
- data/lib/atom/feed.rb +229 -0
- data/lib/atom/http.rb +132 -0
- data/lib/atom/text.rb +163 -0
- data/lib/atom/xml.rb +200 -0
- data/lib/atom/yaml.rb +101 -0
- data/setup.rb +1585 -0
- data/test/conformance/order.rb +117 -0
- data/test/conformance/title.rb +108 -0
- data/test/conformance/updated.rb +33 -0
- data/test/conformance/xhtmlcontentdiv.rb +18 -0
- data/test/conformance/xmlnamespace.rb +54 -0
- data/test/runtests.rb +14 -0
- data/test/test_constructs.rb +91 -0
- data/test/test_feed.rb +128 -0
- data/test/test_general.rb +99 -0
- data/test/test_http.rb +86 -0
- data/test/test_protocol.rb +69 -0
- data/test/test_xml.rb +353 -0
- metadata +107 -0
data/COPYING
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
copyright © 2006 Brendan Taylor
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to
|
5
|
+
deal in the Software without restriction, including without limitation the
|
6
|
+
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
7
|
+
sell copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
16
|
+
THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
17
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
18
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
= atom-tools README
|
2
|
+
|
3
|
+
atom-tools is an all-in-one library for parsing, creating and manipulating Atom <http://www.w3.org/2005/Atom> feeds and entries.
|
4
|
+
It handles all the nasty XML and HTTP details so that you don't have to.
|
5
|
+
|
6
|
+
== Example
|
7
|
+
|
8
|
+
require "atom/feed"
|
9
|
+
require "open-uri"
|
10
|
+
|
11
|
+
feed = Atom::Feed.new "http://www.tbray.org/ongoing/ongoing.atom"
|
12
|
+
# => <http://www.tbray.org/ongoing/ongoing.atom entries: 0 title=''>
|
13
|
+
|
14
|
+
feed.update!
|
15
|
+
feed.entries.length
|
16
|
+
# => 20
|
17
|
+
|
18
|
+
feed.title.to_s
|
19
|
+
# => "ongoing"
|
20
|
+
|
21
|
+
feed.authors.first.name
|
22
|
+
# => "Tim Bray"
|
23
|
+
|
24
|
+
entry = feed.entries.first
|
25
|
+
# => #<Atom::Entry id:'http://www.tbray.org/ongoing/When/200x/2006/11/07/Munich'>
|
26
|
+
|
27
|
+
entry.title.to_s
|
28
|
+
# => "M\303\274nchen"
|
29
|
+
|
30
|
+
entry.links.last
|
31
|
+
# => {"href"=>"http://www.tbray.org/ongoing/When/200x/2006/11/07/Munich#comments", "rel"=>"replies", "type" => "application/xhtml+xml"}
|
32
|
+
|
33
|
+
entry.summary.to_s
|
34
|
+
# => "That local spelling is nicer than the ugly English [...]"
|
35
|
+
|
36
|
+
Things are explained in more detail in the RDoc.
|
37
|
+
|
38
|
+
== The Atom Publishing Protocol
|
39
|
+
|
40
|
+
require "atom/app"
|
41
|
+
|
42
|
+
app = Atom::App.new "http://necronomicorp.com/app.xml"
|
43
|
+
coll = app.collections.first
|
44
|
+
# => <http://necronomicorp.com/testatom?app entries: 0 title='testing: entry endpoint'>
|
45
|
+
|
46
|
+
coll.update!
|
47
|
+
# => <http://necronomicorp.com/testatom?app entries: 10 title='testing the APP'>
|
48
|
+
|
49
|
+
entry = coll.entries.first
|
50
|
+
entry.title
|
51
|
+
# => 'html entities'#text
|
52
|
+
|
53
|
+
# modify the title
|
54
|
+
entry.title = "HTML entities"
|
55
|
+
|
56
|
+
# store the modified entry
|
57
|
+
coll.put! entry
|
58
|
+
# => #<Net::HTTPOK 200 OK readbody=true>
|
59
|
+
|
60
|
+
coll.entries.first.title
|
61
|
+
# => 'HTML entities'#text
|
62
|
+
|
63
|
+
For details on authentication, see the documentation for Atom::HTTP
|
64
|
+
|
65
|
+
== Advanced Use
|
66
|
+
|
67
|
+
=== Extension Elements
|
68
|
+
|
69
|
+
irt = REXML::Element.new("in-reply-to")
|
70
|
+
irt.add_namespace "http://purl.org/syndication/thread/1.0"
|
71
|
+
|
72
|
+
irt.attributes["ref"] = "tag:entries.com,2005:1"
|
73
|
+
|
74
|
+
entry.extensions << irt
|
75
|
+
|
76
|
+
entry.to_s
|
77
|
+
# => '<entry xmlns="http://www.w3.org/2005/Atom"><in-reply-to ref="tag:entries.com,2005:1" xmlns="http://purl.org/syndication/thread/1.0"/></entry>'
|
78
|
+
|
79
|
+
== YAML
|
80
|
+
|
81
|
+
if you feel like writing this stuff by hand, atom-tools can slurp an
|
82
|
+
atom:entry from YAML:
|
83
|
+
|
84
|
+
require "atom/yaml"
|
85
|
+
|
86
|
+
yaml = <<END
|
87
|
+
title: Atom-Drunk Pirates Run Amok!
|
88
|
+
tags: tag1 tag2
|
89
|
+
authors:
|
90
|
+
-
|
91
|
+
name: Brendan Taylor
|
92
|
+
email: whateley@gmail.com
|
93
|
+
-
|
94
|
+
name: Harvey
|
95
|
+
uri: http://fake.com/
|
96
|
+
|
97
|
+
content: |
|
98
|
+
<p>blah blah blah blah</p>
|
99
|
+
|
100
|
+
<p>and so on.</p>
|
101
|
+
END
|
102
|
+
|
103
|
+
entry = Atom::Entry.from_yaml(yaml)
|
data/Rakefile
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
require "rake"
|
2
|
+
require "rake/testtask"
|
3
|
+
require "rake/rdoctask"
|
4
|
+
require "rake/gempackagetask"
|
5
|
+
|
6
|
+
require "rake/clean"
|
7
|
+
|
8
|
+
NAME = "atom-tools"
|
9
|
+
VERS = "0.9.0"
|
10
|
+
|
11
|
+
# the following from markaby-0.5's tools/rakehelp
|
12
|
+
def setup_tests
|
13
|
+
Rake::TestTask.new do |t|
|
14
|
+
t.libs << "test"
|
15
|
+
t.test_files = FileList['test/test*.rb']
|
16
|
+
t.verbose = true
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def setup_rdoc files
|
21
|
+
Rake::RDocTask.new do |rdoc|
|
22
|
+
rdoc.title = NAME + " documentation"
|
23
|
+
rdoc.rdoc_dir = 'doc'
|
24
|
+
rdoc.options << '--line-numbers'
|
25
|
+
rdoc.options << '--inline-source'
|
26
|
+
rdoc.rdoc_files.add(files)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def setup_gem(pkg_name, pkg_version, author, summary, dependencies, test_file)
|
31
|
+
pkg_version = pkg_version
|
32
|
+
pkg_name = pkg_name
|
33
|
+
pkg_file_name = "#{pkg_name}-#{pkg_version}"
|
34
|
+
|
35
|
+
spec = Gem::Specification.new do |s|
|
36
|
+
s.name = pkg_name
|
37
|
+
s.version = pkg_version
|
38
|
+
s.platform = Gem::Platform::RUBY
|
39
|
+
s.author = author
|
40
|
+
s.summary = summary
|
41
|
+
s.test_file = test_file
|
42
|
+
s.has_rdoc = true
|
43
|
+
s.extra_rdoc_files = [ "README" ]
|
44
|
+
dependencies.each do |dep|
|
45
|
+
s.add_dependency(*dep)
|
46
|
+
end
|
47
|
+
s.files = %w(COPYING README Rakefile setup.rb) +
|
48
|
+
Dir.glob("{bin,doc,test,lib}/**/*") +
|
49
|
+
Dir.glob("ext/**/*.{h,c,rb}") +
|
50
|
+
Dir.glob("examples/**/*.rb") +
|
51
|
+
Dir.glob("tools/*.rb")
|
52
|
+
|
53
|
+
s.require_path = "lib"
|
54
|
+
s.extensions = FileList["ext/**/extconf.rb"].to_a
|
55
|
+
|
56
|
+
s.bindir = "bin"
|
57
|
+
end
|
58
|
+
|
59
|
+
Rake::GemPackageTask.new(spec) do |p|
|
60
|
+
p.gem_spec = spec
|
61
|
+
p.need_tar = true
|
62
|
+
end
|
63
|
+
|
64
|
+
task :install do
|
65
|
+
sh %{rake package}
|
66
|
+
sh %{gem install pkg/#{pkg_name}-#{pkg_version}}
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
task :default => [:package]
|
71
|
+
|
72
|
+
setup_tests
|
73
|
+
setup_rdoc ['README', 'lib/**/*.rb']
|
74
|
+
|
75
|
+
summary = "Tools for working with Atom Entries, Feeds and Collections"
|
76
|
+
test_file = "test/runtests.rb"
|
77
|
+
setup_gem(NAME, VERS, "Brendan Taylor", summary, [], test_file)
|
data/bin/atom-client.rb
ADDED
@@ -0,0 +1,246 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
# syntax: ./atom-client.rb <introspection-url> [username] [password]
|
4
|
+
# a
|
5
|
+
|
6
|
+
require "tempfile"
|
7
|
+
|
8
|
+
require "atom/yaml"
|
9
|
+
require "atom/app"
|
10
|
+
require "atom/http"
|
11
|
+
|
12
|
+
require "rubygems"
|
13
|
+
require "bluecloth"
|
14
|
+
|
15
|
+
require "time"
|
16
|
+
|
17
|
+
require 'yaml'
|
18
|
+
|
19
|
+
class Tempfile
|
20
|
+
def edit_externally
|
21
|
+
self.close
|
22
|
+
|
23
|
+
system("#{EDITOR} #{self.path}")
|
24
|
+
|
25
|
+
self.open
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class String
|
30
|
+
def edit_externally
|
31
|
+
tempfile = Tempfile.new("entry")
|
32
|
+
tempfile.puts self
|
33
|
+
|
34
|
+
tempfile.edit_externally
|
35
|
+
|
36
|
+
ret = tempfile.read
|
37
|
+
tempfile.delete
|
38
|
+
|
39
|
+
ret
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class Atom::Entry
|
44
|
+
def prepare_for_output
|
45
|
+
filter_hook
|
46
|
+
|
47
|
+
update!
|
48
|
+
end
|
49
|
+
|
50
|
+
def filter_hook
|
51
|
+
# so much for actual text content...
|
52
|
+
if @content and @content["type"] == "text"
|
53
|
+
self.content = BlueCloth.new( @content.to_s ).to_html
|
54
|
+
@content["type"] = "xhtml"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def edit
|
59
|
+
yaml = YAML.load(self.to_yaml)
|
60
|
+
|
61
|
+
# humans don't care about these things
|
62
|
+
yaml.delete "id"
|
63
|
+
|
64
|
+
if yaml["links"]
|
65
|
+
yaml["links"].delete(yaml["links"].find { |l| l["rel"] == "edit" })
|
66
|
+
yaml["links"].delete(yaml["links"].find { |l| l["rel"] == "alternate" })
|
67
|
+
yaml.delete("links") if yaml["links"].empty?
|
68
|
+
end
|
69
|
+
|
70
|
+
entry = write_entry(yaml.to_yaml)
|
71
|
+
# the id doesn't appear in YAML, it should remain the same
|
72
|
+
entry.id = self.id
|
73
|
+
|
74
|
+
entry
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# maybe this should handle displaying the list too.
|
79
|
+
def choose_from list
|
80
|
+
item = nil
|
81
|
+
itemno = nil
|
82
|
+
|
83
|
+
# oh wow this is pathetic
|
84
|
+
until item
|
85
|
+
if itemno
|
86
|
+
puts "try picking a number on the list."
|
87
|
+
end
|
88
|
+
|
89
|
+
print "? "
|
90
|
+
itemno = $stdin.gets.chomp
|
91
|
+
|
92
|
+
item = list[itemno.to_i]
|
93
|
+
end
|
94
|
+
|
95
|
+
item
|
96
|
+
end
|
97
|
+
|
98
|
+
def choose_collection server
|
99
|
+
puts "which collection?"
|
100
|
+
|
101
|
+
collections = []
|
102
|
+
|
103
|
+
# still lame
|
104
|
+
server.collections.each_with_index do |coll, index|
|
105
|
+
collections << coll
|
106
|
+
|
107
|
+
puts "#{index}: #{coll.title}"
|
108
|
+
end
|
109
|
+
|
110
|
+
choose_from collections
|
111
|
+
end
|
112
|
+
|
113
|
+
def choose_entry_url coll
|
114
|
+
puts "which entry?"
|
115
|
+
|
116
|
+
coll.entries.each_with_index do |entry, index|
|
117
|
+
puts "#{index}: #{entry.title}"
|
118
|
+
end
|
119
|
+
|
120
|
+
entry = choose_from coll.entries
|
121
|
+
|
122
|
+
edit_link = entry.links.find do |link|
|
123
|
+
link["rel"] = "edit"
|
124
|
+
end
|
125
|
+
|
126
|
+
edit_link["href"]
|
127
|
+
end
|
128
|
+
|
129
|
+
def write_entry(editstring = "")
|
130
|
+
begin
|
131
|
+
edited = editstring.edit_externally
|
132
|
+
|
133
|
+
if edited == editstring
|
134
|
+
puts "unchanged content, aborted"
|
135
|
+
exit
|
136
|
+
end
|
137
|
+
|
138
|
+
entry = Atom::Entry.from_yaml edited
|
139
|
+
|
140
|
+
entry.prepare_for_output
|
141
|
+
|
142
|
+
# XXX disabled until the APP WG can decide what a valid entry is
|
143
|
+
=begin
|
144
|
+
valid, message = entry.valid?
|
145
|
+
unless valid
|
146
|
+
print "entry is invalid (#{message}). post anyway? (y/n)? "
|
147
|
+
(gets.chomp == "y") || (raise Atom::InvalidEntry.new)
|
148
|
+
end
|
149
|
+
=end
|
150
|
+
|
151
|
+
# this has to be here ATM to we can detect malformed atom:content
|
152
|
+
puts entry.to_s
|
153
|
+
rescue ArgumentError,REXML::ParseException => e
|
154
|
+
puts e
|
155
|
+
|
156
|
+
puts "press enter to edit again..."
|
157
|
+
$stdin.gets
|
158
|
+
|
159
|
+
editstring = edited
|
160
|
+
|
161
|
+
retry
|
162
|
+
rescue Atom::InvalidEntry
|
163
|
+
editstring = edited
|
164
|
+
retry
|
165
|
+
end
|
166
|
+
|
167
|
+
entry
|
168
|
+
end
|
169
|
+
|
170
|
+
module Atom
|
171
|
+
class InvalidEntry < RuntimeError
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
EDITOR = ENV["EDITOR"] || "env vim"
|
176
|
+
|
177
|
+
# now that i'm supporting -07 the interface has been shittified. apologies.
|
178
|
+
introspection_url = ARGV[0]
|
179
|
+
|
180
|
+
http = Atom::HTTP.new
|
181
|
+
http.user = ARGV[1]
|
182
|
+
http.pass = ARGV[2]
|
183
|
+
|
184
|
+
server = Atom::App.new(introspection_url, http)
|
185
|
+
|
186
|
+
coll = choose_collection server
|
187
|
+
|
188
|
+
# XXX the server should *probably* replace this, but who knows yet?
|
189
|
+
CLIENT_ID = "http://necronomicorp.com/dev/null"
|
190
|
+
|
191
|
+
new = lambda do
|
192
|
+
entry = Atom::Entry.new
|
193
|
+
|
194
|
+
entry.title = ""
|
195
|
+
entry.content = ""
|
196
|
+
|
197
|
+
entry = entry.edit
|
198
|
+
|
199
|
+
entry.id = CLIENT_ID
|
200
|
+
entry.published = Time.now.iso8601
|
201
|
+
|
202
|
+
res = coll.post! entry
|
203
|
+
|
204
|
+
# XXX error recovery here, lost updates suck
|
205
|
+
puts res.body
|
206
|
+
end
|
207
|
+
|
208
|
+
edit = lambda do
|
209
|
+
coll.update!
|
210
|
+
|
211
|
+
coll.entries.each_with_index do |entry,idx|
|
212
|
+
puts "#{idx}: #{entry.title}"
|
213
|
+
end
|
214
|
+
|
215
|
+
entry = choose_from(coll.entries) { |entry| entry.title }
|
216
|
+
|
217
|
+
url = entry.edit_url
|
218
|
+
|
219
|
+
entry = http.get_atom_entry url
|
220
|
+
|
221
|
+
res = coll.put! entry.edit, url
|
222
|
+
|
223
|
+
# XXX error recovery here, lost updates suck
|
224
|
+
puts res.body
|
225
|
+
end
|
226
|
+
|
227
|
+
delete = lambda do
|
228
|
+
coll.update!
|
229
|
+
|
230
|
+
coll.entries.each_with_index do |entry,idx|
|
231
|
+
puts "#{idx} #{entry.title}"
|
232
|
+
end
|
233
|
+
|
234
|
+
entry = choose_from(coll.entries)
|
235
|
+
res = coll.delete! entry
|
236
|
+
|
237
|
+
puts res.body
|
238
|
+
end
|
239
|
+
|
240
|
+
actions = [ new, edit, delete ]
|
241
|
+
|
242
|
+
puts "0: new entry"
|
243
|
+
puts "1: edit entry"
|
244
|
+
puts "2: delete entry"
|
245
|
+
|
246
|
+
choose_from(actions).call
|
data/bin/atom-server.rb~
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
require "atom/pub-server"
|
4
|
+
require "webrick/httpserver"
|
5
|
+
require "atom/feed"
|
6
|
+
|
7
|
+
# syntax: ./atom-server <port> <path>
|
8
|
+
|
9
|
+
module Atom
|
10
|
+
# @docs must implement [], []=, next_id!
|
11
|
+
# including servlet must implement gen_id, url_to_key, key_to_url, do_GET
|
12
|
+
class MemoryCollection < WEBrick::HTTPServlet::AbstractServlet
|
13
|
+
include Atom::AtomPub
|
14
|
+
|
15
|
+
def initialize server, docs
|
16
|
+
super
|
17
|
+
|
18
|
+
@docs = docs
|
19
|
+
end
|
20
|
+
|
21
|
+
def gen_id key
|
22
|
+
key
|
23
|
+
end
|
24
|
+
|
25
|
+
def key_to_url req, key
|
26
|
+
req.script_name + "/" + key
|
27
|
+
end
|
28
|
+
|
29
|
+
def url_to_key url
|
30
|
+
url.split("/").last
|
31
|
+
end
|
32
|
+
|
33
|
+
def add_edit entry, key
|
34
|
+
edit = entry.links.new
|
35
|
+
edit["rel"] = "edit"
|
36
|
+
edit
|
37
|
+
end
|
38
|
+
|
39
|
+
def do_GET req, res
|
40
|
+
res.body = if req.path_info.empty? or req.path_info == "/"
|
41
|
+
feed = Atom::Feed.new
|
42
|
+
|
43
|
+
@docs.each do |key,doc|
|
44
|
+
entry = doc.to_atom_entry
|
45
|
+
add_edit(entry, key)["href"] = key_to_url(req, key)
|
46
|
+
feed << entry
|
47
|
+
end
|
48
|
+
|
49
|
+
feed.entries.first.to_s
|
50
|
+
else
|
51
|
+
key = url_to_key(req.request_uri.to_s)
|
52
|
+
entry = @docs[key].to_atom_entry
|
53
|
+
add_edit(entry, key)["href"] = key_to_url(req, key)
|
54
|
+
entry.to_s
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
h = WEBrick::HTTPServer.new(:Port => ARGV[0])
|
61
|
+
|
62
|
+
docs = {}
|
63
|
+
docs.instance_variable_set :@last_key, "0"
|
64
|
+
|
65
|
+
def docs.next_key!
|
66
|
+
@last_key.next!
|
67
|
+
end
|
68
|
+
|
69
|
+
h.mount(ARGV[1], Atom::MemoryCollection, docs)
|
70
|
+
|
71
|
+
h.start
|