rxerces 0.4.0 → 0.6.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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGES.md +19 -0
- data/README.md +14 -3
- data/benchmarks/README.md +68 -0
- data/benchmarks/css_benchmark.rb +115 -0
- data/benchmarks/parse_benchmark.rb +103 -0
- data/benchmarks/run_all.rb +25 -0
- data/benchmarks/serialization_benchmark.rb +93 -0
- data/benchmarks/traversal_benchmark.rb +149 -0
- data/benchmarks/xpath_benchmark.rb +100 -0
- data/ext/rxerces/rxerces.cpp +977 -50
- data/lib/rxerces/nokogiri.rb +26 -0
- data/lib/rxerces/version.rb +1 -1
- data/rxerces.gemspec +1 -1
- data/spec/document_spec.rb +117 -0
- data/spec/node_spec.rb +408 -4
- data/spec/nodeset_spec.rb +59 -0
- data/spec/nokogiri_compatibility_spec.rb +44 -0
- data/spec/rxerces_shared.rb +1 -1
- data.tar.gz.sig +0 -0
- metadata +8 -1
- metadata.gz.sig +0 -0
data/lib/rxerces/nokogiri.rb
CHANGED
|
@@ -21,6 +21,25 @@ module Nokogiri
|
|
|
21
21
|
Schema = RXerces::XML::Schema
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
+
# Nokogiri-compatible HTML module
|
|
25
|
+
# Since RXerces uses Xerces-C which is an XML parser,
|
|
26
|
+
# HTML parsing delegates to XML parsing
|
|
27
|
+
module HTML
|
|
28
|
+
# Parse HTML from a string - delegates to XML parsing
|
|
29
|
+
# @param string [String] HTML string to parse
|
|
30
|
+
# @return [RXerces::XML::Document] parsed document
|
|
31
|
+
def self.parse(string)
|
|
32
|
+
RXerces::XML::Document.parse(string)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Alias Document class for compatibility
|
|
36
|
+
Document = RXerces::XML::Document
|
|
37
|
+
Node = RXerces::XML::Node
|
|
38
|
+
Element = RXerces::XML::Element
|
|
39
|
+
Text = RXerces::XML::Text
|
|
40
|
+
NodeSet = RXerces::XML::NodeSet
|
|
41
|
+
end
|
|
42
|
+
|
|
24
43
|
# Top-level parse method for compatibility
|
|
25
44
|
# @param string [String] XML string to parse
|
|
26
45
|
# @return [RXerces::XML::Document] parsed document
|
|
@@ -28,6 +47,13 @@ module Nokogiri
|
|
|
28
47
|
RXerces::XML::Document.parse(string)
|
|
29
48
|
end
|
|
30
49
|
|
|
50
|
+
# Top-level HTML parsing method
|
|
51
|
+
# @param string [String] HTML string to parse
|
|
52
|
+
# @return [RXerces::XML::Document] parsed document
|
|
53
|
+
def self.HTML(string)
|
|
54
|
+
RXerces::XML::Document.parse(string)
|
|
55
|
+
end
|
|
56
|
+
|
|
31
57
|
class << self
|
|
32
58
|
alias_method :parse, :XML
|
|
33
59
|
end
|
data/lib/rxerces/version.rb
CHANGED
data/rxerces.gemspec
CHANGED
data/spec/document_spec.rb
CHANGED
|
@@ -64,6 +64,65 @@ RSpec.describe RXerces::XML::Document do
|
|
|
64
64
|
end
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
+
describe "#css" do
|
|
68
|
+
# Check if Xalan support is compiled in (CSS requires XPath which needs Xalan)
|
|
69
|
+
xalan_available = begin
|
|
70
|
+
test_xml = '<root><item id="1">A</item><item id="2">B</item></root>'
|
|
71
|
+
test_doc = RXerces::XML::Document.parse(test_xml)
|
|
72
|
+
result = test_doc.xpath('//item[@id="1"]')
|
|
73
|
+
result.length == 1
|
|
74
|
+
rescue
|
|
75
|
+
false
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
before(:all) do
|
|
79
|
+
unless xalan_available
|
|
80
|
+
skip "Xalan-C not available - CSS selectors require Xalan-C library"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
let(:xml) do
|
|
85
|
+
<<-XML
|
|
86
|
+
<library>
|
|
87
|
+
<book id="book1" class="fiction">
|
|
88
|
+
<title>1984</title>
|
|
89
|
+
</book>
|
|
90
|
+
<book id="book2" class="non-fiction">
|
|
91
|
+
<title>Sapiens</title>
|
|
92
|
+
</book>
|
|
93
|
+
</library>
|
|
94
|
+
XML
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
let(:doc) { RXerces::XML::Document.parse(xml) }
|
|
98
|
+
|
|
99
|
+
it "returns a NodeSet" do
|
|
100
|
+
result = doc.css('book')
|
|
101
|
+
expect(result).to be_a(RXerces::XML::NodeSet)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it "finds elements by tag name" do
|
|
105
|
+
books = doc.css('book')
|
|
106
|
+
expect(books.length).to eq(2)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it "finds elements by class" do
|
|
110
|
+
fiction = doc.css('.fiction')
|
|
111
|
+
expect(fiction.length).to eq(1)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it "finds elements by id" do
|
|
115
|
+
book = doc.css('#book1')
|
|
116
|
+
expect(book.length).to eq(1)
|
|
117
|
+
expect(book[0].xpath('.//title')[0].text.strip).to eq('1984')
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
it "finds elements with combined selectors" do
|
|
121
|
+
fiction_books = doc.css('book.fiction')
|
|
122
|
+
expect(fiction_books.length).to eq(1)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
67
126
|
describe "#encoding" do
|
|
68
127
|
it "returns UTF-8 for documents without explicit encoding" do
|
|
69
128
|
doc = RXerces::XML::Document.parse(simple_xml)
|
|
@@ -117,4 +176,62 @@ RSpec.describe RXerces::XML::Document do
|
|
|
117
176
|
expect(result.first.text).to eq('New content')
|
|
118
177
|
end
|
|
119
178
|
end
|
|
179
|
+
|
|
180
|
+
describe "#errors" do
|
|
181
|
+
it "returns empty array for valid XML" do
|
|
182
|
+
doc = RXerces::XML::Document.parse(simple_xml)
|
|
183
|
+
expect(doc.errors).to eq([])
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
it "returns empty array for complex valid XML" do
|
|
187
|
+
doc = RXerces::XML::Document.parse(complex_xml)
|
|
188
|
+
expect(doc.errors).to eq([])
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
context "with malformed XML" do
|
|
192
|
+
it "raises error and provides line/column information for unclosed tags" do
|
|
193
|
+
expect {
|
|
194
|
+
RXerces::XML::Document.parse('<root><item>test</root>')
|
|
195
|
+
}.to raise_error(RuntimeError, /Fatal error at line \d+, column \d+/)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
it "raises error with detailed message for multiple errors" do
|
|
199
|
+
expect {
|
|
200
|
+
RXerces::XML::Document.parse('<root><item>test</item><unclosed>')
|
|
201
|
+
}.to raise_error(RuntimeError, /Fatal error at line/)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
it "raises error for completely invalid XML" do
|
|
205
|
+
expect {
|
|
206
|
+
RXerces::XML::Document.parse('not xml at all')
|
|
207
|
+
}.to raise_error(RuntimeError, /Fatal error at line/)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
it "raises error for mismatched tags" do
|
|
211
|
+
expect {
|
|
212
|
+
RXerces::XML::Document.parse('<root><item>test</other></root>')
|
|
213
|
+
}.to raise_error(RuntimeError, /Fatal error at line/)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
context "error message format" do
|
|
218
|
+
it "includes line number in error message" do
|
|
219
|
+
expect {
|
|
220
|
+
RXerces::XML::Document.parse('<root><bad>')
|
|
221
|
+
}.to raise_error(RuntimeError, /line \d+/)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
it "includes column number in error message" do
|
|
225
|
+
expect {
|
|
226
|
+
RXerces::XML::Document.parse('<root><bad>')
|
|
227
|
+
}.to raise_error(RuntimeError, /column \d+/)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
it "describes the error type" do
|
|
231
|
+
expect {
|
|
232
|
+
RXerces::XML::Document.parse('<root><item>test</root>')
|
|
233
|
+
}.to raise_error(RuntimeError, /expected end of tag/)
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
120
237
|
end
|
data/spec/node_spec.rb
CHANGED
|
@@ -110,6 +110,40 @@ RSpec.describe RXerces::XML::Node do
|
|
|
110
110
|
end
|
|
111
111
|
end
|
|
112
112
|
|
|
113
|
+
describe "#get_attribute" do
|
|
114
|
+
it "is an alias for []" do
|
|
115
|
+
person = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
|
|
116
|
+
expect(person.get_attribute('id')).to eq('1')
|
|
117
|
+
expect(person.get_attribute('name')).to eq('Alice')
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
describe "#attribute" do
|
|
122
|
+
it "is an alias for []" do
|
|
123
|
+
person = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
|
|
124
|
+
expect(person.attribute('id')).to eq('1')
|
|
125
|
+
expect(person.attribute('name')).to eq('Alice')
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
describe "#has_attribute?" do
|
|
130
|
+
it "returns true when attribute exists" do
|
|
131
|
+
person = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
|
|
132
|
+
expect(person.has_attribute?('id')).to be true
|
|
133
|
+
expect(person.has_attribute?('name')).to be true
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
it "returns false when attribute does not exist" do
|
|
137
|
+
person = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
|
|
138
|
+
expect(person.has_attribute?('nonexistent')).to be false
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
it "returns false for non-element nodes" do
|
|
142
|
+
text_node = root.children.find { |n| n.is_a?(RXerces::XML::Text) }
|
|
143
|
+
expect(text_node.has_attribute?('anything')).to be false if text_node
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
113
147
|
describe "#children" do
|
|
114
148
|
it "returns an array of child nodes" do
|
|
115
149
|
children = root.children
|
|
@@ -158,6 +192,94 @@ RSpec.describe RXerces::XML::Node do
|
|
|
158
192
|
end
|
|
159
193
|
end
|
|
160
194
|
|
|
195
|
+
describe "#ancestors" do
|
|
196
|
+
it "returns an array of ancestor nodes" do
|
|
197
|
+
person = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
|
|
198
|
+
age = person.children.find { |n| n.name == 'age' }
|
|
199
|
+
ancestors = age.ancestors
|
|
200
|
+
|
|
201
|
+
expect(ancestors).to be_an(Array)
|
|
202
|
+
expect(ancestors.length).to eq(2)
|
|
203
|
+
expect(ancestors[0].name).to eq('person')
|
|
204
|
+
expect(ancestors[1].name).to eq('root')
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
it "returns ancestors in order from immediate parent to root" do
|
|
208
|
+
person = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
|
|
209
|
+
city = person.children.find { |n| n.name == 'city' }
|
|
210
|
+
ancestors = city.ancestors
|
|
211
|
+
|
|
212
|
+
expect(ancestors.map(&:name)).to eq(['person', 'root'])
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
it "returns empty array for root element" do
|
|
216
|
+
ancestors = root.ancestors
|
|
217
|
+
expect(ancestors).to be_an(Array)
|
|
218
|
+
expect(ancestors).to be_empty
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
it "returns only one ancestor for direct children of root" do
|
|
222
|
+
person = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
|
|
223
|
+
ancestors = person.ancestors
|
|
224
|
+
|
|
225
|
+
expect(ancestors.length).to eq(1)
|
|
226
|
+
expect(ancestors[0].name).to eq('root')
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
it "does not include the document node in ancestors" do
|
|
230
|
+
person = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
|
|
231
|
+
ancestors = person.ancestors
|
|
232
|
+
|
|
233
|
+
expect(ancestors.any? { |a| a.name == '#document' }).to be false
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
context "with selector" do
|
|
237
|
+
# Check if Xalan support is compiled in (selectors require XPath which needs Xalan)
|
|
238
|
+
xalan_available = begin
|
|
239
|
+
test_xml = '<root><item id="1">A</item></root>'
|
|
240
|
+
test_doc = RXerces::XML::Document.parse(test_xml)
|
|
241
|
+
result = test_doc.xpath('//item[@id="1"]')
|
|
242
|
+
result.length == 1
|
|
243
|
+
rescue
|
|
244
|
+
false
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
before(:all) do
|
|
248
|
+
unless xalan_available
|
|
249
|
+
skip "Xalan-C not available - ancestor selectors require Xalan-C library"
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
it "filters ancestors by tag name selector" do
|
|
254
|
+
person = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
|
|
255
|
+
city = person.children.find { |n| n.name == 'city' }
|
|
256
|
+
ancestors = city.ancestors('person')
|
|
257
|
+
|
|
258
|
+
expect(ancestors.length).to eq(1)
|
|
259
|
+
expect(ancestors[0].name).to eq('person')
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
it "filters ancestors by CSS class selector" do
|
|
263
|
+
person = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
|
|
264
|
+
city = person.children.find { |n| n.name == 'city' }
|
|
265
|
+
person_ancestors = city.ancestors('person[name]')
|
|
266
|
+
|
|
267
|
+
expect(person_ancestors.length).to eq(1)
|
|
268
|
+
expect(person_ancestors[0].name).to eq('person')
|
|
269
|
+
expect(person_ancestors[0]['name']).to eq('Alice')
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
it "returns empty array when no ancestors match selector" do
|
|
273
|
+
person = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
|
|
274
|
+
city = person.children.find { |n| n.name == 'city' }
|
|
275
|
+
ancestors = city.ancestors('nonexistent')
|
|
276
|
+
|
|
277
|
+
expect(ancestors).to be_an(Array)
|
|
278
|
+
expect(ancestors).to be_empty
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
161
283
|
describe "#attributes" do
|
|
162
284
|
it "returns a hash of attributes" do
|
|
163
285
|
person = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
|
|
@@ -281,6 +403,103 @@ RSpec.describe RXerces::XML::Node do
|
|
|
281
403
|
end
|
|
282
404
|
end
|
|
283
405
|
|
|
406
|
+
describe "#element_children" do
|
|
407
|
+
it "returns only element children, filtering out text nodes" do
|
|
408
|
+
person = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
|
|
409
|
+
element_children = person.element_children
|
|
410
|
+
|
|
411
|
+
expect(element_children).to be_an(Array)
|
|
412
|
+
expect(element_children.all? { |n| n.is_a?(RXerces::XML::Element) }).to be true
|
|
413
|
+
expect(element_children.length).to eq(2) # age and city elements
|
|
414
|
+
expect(element_children.map(&:name)).to match_array(['age', 'city'])
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
it "returns empty array for elements with no element children" do
|
|
418
|
+
person = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
|
|
419
|
+
age = person.element_children.find { |n| n.name == 'age' }
|
|
420
|
+
expect(age.element_children).to be_empty
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
it "returns empty array for text nodes" do
|
|
424
|
+
person = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
|
|
425
|
+
text_node = person.children.find { |n| n.is_a?(RXerces::XML::Text) }
|
|
426
|
+
expect(text_node.element_children).to be_empty if text_node
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
describe "#elements" do
|
|
431
|
+
it "is an alias for element_children" do
|
|
432
|
+
person = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
|
|
433
|
+
expect(person.elements.map(&:name)).to eq(person.element_children.map(&:name))
|
|
434
|
+
expect(person.elements.length).to eq(2)
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
describe "#next_element" do
|
|
439
|
+
it "returns the next element sibling, skipping text nodes" do
|
|
440
|
+
people = root.children.select { |n| n.is_a?(RXerces::XML::Element) }
|
|
441
|
+
first_person = people[0]
|
|
442
|
+
next_element = first_person.next_element
|
|
443
|
+
|
|
444
|
+
expect(next_element).to be_a(RXerces::XML::Element)
|
|
445
|
+
expect(next_element.name).to eq('person')
|
|
446
|
+
expect(next_element['id']).to eq('2')
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
it "returns nil when there is no next element" do
|
|
450
|
+
people = root.children.select { |n| n.is_a?(RXerces::XML::Element) }
|
|
451
|
+
last_person = people.last
|
|
452
|
+
expect(last_person.next_element).to be_nil
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
it "can navigate through all element siblings" do
|
|
456
|
+
first_element = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
|
|
457
|
+
siblings = []
|
|
458
|
+
current = first_element
|
|
459
|
+
|
|
460
|
+
while current
|
|
461
|
+
siblings << current
|
|
462
|
+
current = current.next_element
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
expect(siblings.length).to eq(2)
|
|
466
|
+
expect(siblings[0]['id']).to eq('1')
|
|
467
|
+
expect(siblings[1]['id']).to eq('2')
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
describe "#previous_element" do
|
|
472
|
+
it "returns the previous element sibling, skipping text nodes" do
|
|
473
|
+
people = root.children.select { |n| n.is_a?(RXerces::XML::Element) }
|
|
474
|
+
second_person = people[1]
|
|
475
|
+
prev_element = second_person.previous_element
|
|
476
|
+
|
|
477
|
+
expect(prev_element).to be_a(RXerces::XML::Element)
|
|
478
|
+
expect(prev_element.name).to eq('person')
|
|
479
|
+
expect(prev_element['id']).to eq('1')
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
it "returns nil when there is no previous element" do
|
|
483
|
+
first_element = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
|
|
484
|
+
expect(first_element.previous_element).to be_nil
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
it "can navigate backward through all element siblings" do
|
|
488
|
+
last_element = root.children.select { |n| n.is_a?(RXerces::XML::Element) }.last
|
|
489
|
+
siblings = []
|
|
490
|
+
current = last_element
|
|
491
|
+
|
|
492
|
+
while current
|
|
493
|
+
siblings.unshift(current)
|
|
494
|
+
current = current.previous_element
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
expect(siblings.length).to eq(2)
|
|
498
|
+
expect(siblings[0]['id']).to eq('1')
|
|
499
|
+
expect(siblings[1]['id']).to eq('2')
|
|
500
|
+
end
|
|
501
|
+
end
|
|
502
|
+
|
|
284
503
|
describe "#add_child" do
|
|
285
504
|
it "adds a text node from a string" do
|
|
286
505
|
person = root.children.find { |n| n.is_a?(RXerces::XML::Element) }
|
|
@@ -333,6 +552,63 @@ RSpec.describe RXerces::XML::Node do
|
|
|
333
552
|
xml_output = simple_doc.to_s
|
|
334
553
|
expect(xml_output).to include("Content")
|
|
335
554
|
end
|
|
555
|
+
|
|
556
|
+
context "with nodes from different documents" do
|
|
557
|
+
it "raises error when adding node from different document" do
|
|
558
|
+
doc1 = RXerces::XML::Document.parse('<root><item>one</item></root>')
|
|
559
|
+
doc2 = RXerces::XML::Document.parse('<other><item>two</item></other>')
|
|
560
|
+
|
|
561
|
+
root1 = doc1.root
|
|
562
|
+
item2 = doc2.root.children.find { |n| n.is_a?(RXerces::XML::Element) }
|
|
563
|
+
|
|
564
|
+
expect {
|
|
565
|
+
root1.add_child(item2)
|
|
566
|
+
}.to raise_error(RuntimeError, /belongs to a different document/)
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
it "provides helpful error message mentioning importNode" do
|
|
570
|
+
doc1 = RXerces::XML::Document.parse('<root></root>')
|
|
571
|
+
doc2 = RXerces::XML::Document.parse('<other><child/></other>')
|
|
572
|
+
|
|
573
|
+
expect {
|
|
574
|
+
doc1.root.add_child(doc2.root.children.first)
|
|
575
|
+
}.to raise_error(RuntimeError, /importNode/)
|
|
576
|
+
end
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
context "when child already has a parent" do
|
|
580
|
+
it "moves node from one parent to another (detaches automatically)" do
|
|
581
|
+
doc = RXerces::XML::Document.parse('<root><parent1><child>text</child></parent1><parent2/></root>')
|
|
582
|
+
parent1 = doc.xpath('//parent1').first
|
|
583
|
+
parent2 = doc.xpath('//parent2').first
|
|
584
|
+
child = doc.xpath('//child').first
|
|
585
|
+
|
|
586
|
+
# Verify initial state
|
|
587
|
+
expect(parent1.children.select { |n| n.is_a?(RXerces::XML::Element) }.length).to eq(1)
|
|
588
|
+
expect(parent2.children.select { |n| n.is_a?(RXerces::XML::Element) }.length).to eq(0)
|
|
589
|
+
|
|
590
|
+
# Move child from parent1 to parent2
|
|
591
|
+
parent2.add_child(child)
|
|
592
|
+
|
|
593
|
+
# Child should now be under parent2, not parent1
|
|
594
|
+
expect(parent1.children.select { |n| n.is_a?(RXerces::XML::Element) }.length).to eq(0)
|
|
595
|
+
expect(parent2.children.select { |n| n.is_a?(RXerces::XML::Element) }.length).to eq(1)
|
|
596
|
+
expect(doc.xpath('//parent2/child').length).to eq(1)
|
|
597
|
+
expect(doc.xpath('//parent1/child').length).to eq(0)
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
it "preserves node content when moving" do
|
|
601
|
+
doc = RXerces::XML::Document.parse('<root><a><item>content</item></a><b/></root>')
|
|
602
|
+
a = doc.xpath('//a').first
|
|
603
|
+
b = doc.xpath('//b').first
|
|
604
|
+
item = doc.xpath('//item').first
|
|
605
|
+
|
|
606
|
+
b.add_child(item)
|
|
607
|
+
|
|
608
|
+
expect(item.text).to eq('content')
|
|
609
|
+
expect(doc.xpath('//b/item').first.text).to eq('content')
|
|
610
|
+
end
|
|
611
|
+
end
|
|
336
612
|
end
|
|
337
613
|
|
|
338
614
|
describe "#remove" do
|
|
@@ -547,13 +823,141 @@ RSpec.describe RXerces::XML::Node do
|
|
|
547
823
|
end
|
|
548
824
|
end
|
|
549
825
|
|
|
826
|
+
describe "#at_css" do
|
|
827
|
+
# Check if Xalan support is compiled in (CSS requires XPath which needs Xalan)
|
|
828
|
+
xalan_available = begin
|
|
829
|
+
test_xml = '<root><item id="1">A</item><item id="2">B</item></root>'
|
|
830
|
+
test_doc = RXerces::XML::Document.parse(test_xml)
|
|
831
|
+
result = test_doc.xpath('//item[@id="1"]')
|
|
832
|
+
result.length == 1
|
|
833
|
+
rescue
|
|
834
|
+
false
|
|
835
|
+
end
|
|
836
|
+
|
|
837
|
+
before(:all) do
|
|
838
|
+
unless xalan_available
|
|
839
|
+
skip "Xalan-C not available - CSS selectors require Xalan-C library"
|
|
840
|
+
end
|
|
841
|
+
end
|
|
842
|
+
|
|
843
|
+
it "is an alias for at (which uses CSS converted to XPath)" do
|
|
844
|
+
xml = '<root><item class="foo">First</item><item class="bar">Second</item></root>'
|
|
845
|
+
doc = RXerces::XML::Document.parse(xml)
|
|
846
|
+
result = doc.root.at_css('.foo')
|
|
847
|
+
expect(result).to be_a(RXerces::XML::Element)
|
|
848
|
+
expect(result.text).to eq('First')
|
|
849
|
+
end
|
|
850
|
+
|
|
851
|
+
it "returns the first matching element" do
|
|
852
|
+
xml = '<root><item>A</item><item>B</item></root>'
|
|
853
|
+
doc = RXerces::XML::Document.parse(xml)
|
|
854
|
+
result = doc.root.at_css('item')
|
|
855
|
+
expect(result.text).to eq('A')
|
|
856
|
+
end
|
|
857
|
+
|
|
858
|
+
it "returns nil when no match found" do
|
|
859
|
+
xml = '<root><item>A</item></root>'
|
|
860
|
+
doc = RXerces::XML::Document.parse(xml)
|
|
861
|
+
result = doc.root.at_css('nonexistent')
|
|
862
|
+
expect(result).to be_nil
|
|
863
|
+
end
|
|
864
|
+
end
|
|
865
|
+
|
|
550
866
|
describe "#css" do
|
|
551
|
-
|
|
552
|
-
|
|
867
|
+
# Check if Xalan support is compiled in (CSS requires XPath which needs Xalan)
|
|
868
|
+
xalan_available = begin
|
|
869
|
+
test_xml = '<root><item id="1">A</item><item id="2">B</item></root>'
|
|
870
|
+
test_doc = RXerces::XML::Document.parse(test_xml)
|
|
871
|
+
result = test_doc.xpath('//item[@id="1"]')
|
|
872
|
+
result.length == 1
|
|
873
|
+
rescue
|
|
874
|
+
false
|
|
875
|
+
end
|
|
876
|
+
|
|
877
|
+
before(:all) do
|
|
878
|
+
unless xalan_available
|
|
879
|
+
skip "Xalan-C not available - CSS selectors require Xalan-C library"
|
|
880
|
+
end
|
|
553
881
|
end
|
|
554
882
|
|
|
555
|
-
|
|
556
|
-
|
|
883
|
+
let(:xml) do
|
|
884
|
+
<<-XML
|
|
885
|
+
<library>
|
|
886
|
+
<book id="book1" class="fiction bestseller">
|
|
887
|
+
<title>1984</title>
|
|
888
|
+
<author>George Orwell</author>
|
|
889
|
+
</book>
|
|
890
|
+
<book id="book2" class="fiction">
|
|
891
|
+
<title>Brave New World</title>
|
|
892
|
+
<author>Aldous Huxley</author>
|
|
893
|
+
</book>
|
|
894
|
+
<book id="book3" class="non-fiction">
|
|
895
|
+
<title>Sapiens</title>
|
|
896
|
+
<author>Yuval Noah Harari</author>
|
|
897
|
+
</book>
|
|
898
|
+
</library>
|
|
899
|
+
XML
|
|
900
|
+
end
|
|
901
|
+
|
|
902
|
+
let(:doc) { RXerces::XML::Document.parse(xml) }
|
|
903
|
+
let(:root) { doc.root }
|
|
904
|
+
|
|
905
|
+
it "finds elements by tag name" do
|
|
906
|
+
books = root.css('book')
|
|
907
|
+
expect(books.length).to eq(3)
|
|
908
|
+
end
|
|
909
|
+
|
|
910
|
+
it "finds elements by class" do
|
|
911
|
+
fiction = root.css('.fiction')
|
|
912
|
+
expect(fiction.length).to eq(2)
|
|
913
|
+
end
|
|
914
|
+
|
|
915
|
+
it "finds elements by id" do
|
|
916
|
+
book = root.css('#book1')
|
|
917
|
+
expect(book.length).to eq(1)
|
|
918
|
+
expect(book[0].xpath('.//title')[0].text.strip).to eq('1984')
|
|
919
|
+
end
|
|
920
|
+
|
|
921
|
+
it "finds elements by tag and class" do
|
|
922
|
+
fiction_books = root.css('book.fiction')
|
|
923
|
+
expect(fiction_books.length).to eq(2)
|
|
924
|
+
end
|
|
925
|
+
|
|
926
|
+
it "finds elements by tag and id" do
|
|
927
|
+
book = root.css('book#book2')
|
|
928
|
+
expect(book.length).to eq(1)
|
|
929
|
+
expect(book[0].xpath('.//title')[0].text.strip).to eq('Brave New World')
|
|
930
|
+
end
|
|
931
|
+
|
|
932
|
+
it "finds elements with attribute selector" do
|
|
933
|
+
books_with_id = root.css('book[id]')
|
|
934
|
+
expect(books_with_id.length).to eq(3)
|
|
935
|
+
end
|
|
936
|
+
|
|
937
|
+
it "finds elements with attribute value selector" do
|
|
938
|
+
book = root.css('book[id=book3]')
|
|
939
|
+
expect(book.length).to eq(1)
|
|
940
|
+
expect(book[0].xpath('.//title')[0].text.strip).to eq('Sapiens')
|
|
941
|
+
end
|
|
942
|
+
|
|
943
|
+
it "handles descendant combinator" do
|
|
944
|
+
titles = root.css('library title')
|
|
945
|
+
expect(titles.length).to eq(3)
|
|
946
|
+
end
|
|
947
|
+
|
|
948
|
+
it "handles child combinator" do
|
|
949
|
+
books = root.css('library > book')
|
|
950
|
+
expect(books.length).to eq(3)
|
|
951
|
+
end
|
|
952
|
+
|
|
953
|
+
it "finds nested elements" do
|
|
954
|
+
authors = root.css('book author')
|
|
955
|
+
expect(authors.length).to eq(3)
|
|
956
|
+
end
|
|
957
|
+
|
|
958
|
+
it "combines multiple selectors" do
|
|
959
|
+
result = root.css('book.fiction title')
|
|
960
|
+
expect(result.length).to eq(2)
|
|
557
961
|
end
|
|
558
962
|
end
|
|
559
963
|
end
|
data/spec/nodeset_spec.rb
CHANGED
|
@@ -86,6 +86,65 @@ RSpec.describe RXerces::XML::NodeSet do
|
|
|
86
86
|
end
|
|
87
87
|
end
|
|
88
88
|
|
|
89
|
+
describe "#first" do
|
|
90
|
+
it "returns the first node" do
|
|
91
|
+
first = nodeset.first
|
|
92
|
+
expect(first).to be_a(RXerces::XML::Element)
|
|
93
|
+
expect(first.text.strip).to eq('First')
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it "returns nil for empty nodeset" do
|
|
97
|
+
expect(empty_nodeset.first).to be_nil
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
describe "#last" do
|
|
102
|
+
it "returns the last node" do
|
|
103
|
+
last = nodeset.last
|
|
104
|
+
expect(last).to be_a(RXerces::XML::Element)
|
|
105
|
+
expect(last.text.strip).to eq('Third')
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it "returns nil for empty nodeset" do
|
|
109
|
+
expect(empty_nodeset.last).to be_nil
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
describe "#empty?" do
|
|
114
|
+
it "returns false for non-empty nodeset" do
|
|
115
|
+
expect(nodeset.empty?).to be false
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it "returns true for empty nodeset" do
|
|
119
|
+
expect(empty_nodeset.empty?).to be true
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
describe "#inner_html" do
|
|
124
|
+
it "returns concatenated inner_html of all nodes" do
|
|
125
|
+
result = nodeset.inner_html
|
|
126
|
+
expect(result).to be_a(String)
|
|
127
|
+
expect(result).to eq('FirstSecondThird')
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
it "returns empty string for empty nodeset" do
|
|
131
|
+
expect(empty_nodeset.inner_html).to eq('')
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it "includes child elements in inner_html" do
|
|
135
|
+
xml_with_children = <<-XML
|
|
136
|
+
<root>
|
|
137
|
+
<div><span>A</span></div>
|
|
138
|
+
<div><span>B</span></div>
|
|
139
|
+
</root>
|
|
140
|
+
XML
|
|
141
|
+
doc = RXerces::XML::Document.parse(xml_with_children)
|
|
142
|
+
divs = doc.xpath('//div')
|
|
143
|
+
expect(divs.inner_html).to include('<span>A</span>')
|
|
144
|
+
expect(divs.inner_html).to include('<span>B</span>')
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
89
148
|
it "includes Enumerable" do
|
|
90
149
|
expect(RXerces::XML::NodeSet.ancestors).to include(Enumerable)
|
|
91
150
|
end
|