rxerces 0.2.0 → 0.4.0

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.
@@ -18,6 +18,7 @@ module Nokogiri
18
18
  Element = RXerces::XML::Element
19
19
  Text = RXerces::XML::Text
20
20
  NodeSet = RXerces::XML::NodeSet
21
+ Schema = RXerces::XML::Schema
21
22
  end
22
23
 
23
24
  # Top-level parse method for compatibility
@@ -1,3 +1,3 @@
1
1
  module RXerces
2
- VERSION = "0.2.0".freeze
2
+ VERSION = "0.4.0".freeze
3
3
  end
data/rxerces.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |spec|
2
2
  spec.name = "rxerces"
3
- spec.version = "0.2.0"
3
+ spec.version = "0.4.0"
4
4
  spec.author = "Daniel J. Berger"
5
5
  spec.email = "djberg96@gmail.com"
6
6
  spec.cert_chain = ["certs/djberg96_pub.pem"]
@@ -17,7 +17,10 @@ Gem::Specification.new do |spec|
17
17
  spec.add_development_dependency "rake-compiler", "~> 1.2"
18
18
  spec.add_development_dependency "rspec", "~> 3.12"
19
19
 
20
- spec.description = "A Ruby XML library with Nokogiri-compatible API, powered by Xerces-C instead of libxml2"
20
+ spec.description = <<-EOF
21
+ A Ruby XML library with Nokogiri-compatible API, powered by Xerces-C
22
+ instead of libxml2. It also optionally uses Xalan for Xpath 1.0 compliance.
23
+ EOF
21
24
 
