tp-blather 0.8.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (150) hide show
  1. data/.autotest +13 -0
  2. data/.gemtest +0 -0
  3. data/.gitignore +19 -0
  4. data/.rspec +3 -0
  5. data/.travis.yml +8 -0
  6. data/CHANGELOG.md +249 -0
  7. data/Gemfile +4 -0
  8. data/Guardfile +5 -0
  9. data/LICENSE +22 -0
  10. data/README.md +413 -0
  11. data/Rakefile +20 -0
  12. data/TODO.md +2 -0
  13. data/blather.gemspec +51 -0
  14. data/examples/certs/README +20 -0
  15. data/examples/certs/ca-bundle.crt +3987 -0
  16. data/examples/echo.rb +19 -0
  17. data/examples/execute.rb +17 -0
  18. data/examples/ping_pong.rb +38 -0
  19. data/examples/print_hierarchy.rb +77 -0
  20. data/examples/rosterprint.rb +15 -0
  21. data/examples/stream_only.rb +28 -0
  22. data/examples/trusted_echo.rb +21 -0
  23. data/examples/xmpp4r/echo.rb +36 -0
  24. data/lib/blather.rb +112 -0
  25. data/lib/blather/cert_store.rb +53 -0
  26. data/lib/blather/client.rb +95 -0
  27. data/lib/blather/client/client.rb +345 -0
  28. data/lib/blather/client/dsl.rb +320 -0
  29. data/lib/blather/client/dsl/pubsub.rb +174 -0
  30. data/lib/blather/core_ext/eventmachine.rb +125 -0
  31. data/lib/blather/core_ext/ipaddr.rb +20 -0
  32. data/lib/blather/errors.rb +69 -0
  33. data/lib/blather/errors/sasl_error.rb +44 -0
  34. data/lib/blather/errors/stanza_error.rb +110 -0
  35. data/lib/blather/errors/stream_error.rb +84 -0
  36. data/lib/blather/file_transfer.rb +107 -0
  37. data/lib/blather/file_transfer/ibb.rb +68 -0
  38. data/lib/blather/file_transfer/s5b.rb +114 -0
  39. data/lib/blather/jid.rb +141 -0
  40. data/lib/blather/roster.rb +118 -0
  41. data/lib/blather/roster_item.rb +146 -0
  42. data/lib/blather/stanza.rb +167 -0
  43. data/lib/blather/stanza/disco.rb +32 -0
  44. data/lib/blather/stanza/disco/capabilities.rb +161 -0
  45. data/lib/blather/stanza/disco/disco_info.rb +205 -0
  46. data/lib/blather/stanza/disco/disco_items.rb +134 -0
  47. data/lib/blather/stanza/iq.rb +144 -0
  48. data/lib/blather/stanza/iq/command.rb +339 -0
  49. data/lib/blather/stanza/iq/ibb.rb +86 -0
  50. data/lib/blather/stanza/iq/ping.rb +50 -0
  51. data/lib/blather/stanza/iq/query.rb +53 -0
  52. data/lib/blather/stanza/iq/roster.rb +185 -0
  53. data/lib/blather/stanza/iq/s5b.rb +208 -0
  54. data/lib/blather/stanza/iq/si.rb +415 -0
  55. data/lib/blather/stanza/iq/vcard.rb +149 -0
  56. data/lib/blather/stanza/message.rb +428 -0
  57. data/lib/blather/stanza/message/muc_user.rb +119 -0
  58. data/lib/blather/stanza/muc/muc_user_base.rb +54 -0
  59. data/lib/blather/stanza/presence.rb +172 -0
  60. data/lib/blather/stanza/presence/c.rb +100 -0
  61. data/lib/blather/stanza/presence/muc.rb +35 -0
  62. data/lib/blather/stanza/presence/muc_user.rb +147 -0
  63. data/lib/blather/stanza/presence/status.rb +218 -0
  64. data/lib/blather/stanza/presence/subscription.rb +100 -0
  65. data/lib/blather/stanza/pubsub.rb +119 -0
  66. data/lib/blather/stanza/pubsub/affiliations.rb +79 -0
  67. data/lib/blather/stanza/pubsub/create.rb +65 -0
  68. data/lib/blather/stanza/pubsub/errors.rb +18 -0
  69. data/lib/blather/stanza/pubsub/event.rb +139 -0
  70. data/lib/blather/stanza/pubsub/items.rb +103 -0
  71. data/lib/blather/stanza/pubsub/publish.rb +103 -0
  72. data/lib/blather/stanza/pubsub/retract.rb +92 -0
  73. data/lib/blather/stanza/pubsub/subscribe.rb +68 -0
  74. data/lib/blather/stanza/pubsub/subscription.rb +135 -0
  75. data/lib/blather/stanza/pubsub/subscriptions.rb +83 -0
  76. data/lib/blather/stanza/pubsub/unsubscribe.rb +84 -0
  77. data/lib/blather/stanza/pubsub_owner.rb +51 -0
  78. data/lib/blather/stanza/pubsub_owner/delete.rb +52 -0
  79. data/lib/blather/stanza/pubsub_owner/purge.rb +52 -0
  80. data/lib/blather/stanza/x.rb +416 -0
  81. data/lib/blather/stream.rb +266 -0
  82. data/lib/blather/stream/client.rb +32 -0
  83. data/lib/blather/stream/component.rb +39 -0
  84. data/lib/blather/stream/features.rb +70 -0
  85. data/lib/blather/stream/features/register.rb +38 -0
  86. data/lib/blather/stream/features/resource.rb +63 -0
  87. data/lib/blather/stream/features/sasl.rb +190 -0
  88. data/lib/blather/stream/features/session.rb +45 -0
  89. data/lib/blather/stream/features/tls.rb +29 -0
  90. data/lib/blather/stream/parser.rb +102 -0
  91. data/lib/blather/version.rb +3 -0
  92. data/lib/blather/xmpp_node.rb +94 -0
  93. data/spec/blather/client/client_spec.rb +687 -0
  94. data/spec/blather/client/dsl/pubsub_spec.rb +492 -0
  95. data/spec/blather/client/dsl_spec.rb +266 -0
  96. data/spec/blather/errors/sasl_error_spec.rb +33 -0
  97. data/spec/blather/errors/stanza_error_spec.rb +129 -0
  98. data/spec/blather/errors/stream_error_spec.rb +108 -0
  99. data/spec/blather/errors_spec.rb +33 -0
  100. data/spec/blather/file_transfer_spec.rb +135 -0
  101. data/spec/blather/jid_spec.rb +87 -0
  102. data/spec/blather/roster_item_spec.rb +134 -0
  103. data/spec/blather/roster_spec.rb +107 -0
  104. data/spec/blather/stanza/discos/disco_info_spec.rb +247 -0
  105. data/spec/blather/stanza/discos/disco_items_spec.rb +154 -0
  106. data/spec/blather/stanza/iq/command_spec.rb +206 -0
  107. data/spec/blather/stanza/iq/ibb_spec.rb +124 -0
  108. data/spec/blather/stanza/iq/ping_spec.rb +45 -0
  109. data/spec/blather/stanza/iq/query_spec.rb +64 -0
  110. data/spec/blather/stanza/iq/roster_spec.rb +139 -0
  111. data/spec/blather/stanza/iq/s5b_spec.rb +57 -0
  112. data/spec/blather/stanza/iq/si_spec.rb +98 -0
  113. data/spec/blather/stanza/iq/vcard_spec.rb +93 -0
  114. data/spec/blather/stanza/iq_spec.rb +61 -0
  115. data/spec/blather/stanza/message/muc_user_spec.rb +152 -0
  116. data/spec/blather/stanza/message_spec.rb +282 -0
  117. data/spec/blather/stanza/presence/c_spec.rb +56 -0
  118. data/spec/blather/stanza/presence/muc_spec.rb +37 -0
  119. data/spec/blather/stanza/presence/muc_user_spec.rb +83 -0
  120. data/spec/blather/stanza/presence/status_spec.rb +144 -0
  121. data/spec/blather/stanza/presence/subscription_spec.rb +102 -0
  122. data/spec/blather/stanza/presence_spec.rb +125 -0
  123. data/spec/blather/stanza/pubsub/affiliations_spec.rb +57 -0
  124. data/spec/blather/stanza/pubsub/create_spec.rb +56 -0
  125. data/spec/blather/stanza/pubsub/event_spec.rb +98 -0
  126. data/spec/blather/stanza/pubsub/items_spec.rb +79 -0
  127. data/spec/blather/stanza/pubsub/publish_spec.rb +83 -0
  128. data/spec/blather/stanza/pubsub/retract_spec.rb +75 -0
  129. data/spec/blather/stanza/pubsub/subscribe_spec.rb +61 -0
  130. data/spec/blather/stanza/pubsub/subscription_spec.rb +97 -0
  131. data/spec/blather/stanza/pubsub/subscriptions_spec.rb +59 -0
  132. data/spec/blather/stanza/pubsub/unsubscribe_spec.rb +74 -0
  133. data/spec/blather/stanza/pubsub_owner/delete_spec.rb +50 -0
  134. data/spec/blather/stanza/pubsub_owner/purge_spec.rb +50 -0
  135. data/spec/blather/stanza/pubsub_owner_spec.rb +27 -0
  136. data/spec/blather/stanza/pubsub_spec.rb +68 -0
  137. data/spec/blather/stanza/x_spec.rb +231 -0
  138. data/spec/blather/stanza_spec.rb +134 -0
  139. data/spec/blather/stream/client_spec.rb +1090 -0
  140. data/spec/blather/stream/component_spec.rb +108 -0
  141. data/spec/blather/stream/parser_spec.rb +152 -0
  142. data/spec/blather/stream/ssl_spec.rb +32 -0
  143. data/spec/blather/xmpp_node_spec.rb +47 -0
  144. data/spec/blather_spec.rb +34 -0
  145. data/spec/fixtures/pubsub.rb +311 -0
  146. data/spec/spec_helper.rb +17 -0
  147. data/yard/templates/default/class/html/handlers.erb +18 -0
  148. data/yard/templates/default/class/setup.rb +10 -0
  149. data/yard/templates/default/class/text/handlers.erb +1 -0
  150. metadata +459 -0