22
25
  spec.metadata = {
23
26
  'homepage_uri' => 'https://github.com/djberg96/rxerces',
@@ -63,4 +63,58 @@ RSpec.describe RXerces::XML::Document do
63
63
  expect(result).to be_a(RXerces::XML::NodeSet)
64
64
  end
65
65
  end
66
+
67
+ describe "#encoding" do
68
+ it "returns UTF-8 for documents without explicit encoding" do
69
+ doc = RXerces::XML::Document.parse(simple_xml)
70
+ expect(doc.encoding).to eq('UTF-8')
71
+ end
72
+
73
+ it "returns the encoding specified in the XML declaration" do
74
+ xml_with_encoding = '<?xml version="1.0" encoding="ISO-8859-1"?><root><item>Test</item></root>'
75
+ doc = RXerces::XML::Document.parse(xml_with_encoding)
76
+ expect(doc.encoding).to eq('ISO-8859-1')
77
+ end
78
+
79
+ it "returns the encoding for UTF-16 documents" do
80
+ xml_with_encoding = '<?xml version="1.0" encoding="UTF-16"?><root><item>Test</item></root>'
81
+ doc = RXerces::XML::Document.parse(xml_with_encoding)
82
+ expect(doc.encoding).to eq('UTF-16')
83
+ end
84
+ end
85
+
86
+ describe "#create_element" do
87
+ let(:doc) { RXerces::XML::Document.parse(simple_xml) }
88
+
89
+ it "creates a new element with the specified name" do
90
+ element = doc.create_element('book')
91
+ expect(element).to be_a(RXerces::XML::Element)
92
+ expect(element.name).to eq('book')
93
+ end
94
+
95
+ it "creates an element that can have attributes set" do
96
+ element = doc.create_element('book')
97
+ attributes = element.attributes
98
+ expect(attributes).to be_a(Hash)
99
+ expect(attributes).to be_empty
100
+ end
101
+
102
+ it "creates an element that can have children added" do
103
+ element = doc.create_element('book')
104
+ element.add_child('Test Content')
105
+ expect(element.text).to eq('Test Content')
106
+ end
107
+
108
+ it "creates an element that can be added to the document" do
109
+ root = doc.root
110
+ new_element = doc.create_element('new_child')
111
+ new_element.add_child('New content')
112
+ root.add_child(new_element)
113
+
114
+ # Verify the new element is in the document
115
+ result = doc.xpath('//new_child')
116
+ expect(result.length).to eq(1)
117
+ expect(result.first.text).to eq('New content')
118
+ end
119
+ end
66
120
  end
data/spec/node_spec.rb CHANGED
@@ -29,6 +29,32 @@ RSpec.describe RXerces::XML::Node do
29
29
  end
30
30
  end
31
31
 
32
+ describe "#namespace" do
33
+ let(:ns_xml) do
34
+ <<-XML
35
+ <root xmlns="http://example.com/default">
36
+ <item>Default namespace</item>
37
+ </root>
38
+ XML
39
+ end
40
+ let(:ns_doc) { RXerces::XML::Document.parse(ns_xml) }
41
+
42
+ it "returns nil for nodes without a namespace" do
43
+ expect(root.namespace).to be_nil
44
+ end
45
+
46
+ it "returns the default namespace URI" do
47
+ ns_root = ns_doc.root
48
+ expect(ns_root.namespace).to eq('http://example.com/default')
49
+ end
50
+
51
+ it "returns the namespace for child elements inheriting default namespace" do
52
+ ns_root = ns_doc.root
53
+ item = ns_root.children.find { |n| n.is_a?(RXerces::XML::Element) }
54
+ expect(item.namespace).to eq('http://example.com/default')
55
+ end
56
+ end
57
+
32
58
  describe "#text" do
33
59
  it "returns text content" do
34
60
  person = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
@@ -102,10 +128,432 @@ RSpec.describe RXerces::XML::Node do
102
128
  end
103
129
  end
104
130
 
131
+ describe "#parent" do
132
+ it "returns the parent node" do
133
+ person = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
134
+ parent = person.parent
135
+ expect(parent).to be_a(RXerces::XML::Element)
136
+ expect(parent.name).to eq('root')
137
+ end
138
+
139
+ it "returns the parent for nested elements" do
140
+ person = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
141
+ age = person.children.find { |n| n.name == 'age' }
142
+ parent = age.parent
143
+ expect(parent).to be_a(RXerces::XML::Element)
144
+ expect(parent.name).to eq('person')
145
+ expect(parent['id']).to eq('1')
146
+ end
147
+
148
+ it "returns the document for root element" do
149
+ parent = root.parent
150
+ expect(parent).not_to be_nil
151
+ expect(parent.name).to eq('#document')
152
+ end
153
+
154
+ it "returns nil for nodes without parent" do
155
+ # This is edge case - all nodes in a parsed document have parents
156
+ # but we test the safety of the method
157
+ expect(root.parent).not_to be_nil
158
+ end
159
+ end
160
+
161
+ describe "#attributes" do
162
+ it "returns a hash of attributes" do
163
+ person = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
164
+ attrs = person.attributes
165
+ expect(attrs).to be_a(Hash)
166
+ expect(attrs['id']).to eq('1')
167
+ expect(attrs['name']).to eq('Alice')
168
+ end
169
+
170
+ it "returns all attributes" do
171
+ person = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
172
+ attrs = person.attributes
173
+ expect(attrs.keys).to match_array(['id', 'name'])
174
+ end
175
+
176
+ it "returns empty hash for elements without attributes" do
177
+ person = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
178
+ age = person.children.find { |n| n.name == 'age' }
179
+ attrs = age.attributes
180
+ expect(attrs).to be_a(Hash)
181
+ expect(attrs).to be_empty
182
+ end
183
+
184
+ it "returns empty hash for text nodes" do
185
+ person = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
186
+ text_node = person.children.find { |n| n.is_a?(RXerces::XML::Text) }
187
+ attrs = text_node.attributes
188
+ expect(attrs).to be_a(Hash)
189
+ expect(attrs).to be_empty
190
+ end
191
+ end
192
+
193
+ describe "#next_sibling" do
194
+ it "returns the next sibling node" do
195
+ people = root.children.select { |n| n.is_a?(RXerces::XML::Element) }
196
+ first_person = people[0]
197
+ next_node = first_person.next_sibling
198
+
199
+ # May be a text node (whitespace) between elements
200
+ # Skip to next element if needed
201
+ while next_node && next_node.is_a?(RXerces::XML::Text)
202
+ next_node = next_node.next_sibling
203
+ end
204
+
205
+ expect(next_node).to be_a(RXerces::XML::Element)
206
+ expect(next_node.name).to eq('person')
207
+ expect(next_node['id']).to eq('2')
208
+ end
209
+
210
+ it "returns nil for last sibling" do
211
+ people = root.children.select { |n| n.is_a?(RXerces::XML::Element) }
212
+ last_person = people.last
213
+
214
+ # Navigate past any trailing whitespace
215
+ next_node = last_person.next_sibling
216
+ while next_node && next_node.is_a?(RXerces::XML::Text)
217
+ next_node = next_node.next_sibling
218
+ end
219
+
220
+ expect(next_node).to be_nil
221
+ end
222
+
223
+ it "can navigate through all siblings" do
224
+ first_element = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
225
+ siblings = []
226
+ current = first_element
227
+
228
+ while current
229
+ siblings << current if current.is_a?(RXerces::XML::Element)
230
+ current = current.next_sibling
231
+ end
232
+
233
+ expect(siblings.length).to eq(2)
234
+ expect(siblings[0]['id']).to eq('1')
235
+ expect(siblings[1]['id']).to eq('2')
236
+ end
237
+ end
238
+
239
+ describe "#previous_sibling" do
240
+ it "returns the previous sibling node" do
241
+ people = root.children.select { |n| n.is_a?(RXerces::XML::Element) }
242
+ second_person = people[1]
243
+ prev_node = second_person.previous_sibling
244
+
245
+ # May be a text node (whitespace) between elements
246
+ # Skip to previous element if needed
247
+ while prev_node && prev_node.is_a?(RXerces::XML::Text)
248
+ prev_node = prev_node.previous_sibling
249
+ end
250
+
251
+ expect(prev_node).to be_a(RXerces::XML::Element)
252
+ expect(prev_node.name).to eq('person')
253
+ expect(prev_node['id']).to eq('1')
254
+ end
255
+
256
+ it "returns nil for first sibling" do
257
+ first_element = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
258
+
259
+ # Navigate past any leading whitespace
260
+ prev_node = first_element.previous_sibling
261
+ while prev_node && prev_node.is_a?(RXerces::XML::Text)
262
+ prev_node = prev_node.previous_sibling
263
+ end
264
+
265
+ expect(prev_node).to be_nil
266
+ end
267
+
268
+ it "can navigate backward through all siblings" do
269
+ last_element = root.children.select { |n| n.is_a?(RXerces::XML::Element) }.last
270
+ siblings = []
271
+ current = last_element
272
+
273
+ while current
274
+ siblings.unshift(current) if current.is_a?(RXerces::XML::Element)
275
+ current = current.previous_sibling
276
+ end
277
+
278
+ expect(siblings.length).to eq(2)
279
+ expect(siblings[0]['id']).to eq('1')
280
+ expect(siblings[1]['id']).to eq('2')
281
+ end
282
+ end
283
+
284
+ describe "#add_child" do
285
+ it "adds a text node from a string" do
286
+ person = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
287
+ initial_count = person.children.length
288
+
289
+ person.add_child("New text content")
290
+
291
+ expect(person.children.length).to eq(initial_count + 1)
292
+ last_child = person.children.last
293
+ expect(last_child).to be_a(RXerces::XML::Text)
294
+ expect(last_child.text).to eq("New text content")
295
+ end
296
+
297
+ it "adds a new element to another element" do
298
+ # Create a simple document to test with
299
+ simple_xml = '<root><parent></parent></root>'
300
+ simple_doc = RXerces::XML::Document.parse(simple_xml)
301
+ parent = simple_doc.root.children.find { |n| n.is_a?(RXerces::XML::Element) }
302
+
303
+ # Add text child
304
+ parent.add_child("Hello World")
305
+
306
+ expect(parent.children.length).to be > 0
307
+ text_child = parent.children.find { |n| n.is_a?(RXerces::XML::Text) }
308
+ expect(text_child.text).to eq("Hello World")
309
+ end
310
+
311
+ it "allows building a document structure" do
312
+ simple_xml = '<root></root>'
313
+ simple_doc = RXerces::XML::Document.parse(simple_xml)
314
+ root = simple_doc.root
315
+
316
+ # Add multiple children
317
+ root.add_child("First text")
318
+ root.add_child("Second text")
319
+
320
+ text_nodes = root.children.select { |n| n.is_a?(RXerces::XML::Text) }
321
+ expect(text_nodes.length).to eq(2)
322
+ expect(text_nodes[0].text).to eq("First text")
323
+ expect(text_nodes[1].text).to eq("Second text")
324
+ end
325
+
326
+ it "modifies the document" do
327
+ simple_xml = '<item></item>'
328
+ simple_doc = RXerces::XML::Document.parse(simple_xml)
329
+ item = simple_doc.root
330
+
331
+ item.add_child("Content")
332
+
333
+ xml_output = simple_doc.to_s
334
+ expect(xml_output).to include("Content")
335
+ end
336
+ end
337
+
338
+ describe "#remove" do
339
+ it "removes a node from its parent" do
340
+ person = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
341
+ initial_count = root.children.select { |n| n.is_a?(RXerces::XML::Element) }.length
342
+
343
+ person.remove
344
+
345
+ remaining = root.children.select { |n| n.is_a?(RXerces::XML::Element) }
346
+ expect(remaining.length).to eq(initial_count - 1)
347
+ end
348
+
349
+ it "removes a child element from parent" do
350
+ person = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
351
+ age = person.children.find { |n| n.name == 'age' }
352
+ initial_count = person.children.select { |n| n.is_a?(RXerces::XML::Element) }.length
353
+
354
+ age.remove
355
+
356
+ remaining = person.children.select { |n| n.is_a?(RXerces::XML::Element) }
357
+ expect(remaining.length).to eq(initial_count - 1)
358
+ expect(person.children.find { |n| n.name == 'age' }).to be_nil
359
+ end
360
+
361
+ it "modifies the document" do
362
+ simple_xml = '<root><item>Remove me</item><keep>Keep me</keep></root>'
363
+ simple_doc = RXerces::XML::Document.parse(simple_xml)
364
+ item = simple_doc.xpath('//item').first
365
+
366
+ item.remove
367
+
368
+ xml_output = simple_doc.to_s
369
+ expect(xml_output).not_to include("Remove me")
370
+ expect(xml_output).to include("Keep me")
371
+ end
372
+
373
+ it "raises error when node has no parent" do
374
+ # The root element's parent is the document, so this should work
375
+ # We'll test with a document node instead
376
+ expect {
377
+ root.parent.remove
378
+ }.to raise_error(RuntimeError, /no parent/)
379
+ end
380
+ end
381
+
382
+ describe "#unlink" do
383
+ it "is an alias for remove" do
384
+ simple_xml = '<root><item>Test</item></root>'
385
+ simple_doc = RXerces::XML::Document.parse(simple_xml)
386
+ item = simple_doc.xpath('//item').first
387
+
388
+ expect(item).to respond_to(:unlink)
389
+ item.unlink
390
+
391
+ xml_output = simple_doc.to_s
392
+ expect(xml_output).not_to include("Test")
393
+ end
394
+ end
395
+
105
396
  describe "#xpath" do
106
397
  it "returns a NodeSet" do
107
398
  result = root.xpath('.//age')
108
399
  expect(result).to be_a(RXerces::XML::NodeSet)
109
400
  end
110
401
  end
402
+
403
+ describe "#inner_html" do
404
+ it "returns the XML content of children without parent tags" do
405
+ person = root.xpath('//person').first
406
+ inner = person.inner_html
407
+ expect(inner).to include('<age>')
408
+ expect(inner).to include('<city>')
409
+ expect(inner).not_to include('<person')
410
+ end
411
+
412
+ it "returns empty string for nodes without children" do
413
+ age = root.xpath('//age').first
414
+ inner = age.inner_html
415
+ expect(inner).to eq('30')
416
+ end
417
+
418
+ it "includes multiple children" do
419
+ person = root.xpath('//person').first
420
+ inner = person.inner_html
421
+ expect(inner).to include('<age>30</age>')
422
+ expect(inner).to include('<city>New York</city>')
423
+ end
424
+ end
425
+
426
+ describe "#inner_xml" do
427
+ it "is an alias for inner_html" do
428
+ person = root.xpath('//person').first
429
+ expect(person.inner_xml).to eq(person.inner_html)
430
+ end
431
+
432
+ it "returns the same XML content" do
433
+ person = root.xpath('//person').first
434
+ inner = person.inner_xml
435
+ expect(inner).to include('<age>')
436
+ expect(inner).to include('<city>')
437
+ end
438
+ end
439
+
440
+ describe "#path" do
441
+ it "returns the XPath to the root element" do
442
+ expect(root.path).to eq('/root[1]')
443
+ end
444
+
445
+ it "returns the XPath to a nested element" do
446
+ person = root.xpath('//person').first
447
+ expect(person.path).to eq('/root[1]/person[1]')
448
+ end
449
+
450
+ it "returns the XPath to the second person element" do
451
+ people = root.xpath('//person')
452
+ second_person = people[1]
453
+ expect(second_person.path).to eq('/root[1]/person[2]')
454
+ end
455
+
456
+ it "returns the XPath to deeply nested elements" do
457
+ age = root.xpath('//age').first
458
+ expect(age.path).to eq('/root[1]/person[1]/age[1]')
459
+ end
460
+ end
461
+
462
+ describe "#blank?" do
463
+ let(:blank_xml) { '<root><empty></empty><whitespace> </whitespace><content>Hello</content></root>' }
464
+ let(:blank_doc) { RXerces::XML::Document.parse(blank_xml) }
465
+
466
+ it "returns false for elements with text content" do
467
+ content = blank_doc.xpath('//content').first
468
+ expect(content.blank?).to be false
469
+ end
470
+
471
+ it "returns true for empty elements" do
472
+ empty = blank_doc.xpath('//empty').first
473
+ expect(empty.blank?).to be true
474
+ end
475
+
476
+ it "returns true for elements with only whitespace" do
477
+ whitespace = blank_doc.xpath('//whitespace').first
478
+ expect(whitespace.blank?).to be true
479
+ end
480
+
481
+ it "returns false for elements with child elements" do
482
+ root = blank_doc.root
483
+ expect(root.blank?).to be false
484
+ end
485
+
486
+ it "returns false for text nodes with content" do
487
+ content = blank_doc.xpath('//content').first
488
+ text_node = content.children.first
489
+ expect(text_node.blank?).to be false
490
+ end
491
+
492
+ it "returns true for text nodes with only whitespace" do
493
+ whitespace = blank_doc.xpath('//whitespace').first
494
+ text_node = whitespace.children.first
495
+ expect(text_node.blank?).to be true
496
+ end
497
+ end
498
+
499
+ describe "#search" do
500
+ it "is an alias for xpath" do
501
+ result1 = root.search('.//age')
502
+ result2 = root.xpath('.//age')
503
+ expect(result1.length).to eq(result2.length)
504
+ expect(result1.first.text).to eq(result2.first.text)
505
+ end
506
+
507
+ it "returns a NodeSet" do
508
+ result = root.search('.//person')
509
+ expect(result).to be_a(RXerces::XML::NodeSet)
510
+ end
511
+
512
+ it "finds nested elements" do
513
+ result = root.search('.//age')
514
+ expect(result.length).to eq(2)
515
+ expect(result.first.text).to eq('30')
516
+ end
517
+ end
518
+
519
+ describe "#at_xpath" do
520
+ it "returns the first matching node" do
521
+ result = root.at_xpath('.//age')
522
+ expect(result).to be_a(RXerces::XML::Element)
523
+ expect(result.text).to eq('30')
524
+ end
525
+
526
+ it "returns nil when no match found" do
527
+ result = root.at_xpath('.//nonexistent')
528
+ expect(result).to be_nil
529
+ end
530
+
531
+ it "returns only the first match when multiple exist" do
532
+ result = root.at_xpath('.//person')
533
+ expect(result['id']).to eq('1')
534
+ end
535
+ end
536
+
537
+ describe "#at" do
538
+ it "is an alias for at_xpath" do
539
+ result1 = root.at('.//age')
540
+ result2 = root.at_xpath('.//age')
541
+ expect(result1.text).to eq(result2.text)
542
+ end
543
+
544
+ it "returns the first matching element" do
545
+ result = root.at('.//city')
546
+ expect(result.text).to eq('New York')
547
+ end
548
+ end
549
+
550
+ describe "#css" do
551
+ it "raises NotImplementedError for CSS selectors" do
552
+ expect { root.css('div.class') }.to raise_error(NotImplementedError, /CSS selectors are not supported/)
553
+ end
554
+
555
+ it "suggests using xpath instead" do
556
+ expect { root.css('p') }.to raise_error(NotImplementedError, /Use xpath/)
557
+ end
558
+ end
111
559
  end
@@ -61,6 +61,12 @@ RSpec.describe "Nokogiri compatibility" do
61
61
  end
62
62
  end
63
63
 
64
+ describe "Nokogiri::XML::Schema" do
65
+ it "is an alias for RXerces::XML::Schema" do
66
+ expect(Nokogiri::XML::Schema).to eq(RXerces::XML::Schema)
67
+ end
68
+ end
69
+
64
70
  describe "API compatibility" do
65
71
  let(:doc) { Nokogiri.XML(simple_xml) }
66
72
 
@@ -95,4 +101,67 @@ RSpec.describe "Nokogiri compatibility" do
95
101
  expect(children).to be_an(Array)
96
102
  end
97
103
  end
104
+
105
+ describe "Schema validation compatibility" do
106
+ let(:xsd) do
107
+ <<~XSD
108
+ <?xml version="1.0"?>
109
+ <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
110
+ <xs:element name="person">
111
+ <xs:complexType>
112
+ <xs:sequence>
113
+ <xs:element name="name" type="xs:string"/>
114
+ <xs:element name="age" type="xs:integer"/>
115
+ </xs:sequence>
116
+ </xs:complexType>
117
+ </xs:element>
118
+ </xs:schema>
119
+ XSD
120
+ end
121
+
122
+ let(:valid_xml) do
123
+ <<~XML
124
+ <?xml version="1.0"?>
125
+ <person>
126
+ <name>John</name>
127
+ <age>30</age>
128
+ </person>
129
+ XML
130
+ end
131
+
132
+ let(:invalid_xml) do
133
+ <<~XML
134
+ <?xml version="1.0"?>
135
+ <person>
136
+ <name>Jane</name>
137
+ <age>invalid</age>
138
+ </person>
139
+ XML
140
+ end
141
+
142
+ it "provides Schema.from_string method" do
143
+ schema = Nokogiri::XML::Schema.from_string(xsd)
144
+ expect(schema).to be_a(Nokogiri::XML::Schema)
145
+ end
146
+
147
+ it "provides Schema.from_document method" do
148
+ doc = Nokogiri::XML.parse(xsd)
149
+ schema = Nokogiri::XML::Schema.from_document(doc)
150
+ expect(schema).to be_a(Nokogiri::XML::Schema)
151
+ end
152
+
153
+ it "validates a valid document" do
154
+ schema = Nokogiri::XML::Schema.from_string(xsd)
155
+ doc = Nokogiri::XML.parse(valid_xml)
156
+ errors = doc.validate(schema)
157
+ expect(errors).to be_empty
158
+ end
159
+
160
+ it "returns errors for an invalid document" do
161
+ schema = Nokogiri::XML::Schema.from_string(xsd)
162
+ doc = Nokogiri::XML.parse(invalid_xml)
163
+ errors = doc.validate(schema)
164
+ expect(errors).not_to be_empty
165
+ end
166
+ end
98
167
  end
@@ -4,7 +4,7 @@ require 'rxerces'
4
4
 
5
5
  RSpec.shared_examples RXerces do
6
6
  example 'version number is set to the expected value' do
7
- expect(RXerces::VERSION).to eq('0.2.0')
7
+ expect(RXerces::VERSION).to eq('0.4.0')
8
8
  expect(RXerces::VERSION).to be_frozen
9
9
  end
10
10
  end