@@ -0,0 +1,52 @@
1
+ module Blather
2
+ class Stanza
3
+ class PubSubOwner
4
+
5
+ # # PubSubOwner Purge Stanza
6
+ #
7
+ # [XEP-0060 Section 8.5 - Purge All Node Items](http://xmpp.org/extensions/xep-0060.html#owner-purge)
8
+ #
9
+ # @handler :pubsub_purge
10
+ class Purge < PubSubOwner
11
+ register :pubsub_purge, :purge, self.registered_ns
12
+
13
+ # Create a new purge stanza
14
+ #
15
+ # @param [Blather::Stanza::Iq::VALID_TYPES] type the IQ stanza type
16
+ # @param [String] host the host to send the request to
17
+ # @param [String] node the name of the node to purge
18
+ def self.new(type = :set, host = nil, node = nil)
19
+ new_node = super(type, host)
20
+ new_node.node = node
21
+ new_node
22
+ end
23
+
24
+ # Get the name of the node to delete
25
+ #
26
+ # @return [String]
27
+ def node
28
+ purge_node[:node]
29
+ end
30
+
31
+ # Set the name of the node to delete
32
+ #
33
+ # @param [String] node
34
+ def node=(node)
35
+ purge_node[:node] = node
36
+ end
37
+
38
+ # Get or create the actual purge node on the stanza
39
+ #
40
+ # @return [Blather::XMPPNode]
41
+ def purge_node
42
+ unless purge_node = pubsub.find_first('ns:purge', :ns => self.class.registered_ns)
43
+ self.pubsub << (purge_node = XMPPNode.new('purge', self.document))
44
+ purge_node.namespace = self.pubsub.namespace
45
+ end
46
+ purge_node
47
+ end
48
+ end # Retract
49
+
50
+ end # PubSub
51
+ end # Stanza
52
+ end # Blather
@@ -0,0 +1,416 @@
1
+ module Blather
2
+ class Stanza
3
+ # # X Stanza
4
+ #
5
+ # [XEP-0004 Data Forms](http://xmpp.org/extensions/xep-0004.html)
6
+ #
7
+ # Data Form node that allows for semi-structured data exchange
8
+ #
9
+ # @handler :x
10
+ class X < XMPPNode
11
+ register :x, 'jabber:x:data'
12
+
13
+ # @private
14
+ VALID_TYPES = [:cancel, :form, :result, :submit].freeze
15
+
16
+ # Create a new X node
17
+ # @param [:cancel, :form, :result, :submit, nil] type the x:form type
18
+ # @param [Array<Array, X::Field>, nil] fields a list of fields.
19
+ # These are passed directly to X::Field.new
20
+ # @return [X] a new X stanza
21
+ def self.new(type = nil, fields = [])
22
+ new_node = super :x
23
+
24
+ case type
25
+ when Nokogiri::XML::Node
26
+ new_node.inherit type
27
+ when Hash
28
+ new_node.type = type[:type]
29
+ new_node.fields = type[:fields]
30
+ else
31
+ new_node.type = type
32
+ new_node.fields = fields
33
+ end
34
+ new_node
35
+ end
36
+
37
+ # Find the X node on the parent or create a new one
38
+ #
39
+ # @param [Blather::Stanza] parent the parent node to search under
40
+ # @return [Blather::Stanza::X]
41
+ def self.find_or_create(parent)
42
+ if found_x = parent.find_first('//ns:x', :ns => self.registered_ns)
43
+ x = self.new found_x
44
+ found_x.remove
45
+ else
46
+ x = self.new
47
+ end
48
+ parent << x
49
+ x
50
+ end
51
+
52
+ # The Form's type
53
+ # @return [Symbol]
54
+ def type
55
+ read_attr :type, :to_sym
56
+ end
57
+
58
+ # Set the Form's type
59
+ # @param [:cancel, :form, :result, :submit] type the new type for the form
60
+ def type=(type)
61
+ if type && !VALID_TYPES.include?(type.to_sym)
62
+ raise ArgumentError, "Invalid Type (#{type}), use: #{VALID_TYPES*' '}"
63
+ end
64
+ write_attr :type, type
65
+ end
66
+
67
+ # List of field objects
68
+ # @return [Blather::Stanza::X::Field]
69
+ def fields
70
+ self.find('ns:field', :ns => self.class.registered_ns).map do |field|
71
+ Field.new(field)
72
+ end
73
+ end
74
+
75
+ # Find a field by var
76
+ # @param var the var for the field you wish to find
77
+ def field(var)
78
+ fields.detect { |f| f.var == var }
79
+ end
80
+
81
+ # Add an array of fields to form
82
+ # @param fields the array of fields, passed directly to Field.new
83
+ def fields=(fields)
84
+ remove_children :field
85
+ [fields].flatten.each do |field|
86
+ self << (f = Field.new(field))
87
+ f.namespace = self.namespace
88
+ end
89
+ end
90
+
91
+ # Check if the x is of type :cancel
92
+ #
93
+ # @return [true, false]
94
+ def cancel?
95
+ self.type == :cancel
96
+ end
97
+
98
+ # Check if the x is of type :form
99
+ #
100
+ # @return [true, false]
101
+ def form?
102
+ self.type == :form
103
+ end
104
+
105
+ # Check if the x is of type :result
106
+ #
107
+ # @return [true, false]
108
+ def result?
109
+ self.type == :result
110
+ end
111
+
112
+ # Check if the x is of type :submit
113
+ #
114
+ # @return [true, false]
115
+ def submit?
116
+ self.type == :submit
117
+ end
118
+
119
+ # Retrieve the form's instructions
120
+ #
121
+ # @return [String]
122
+ def instructions
123
+ content_from 'ns:instructions', :ns => self.registered_ns
124
+ end
125
+
126
+ # Set the form's instructions
127
+ #
128
+ # @param [String] instructions the form's instructions
129
+ def instructions=(instructions)
130
+ self.remove_children :instructions
131
+ if instructions
132
+ self << (i = XMPPNode.new(:instructions, self.document))
133
+ i.namespace = self.namespace
134
+ i << instructions
135
+ end
136
+ end
137
+
138
+ # Retrieve the form's title
139
+ #
140
+ # @return [String]
141
+ def title
142
+ content_from 'ns:title', :ns => self.registered_ns
143
+ end
144
+
145
+ # Set the form's title
146
+ #
147
+ # @param [String] title the form's title
148
+ def title=(title)
149
+ self.remove_children :title
150
+ if title
151
+ self << (t = XMPPNode.new(:title))
152
+ t.namespace = self.namespace
153
+ t << title
154
+ end
155
+ end
156
+
157
+ # Field stanza fragment
158
+ class Field < XMPPNode
159
+ register :field, 'jabber:x:data'
160
+ # @private
161
+ VALID_TYPES = [:boolean, :fixed, :hidden, :"jid-multi", :"jid-single", :"list-multi", :"list-single", :"text-multi", :"text-private", :"text-single"].freeze
162
+
163
+ # Create a new X Field
164
+ # @overload new(node)
165
+ # Imports the XML::Node to create a Field object
166
+ # @param [XML::Node] node the node object to import
167
+ # @overload new(opts = {})
168
+ # Creates a new Field using a hash of options
169
+ # @param [Hash] opts a hash of options
170
+ # @option opts [String] :var the variable for the field
171
+ # @option opts [:boolean, :fixed, :hidden, :"jid-multi", :"jid-single", :"list-multi", :"list-single", :"text-multi", :"text-private", :"text-single"] :type the type of the field
172
+ # @option opts [String] :label the label for the field
173
+ # @option [String, nil] :value the value for the field
174
+ # @option [String, nil] :description the description for the field
175
+ # @option [true, false, nil] :required the required flag for the field
176
+ # @param [Array<Array, X::Field::Option>, nil] :options a list of field options.
177
+ # These are passed directly to X::Field::Option.new
178
+ # @overload new(type, var = nil, label = nil)
179
+ # Create a new Field by name
180
+ # @param [String, nil] var the variable for the field
181
+ # @param [:boolean, :fixed, :hidden, :"jid-multi", :"jid-single", :"list-multi", :"list-single", :"text-multi", :"text-private", :"text-single"] type the type of the field
182
+ # @param [String, nil] label the label for the field
183
+ # @param [String, nil] value the value for the field
184
+ # @param [String, nil] description the description for the field
185
+ # @param [true, false, nil] required the required flag for the field
186
+ # @param [Array<Array, X::Field::Option>, nil] options a list of field options.
187
+ # These are passed directly to X::Field::Option.new
188
+ def self.new(var, type = nil, label = nil, value = nil, description = nil, required = false, options = [])
189
+ new_node = super :field
190
+
191
+ case var
192
+ when Nokogiri::XML::Node
193
+ new_node.inherit var
194
+ when Hash
195
+ new_node.var = var[:var]
196
+ new_node.type = var[:type]
197
+ new_node.label = var[:label]
198
+ new_node.value = var[:value]
199
+ new_node.desc = var[:description]
200
+ new_node.required = var[:required]
201
+ new_node.options = var[:options]
202
+ else
203
+ new_node.var = var
204
+ new_node.type = type
205
+ new_node.label = label
206
+ new_node.value = value
207
+ new_node.desc = description
208
+ new_node.required = required
209
+ new_node.options = options
210
+ end
211
+ new_node
212
+ end
213
+
214
+ # The Field's type
215
+ # @return [String]
216
+ def type
217
+ read_attr :type
218
+ end
219
+
220
+ # Set the Field's type
221
+ # @param [#to_sym] type the new type for the field
222
+ def type=(type)
223
+ if type && !VALID_TYPES.include?(type.to_sym)
224
+ raise ArgumentError, "Invalid Type (#{type}), use: #{VALID_TYPES*' '}"
225
+ end
226
+ write_attr :type, type
227
+ end
228
+
229
+ # The Field's var
230
+ # @return [String]
231
+ def var
232
+ read_attr :var
233
+ end
234
+
235
+ # Set the Field's var
236
+ # @param [String] var the new var for the field
237
+ def var=(var)
238
+ write_attr :var, var
239
+ end
240
+
241
+ # The Field's label
242
+ # @return [String]
243
+ def label
244
+ read_attr :label
245
+ end
246
+
247
+ # Set the Field's label
248
+ # @param [String] label the new label for the field
249
+ def label=(label)
250
+ write_attr :label, label
251
+ end
252
+
253
+ # Get the field's value
254
+ #
255
+ # @param [String]
256
+ def value
257
+ if self.namespace
258
+ content_from 'ns:value', :ns => self.namespace.href
259
+ else
260
+ content_from :value
261
+ end
262
+ end
263
+
264
+ # Set the field's value
265
+ #
266
+ # @param [String] value the field's value
267
+ def value=(value)
268
+ self.remove_children :value
269
+ if value
270
+ self << (v = XMPPNode.new(:value))
271
+ v.namespace = self.namespace
272
+ v << value
273
+ end
274
+ end
275
+
276
+ # Get the field's description
277
+ #
278
+ # @param [String]
279
+ def desc
280
+ if self.namespace
281
+ content_from 'ns:desc', :ns => self.namespace.href
282
+ else
283
+ content_from :desc
284
+ end
285
+ end
286
+
287
+ # Set the field's description
288
+ #
289
+ # @param [String] description the field's description
290
+ def desc=(description)
291
+ self.remove_children :desc
292
+ if description
293
+ self << (d = XMPPNode.new(:desc))
294
+ d.namespace = self.namespace
295
+ d << description
296
+ end
297
+ end
298
+
299
+ # Get the field's required flag
300
+ #
301
+ # @param [true, false]
302
+ def required?
303
+ !!if self.namespace
304
+ self.find_first 'ns:required', :ns => self.namespace.href
305
+ else
306
+ self.find_first 'required'
307
+ end
308
+ end
309
+
310
+ # Set the field's required flag
311
+ #
312
+ # @param [true, false] required the field's required flag
313
+ def required=(required)
314
+ return self.remove_children(:required) unless required
315
+
316
+ self << (r = XMPPNode.new(:required))
317
+ r.namespace = self.namespace
318
+ end
319
+
320
+ # Extract list of option objects
321
+ #
322
+ # @return [Blather::Stanza::X::Field::Option]
323
+ def options
324
+ if self.namespace
325
+ self.find('ns:option', :ns => self.namespace.href)
326
+ else
327
+ self.find('option')
328
+ end.map { |f| Option.new(f) }
329
+ end
330
+
331
+ # Add an array of options to field
332
+ # @param options the array of options, passed directly to Option.new
333
+ def options=(options)
334
+ remove_children :option
335
+ if options
336
+ Array(options).each { |o| self << Option.new(o) }
337
+ end
338
+ end
339
+
340
+ # Compare two Field objects by type, var and label
341
+ # @param [X::Field] o the Field object to compare against
342
+ # @return [true, false]
343
+ def eql?(o, *fields)
344
+ super o, *(fields + [:type, :var, :label, :desc, :required?, :value])
345
+ end
346
+
347
+ # Option stanza fragment
348
+ class Option < XMPPNode
349
+ register :option, 'jabber:x:data'
350
+ # Create a new X Field Option
351
+ # @overload new(node)
352
+ # Imports the XML::Node to create a Field option object
353
+ # @param [XML::Node] node the node object to import
354
+ # @overload new(opts = {})
355
+ # Creates a new Field option using a hash of options
356
+ # @param [Hash] opts a hash of options
357
+ # @option opts [String] :value the value of the field option
358
+ # @option opts [String] :label the human readable label for the field option
359
+ # @overload new(value, label = nil)
360
+ # Create a new Field option by name
361
+ # @param [String] value the value of the field option
362
+ # @param [String, nil] label the human readable label for the field option
363
+ def self.new(value, label = nil)
364
+ new_node = super :option
365
+
366
+ case value
367
+ when Nokogiri::XML::Node
368
+ new_node.inherit value
369
+ when Hash
370
+ new_node.value = value[:value]
371
+ new_node.label = value[:label]
372
+ else
373
+ new_node.value = value
374
+ new_node.label = label
375
+ end
376
+ new_node
377
+ end
378
+
379
+ # The Field Option's value
380
+ # @return [String]
381
+ def value
382
+ if self.namespace
383
+ content_from 'ns:value', :ns => self.namespace.href
384
+ else
385
+ content_from :value
386
+ end
387
+ end
388
+
389
+ # Set the Field Option's value
390
+ # @param [String] value the new value for the field option
391
+ def value=(value)
392
+ self.remove_children :value
393
+ if value
394
+ self << (v = XMPPNode.new(:value))
395
+ v.namespace = self.namespace
396
+ v << value
397
+ end
398
+ end
399
+
400
+ # The Field Option's label
401
+ # @return [String]
402
+ def label
403
+ read_attr :label
404
+ end
405
+
406
+ # Set the Field Option's label
407
+ # @param [String] label the new label for the field option
408
+ def label=(label)
409
+ write_attr :label, label
410
+ end
411
+ end # Option
412
+ end # Field
413
+ end # X
414
+
415
+ end #Stanza
416
+ end