moxml 0.1.26 → 0.2.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
- data/.github/workflows/windows.yml +62 -0
- data/Gemfile +3 -1
- data/README.adoc +69 -6
- data/lib/compat/opal/moxml_boot.rb +5 -0
- data/lib/moxml/adapter/base.rb +53 -49
- data/lib/moxml/adapter/customized_leptris/declaration.rb +32 -0
- data/lib/moxml/adapter/customized_leptris/doctype.rb +31 -0
- data/lib/moxml/adapter/customized_leptris/entity_reference.rb +12 -0
- data/lib/moxml/adapter/customized_leptris/text_segment.rb +26 -0
- data/lib/moxml/adapter/customized_leptris.rb +16 -0
- data/lib/moxml/adapter/customized_libxml/cdata.rb +3 -12
- data/lib/moxml/adapter/customized_libxml/comment.rb +2 -9
- data/lib/moxml/adapter/customized_libxml/declaration.rb +1 -10
- data/lib/moxml/adapter/customized_libxml/node.rb +8 -0
- data/lib/moxml/adapter/customized_libxml/processing_instruction.rb +2 -10
- data/lib/moxml/adapter/customized_libxml/text.rb +2 -9
- data/lib/moxml/adapter/customized_oga/entity_decoder.rb +37 -0
- data/lib/moxml/adapter/customized_oga/raw_value_override.rb +52 -0
- data/lib/moxml/adapter/customized_oga/xml_generator.rb +4 -34
- data/lib/moxml/adapter/customized_oga.rb +2 -0
- data/lib/moxml/adapter/customized_ox/entity_reference.rb +1 -17
- data/lib/moxml/adapter/customized_rexml/entity_reference.rb +1 -11
- data/lib/moxml/adapter/headed_ox.rb +9 -128
- data/lib/moxml/adapter/leptris.rb +965 -0
- data/lib/moxml/adapter/libxml.rb +131 -99
- data/lib/moxml/adapter/nokogiri.rb +29 -8
- data/lib/moxml/adapter/oga.rb +91 -54
- data/lib/moxml/adapter/ox.rb +93 -137
- data/lib/moxml/adapter/rexml.rb +61 -50
- data/lib/moxml/adapter.rb +2 -1
- data/lib/moxml/attribute.rb +8 -4
- data/lib/moxml/attribute_resolver.rb +189 -0
- data/lib/moxml/config.rb +29 -3
- data/lib/moxml/context.rb +12 -0
- data/lib/moxml/element.rb +29 -12
- data/lib/moxml/entity/reference.rb +28 -0
- data/lib/moxml/entity.rb +125 -0
- data/lib/moxml/node.rb +90 -1
- data/lib/moxml/sax/namespace_splitter.rb +5 -2
- data/lib/moxml/signature/model/key/dsa_key_value.rb +1 -2
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xml_emitter.rb +71 -0
- data/lib/moxml/xpath/compiler.rb +60 -7
- data/lib/moxml/xpath/parser.rb +22 -15
- data/lib/moxml.rb +3 -0
- data/spec/examples/readme_examples_spec.rb +0 -4
- data/spec/examples/xpath_examples_spec.rb +0 -11
- data/spec/integration/all_adapters_spec.rb +17 -0
- data/spec/integration/sax_parity_spec.rb +58 -0
- data/spec/integration/shared_examples/edge_cases.rb +0 -10
- data/spec/integration/shared_examples/integration_workflows.rb +0 -10
- data/spec/integration/shared_examples/node_wrappers/attribute_behavior.rb +98 -0
- data/spec/integration/shared_examples/node_wrappers/namespace_behavior.rb +26 -0
- data/spec/integration/shared_examples/node_wrappers/node_behavior.rb +0 -8
- data/spec/moxml/adapter/leptris_spec.rb +75 -0
- data/spec/moxml/adapter/ox_spec.rb +20 -6
- data/spec/moxml/adapter/platform_spec.rb +15 -2
- data/spec/moxml/adapter/shared_examples/adapter_contract.rb +180 -0
- data/spec/moxml/attribute_resolver_spec.rb +108 -0
- data/spec/moxml/doctype_spec.rb +1 -1
- data/spec/moxml/entity_spec.rb +76 -0
- data/spec/moxml/node_spec.rb +104 -0
- data/spec/moxml/sax_entity_parity_spec.rb +358 -0
- data/spec/moxml/xml_emitter_spec.rb +64 -0
- data/spec/moxml/xpath/axes_spec.rb +72 -0
- data/spec/moxml/xpath/parser_spec.rb +6 -0
- data/spec/moxml/xpath_capabilities_spec.rb +5 -3
- data/spec/performance/benchmark_spec.rb +13 -6
- data/spec/performance/thread_safety_spec.rb +0 -4
- data/spec/spec_helper.rb +5 -1
- metadata +21 -2
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SAX event parity across adapters: every bridge must translate its
|
|
4
|
+
# native SAX stream into the same Moxml event sequence — same element
|
|
5
|
+
# names (qualified), same attribute/declaration split (nil key = the
|
|
6
|
+
# default namespace), same characters.
|
|
7
|
+
RSpec.describe "SAX parity across adapters" do
|
|
8
|
+
let(:xml) do
|
|
9
|
+
<<~XML
|
|
10
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
11
|
+
<root xmlns="http://d.org" xmlns:p="http://p.org" kind="a"><p:child id="1">text</p:child></root>
|
|
12
|
+
XML
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def recorded_events(adapter_name)
|
|
16
|
+
events = []
|
|
17
|
+
handler = Class.new(Moxml::SAX::Handler) do
|
|
18
|
+
define_method(:track) { |*event| events << event }
|
|
19
|
+
define_method(:on_start_document) { track(:start_document) }
|
|
20
|
+
define_method(:on_end_document) { track(:end_document) }
|
|
21
|
+
define_method(:on_start_element) { |n, a, ns| track(:start_element, n, a, ns) }
|
|
22
|
+
define_method(:on_end_element) { |n| track(:end_element, n) }
|
|
23
|
+
define_method(:on_characters) { |t| track(:characters, t) }
|
|
24
|
+
end
|
|
25
|
+
Moxml.new(adapter_name).sax_parse(xml, handler.new)
|
|
26
|
+
events
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def assert_parity(adapter_name)
|
|
30
|
+
# REXML and Oga report document-level whitespace (between the
|
|
31
|
+
# declaration and the root) as characters; parity is over the
|
|
32
|
+
# meaningful event stream.
|
|
33
|
+
events = recorded_events(adapter_name)
|
|
34
|
+
.reject { |event| event[0] == :characters && event[1].to_s.strip.empty? }
|
|
35
|
+
|
|
36
|
+
expect(events.first).to eq([:start_document])
|
|
37
|
+
expect(events[1]).to eq(
|
|
38
|
+
[:start_element, "root",
|
|
39
|
+
{ "kind" => "a" },
|
|
40
|
+
{ nil => "http://d.org", "p" => "http://p.org" }],
|
|
41
|
+
)
|
|
42
|
+
expect(events[2]).to eq([:start_element, "p:child", { "id" => "1" }, {}])
|
|
43
|
+
expect(events).to include([:characters, "text"])
|
|
44
|
+
expect(events).to include([:end_element, "p:child"])
|
|
45
|
+
expect(events).to include([:end_element, "root"])
|
|
46
|
+
expect(events.last).to eq([:end_document])
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
adapters = %i[nokogiri oga rexml ox headed_ox libxml]
|
|
50
|
+
adapters << :leptris if Object.const_defined?(:Leptris) &&
|
|
51
|
+
Leptris::XML::Document.respond_to?(:create)
|
|
52
|
+
|
|
53
|
+
adapters.each do |adapter_name|
|
|
54
|
+
it "emits the same event stream via #{adapter_name}" do
|
|
55
|
+
assert_parity(adapter_name)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -80,9 +80,6 @@ RSpec.shared_examples "Moxml Edge Cases" do
|
|
|
80
80
|
|
|
81
81
|
describe "namespace edge cases" do
|
|
82
82
|
it "handles default namespace changes" do
|
|
83
|
-
if context.config.adapter_name == :ox
|
|
84
|
-
pending "Ox doesn't have a native XPath"
|
|
85
|
-
end
|
|
86
83
|
if context.config.adapter_name == :headed_ox
|
|
87
84
|
skip "HeadedOx limitation: Namespace methods not implemented in adapter. Requires Ox namespace API enhancement. See docs/_pages/headed-ox-limitations.adoc"
|
|
88
85
|
end
|
|
@@ -106,9 +103,6 @@ RSpec.shared_examples "Moxml Edge Cases" do
|
|
|
106
103
|
end
|
|
107
104
|
|
|
108
105
|
it "handles recursive namespace definitions" do
|
|
109
|
-
if context.config.adapter_name == :ox
|
|
110
|
-
pending "Ox doesn't have a native XPath"
|
|
111
|
-
end
|
|
112
106
|
if context.config.adapter_name == :headed_ox
|
|
113
107
|
skip "HeadedOx limitation: Namespace methods not implemented in adapter. Requires Ox namespace API enhancement. See docs/_pages/headed-ox-limitations.adoc"
|
|
114
108
|
end
|
|
@@ -132,10 +126,6 @@ RSpec.shared_examples "Moxml Edge Cases" do
|
|
|
132
126
|
if context.config.adapter_name == :headed_ox
|
|
133
127
|
skip "HeadedOx limitation: Namespace-prefixed attribute access needs Ox namespace API enhancement. See docs/_pages/headed-ox-limitations.adoc"
|
|
134
128
|
end
|
|
135
|
-
if context.config.adapter_name == :ox
|
|
136
|
-
skip "Ox doesn't have a native XPath"
|
|
137
|
-
end
|
|
138
|
-
|
|
139
129
|
xml = <<~XML
|
|
140
130
|
<root xmlns:a="http://a.org" xmlns:b="http://b.org">
|
|
141
131
|
<element a:id="1" b:id="2"/>
|
|
@@ -56,9 +56,6 @@ RSpec.shared_examples "Moxml Integration" do
|
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
it "handles xpath queries" do
|
|
59
|
-
if context.config.adapter_name == :ox
|
|
60
|
-
pending "Ox doesn't support namespace-aware XPath with predicates"
|
|
61
|
-
end
|
|
62
59
|
if context.config.adapter_name == :headed_ox
|
|
63
60
|
skip "HeadedOx limitation: Namespace-aware XPath with predicates needs investigation. See docs/_pages/headed-ox-limitations.adoc"
|
|
64
61
|
end
|
|
@@ -76,9 +73,6 @@ RSpec.shared_examples "Moxml Integration" do
|
|
|
76
73
|
|
|
77
74
|
describe "namespace handling" do
|
|
78
75
|
it "handles complex namespace scenarios" do
|
|
79
|
-
if context.config.adapter_name == :ox
|
|
80
|
-
pending "Ox doesn't have a native XPath"
|
|
81
|
-
end
|
|
82
76
|
if context.config.adapter_name == :headed_ox
|
|
83
77
|
skip "HeadedOx limitation: Namespace methods not implemented in adapter. Requires Ox namespace API enhancement. See docs/_pages/headed-ox-limitations.adoc"
|
|
84
78
|
end
|
|
@@ -125,10 +119,6 @@ RSpec.shared_examples "Moxml Integration" do
|
|
|
125
119
|
if context.config.adapter_name == :headed_ox
|
|
126
120
|
skip "HeadedOx limitation: Parent setter not implemented. Requires Ox node reparenting API. See docs/_pages/headed-ox-limitations.adoc"
|
|
127
121
|
end
|
|
128
|
-
if context.config.adapter_name == :ox
|
|
129
|
-
skip "Ox doesn't have a native XPath"
|
|
130
|
-
end
|
|
131
|
-
|
|
132
122
|
# Move nodes
|
|
133
123
|
b_node = doc.at_xpath("//b")
|
|
134
124
|
a_node = doc.at_xpath("//a")
|
|
@@ -65,6 +65,65 @@ RSpec.shared_examples "Moxml::Attribute" do
|
|
|
65
65
|
expect(attribute.namespace.prefix).to eq("other")
|
|
66
66
|
expect(attribute.to_s).to eq('other:attr="value"')
|
|
67
67
|
end
|
|
68
|
+
|
|
69
|
+
it "replaces by expanded name across prefix spellings" do
|
|
70
|
+
element["ns:attr"] = "first"
|
|
71
|
+
element.add_namespace("alias", "http://example.org")
|
|
72
|
+
element["alias:attr"] = "second"
|
|
73
|
+
|
|
74
|
+
expect(element["ns:attr"]).to eq("second")
|
|
75
|
+
expect(element.attributes.size).to eq(1)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
describe "expanded-name writes (AttributeResolver)" do
|
|
80
|
+
before do
|
|
81
|
+
element.add_namespace("p", "http://x.org")
|
|
82
|
+
element.add_namespace("q", "http://x.org")
|
|
83
|
+
element["q:type"] = "namespaced"
|
|
84
|
+
element["type"] = "bare"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it "does not clobber a namespaced attribute sharing the local name" do
|
|
88
|
+
element["type"] = "changed"
|
|
89
|
+
|
|
90
|
+
expect(element["type"]).to eq("changed")
|
|
91
|
+
expect(element["q:type"]).to eq("namespaced")
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it "overwrites by expanded name regardless of prefix spelling" do
|
|
95
|
+
element["p:type"] = "overwritten"
|
|
96
|
+
|
|
97
|
+
expect(element["q:type"]).to eq("overwritten")
|
|
98
|
+
expect(element.attributes.size).to eq(2)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it "stores writes through undeclared prefixes raw for later resolution" do
|
|
102
|
+
element["z:type"] = "value"
|
|
103
|
+
|
|
104
|
+
# No declaration in scope: the read resolves to nothing, but
|
|
105
|
+
# the attribute is stored (detached builds attach under the
|
|
106
|
+
# declaring ancestor later).
|
|
107
|
+
expect(element["z:type"]).to be_nil
|
|
108
|
+
expect(element.attributes.size).to eq(3)
|
|
109
|
+
|
|
110
|
+
element.add_namespace("z", "http://z.org")
|
|
111
|
+
expect(element["z:type"]).to eq("value")
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it "removes the bare attribute without touching the namespaced one" do
|
|
115
|
+
element.remove_attribute("type")
|
|
116
|
+
|
|
117
|
+
expect(element["type"]).to be_nil
|
|
118
|
+
expect(element["q:type"]).to eq("namespaced")
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
it "removes by expanded name regardless of prefix spelling" do
|
|
122
|
+
element.remove_attribute("p:type")
|
|
123
|
+
|
|
124
|
+
expect(element["q:type"]).to be_nil
|
|
125
|
+
expect(element["type"]).to eq("bare")
|
|
126
|
+
end
|
|
68
127
|
end
|
|
69
128
|
|
|
70
129
|
describe "manipulation" do
|
|
@@ -162,4 +221,43 @@ RSpec.shared_examples "Moxml::Attribute" do
|
|
|
162
221
|
end
|
|
163
222
|
end
|
|
164
223
|
end
|
|
224
|
+
|
|
225
|
+
describe "unified XML-correct name resolution" do
|
|
226
|
+
let(:context) { Moxml.new }
|
|
227
|
+
let(:doc) do
|
|
228
|
+
context.parse(
|
|
229
|
+
%(<root xmlns:p="urn:1"><child xmlns:q="urn:1" q:type="v" type="plain" xml:lang="en"/></root>),
|
|
230
|
+
)
|
|
231
|
+
end
|
|
232
|
+
let(:child) { doc.root.children.find(&:element?) }
|
|
233
|
+
|
|
234
|
+
it "matches prefixed lookups by expanded name across redeclared prefixes" do
|
|
235
|
+
# p and q both bind urn:1; the attribute is written q:type
|
|
236
|
+
expect(child["p:type"]).to eq("v")
|
|
237
|
+
expect(child["q:type"]).to eq("v")
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
it "keeps bare and namespaced attributes distinct" do
|
|
241
|
+
expect(child["type"]).to eq("plain")
|
|
242
|
+
expect(child[:type]).to eq("plain")
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
it "resolves the prebound xml prefix without any declaration" do
|
|
246
|
+
expect(child["xml:lang"]).to eq("en")
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
it "returns nil for undeclared prefixes — no prefix-string fallback" do
|
|
250
|
+
expect(child["undeclared:type"]).to be_nil
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
it "never treats namespace declarations as attributes" do
|
|
254
|
+
expect(child["xmlns:q"]).to be_nil
|
|
255
|
+
expect(child["xmlns"]).to be_nil
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
it "attribute(name) agrees with []" do
|
|
259
|
+
expect(child.attribute("p:type").value).to eq("v")
|
|
260
|
+
expect(child.attribute("undeclared:type")).to be_nil
|
|
261
|
+
end
|
|
262
|
+
end
|
|
165
263
|
end
|
|
@@ -170,6 +170,32 @@ RSpec.shared_examples "Moxml::Namespace" do
|
|
|
170
170
|
expect(prefixes).to include("xs")
|
|
171
171
|
end
|
|
172
172
|
|
|
173
|
+
it "sees ancestor declarations made after a first read (cache invalidation)" do
|
|
174
|
+
root = doc.create_element("root")
|
|
175
|
+
root.add_namespace("a", "http://a.org")
|
|
176
|
+
child = doc.create_element("child")
|
|
177
|
+
root.add_child(child)
|
|
178
|
+
child.in_scope_namespaces # materialize the scope cache
|
|
179
|
+
|
|
180
|
+
root.add_namespace("b", "http://b.org")
|
|
181
|
+
|
|
182
|
+
expect(child.in_scope_namespaces.map(&:prefix)).to include("b")
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
it "recomputes the scope after the subtree is reparented" do
|
|
186
|
+
source = doc.create_element("source")
|
|
187
|
+
source.add_namespace("s", "http://s.org")
|
|
188
|
+
child = doc.create_element("child")
|
|
189
|
+
source.add_child(child)
|
|
190
|
+
child.in_scope_namespaces # materialize the scope cache
|
|
191
|
+
|
|
192
|
+
target = doc.create_element("target")
|
|
193
|
+
child.remove
|
|
194
|
+
target.add_child(child)
|
|
195
|
+
|
|
196
|
+
expect(child.in_scope_namespaces.map(&:prefix)).not_to include("s")
|
|
197
|
+
end
|
|
198
|
+
|
|
173
199
|
it "collects namespaces from multiple ancestor levels" do
|
|
174
200
|
root = doc.create_element("root")
|
|
175
201
|
root.add_namespace("xs", "http://www.w3.org/2001/XMLSchema")
|
|
@@ -96,20 +96,12 @@ RSpec.shared_examples "Moxml::Node" do
|
|
|
96
96
|
let(:doc) { context.parse("<root><a><b>1</b></a><a><b>2</b></a></root>") }
|
|
97
97
|
|
|
98
98
|
it "finds nodes by xpath" do
|
|
99
|
-
if context.config.adapter_name == :ox
|
|
100
|
-
pending "Ox doesn't have a native XPath"
|
|
101
|
-
end
|
|
102
|
-
|
|
103
99
|
nodes = doc.xpath("//b")
|
|
104
100
|
expect(nodes.size).to eq(2)
|
|
105
101
|
expect(nodes.map(&:text)).to eq(%w[1 2])
|
|
106
102
|
end
|
|
107
103
|
|
|
108
104
|
it "finds first node by xpath" do
|
|
109
|
-
if context.config.adapter_name == :ox
|
|
110
|
-
pending "Ox doesn't have a native XPath"
|
|
111
|
-
end
|
|
112
|
-
|
|
113
105
|
node = doc.at_xpath("//b")
|
|
114
106
|
expect(node.text).to eq("1")
|
|
115
107
|
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
begin
|
|
4
|
+
require "leptris"
|
|
5
|
+
rescue LoadError
|
|
6
|
+
# Leptris gem not available - skip all specs in this file
|
|
7
|
+
return
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Programmatic document construction relies on the C API added for the
|
|
11
|
+
# Moxml integration (leptris_document_create / leptris_document_set_root,
|
|
12
|
+
# exposed as Leptris::XML::Document.create / #root=). Released leptris
|
|
13
|
+
# 1.1.x does not ship it yet; skip instead of failing until the binding
|
|
14
|
+
# release catches up.
|
|
15
|
+
unless Leptris::XML::Document.respond_to?(:create)
|
|
16
|
+
# String form: the adapter constant is not loaded in this branch —
|
|
17
|
+
# resolving it would raise NameError before the skip applies.
|
|
18
|
+
RSpec.describe "Moxml::Adapter::Leptris" do
|
|
19
|
+
it "waits for a leptris release with programmatic document construction" do
|
|
20
|
+
skip "requires leptris with Leptris::XML::Document.create (unreleased C API)"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
return
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
require "moxml/adapter/leptris"
|
|
28
|
+
|
|
29
|
+
RSpec.describe Moxml::Adapter::Leptris do
|
|
30
|
+
around do |example|
|
|
31
|
+
Moxml.with_config(:leptris, true, "UTF-8") do
|
|
32
|
+
example.run
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it_behaves_like "xml adapter"
|
|
37
|
+
|
|
38
|
+
describe "native xpath dispatch" do
|
|
39
|
+
let(:ctx) { Moxml.new(:leptris) }
|
|
40
|
+
let(:doc) do
|
|
41
|
+
ctx.parse(<<~XML)
|
|
42
|
+
<root xmlns:p="http://x.org">
|
|
43
|
+
<item id="1" p:kind="a">alpha</item>
|
|
44
|
+
<item id="2">beta</item>
|
|
45
|
+
</root>
|
|
46
|
+
XML
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it "evaluates document-context queries on the native engine" do
|
|
50
|
+
expect(doc.xpath("//item[@id='2']").map(&:text)).to eq(["beta"])
|
|
51
|
+
expect(doc.xpath("count(//item)")).to eq(2.0)
|
|
52
|
+
expect(doc.xpath("//item[@p:kind='a']").map { |n| n["id"] }).to eq(["1"])
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it "falls back to the Ruby engine for attribute-node results" do
|
|
56
|
+
attrs = doc.xpath("//item/@id")
|
|
57
|
+
expect(attrs.map(&:name)).to eq(%w[id id])
|
|
58
|
+
expect(attrs.map(&:value)).to eq(%w[1 2])
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it "falls back to the Ruby engine for element-context queries" do
|
|
62
|
+
expect(doc.root.xpath(".//item").size).to eq(2)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it "falls back to the Ruby engine for the xmlns: reserved prefix" do
|
|
66
|
+
expect(doc.xpath("//xmlns:item").size).to eq(0)
|
|
67
|
+
prefixed = ctx.parse('<r xmlns="http://d.org"><item/></r>')
|
|
68
|
+
expect(prefixed.xpath("//xmlns:item").size).to eq(1)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it "raises Moxml::XPathError for invalid expressions" do
|
|
72
|
+
expect { doc.xpath("//item[") }.to raise_error(Moxml::XPathError)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -38,21 +38,35 @@ RSpec.describe Moxml::Adapter::Ox do
|
|
|
38
38
|
|
|
39
39
|
describe "xpath support" do
|
|
40
40
|
let(:doc) do
|
|
41
|
-
described_class.parse("<root><child id='1'>text</child></root>").native
|
|
41
|
+
described_class.parse("<root><child id='1'>text</child><child id='2'>more</child></root>").native
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
it "supports basic element matching" do
|
|
45
45
|
nodes = described_class.xpath(doc, "//child")
|
|
46
|
-
expect(nodes.size).to eq(
|
|
46
|
+
expect(nodes.size).to eq(2)
|
|
47
47
|
expect(nodes.first.name).to eq("child")
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
-
it "supports attribute
|
|
51
|
-
pending("Ox does not support attribute value predicates in XPath (documented limitation)")
|
|
52
|
-
|
|
50
|
+
it "supports attribute value predicates" do
|
|
53
51
|
nodes = described_class.xpath(doc, "//child[@id='1']")
|
|
54
52
|
expect(nodes.size).to eq(1)
|
|
55
|
-
expect(nodes.first.attributes[
|
|
53
|
+
expect(nodes.first.attributes[:id]).to eq("1")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "supports logical operators" do
|
|
57
|
+
nodes = described_class.xpath(doc, "//child[@id='1' or @id='2']")
|
|
58
|
+
expect(nodes.size).to eq(2)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it "supports position predicates" do
|
|
62
|
+
nodes = described_class.xpath(doc, "//child[2]")
|
|
63
|
+
expect(nodes.size).to eq(1)
|
|
64
|
+
expect(nodes.first.attributes[:id]).to eq("2")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "supports XPath functions" do
|
|
68
|
+
count = described_class.xpath(doc, "count(//child)")
|
|
69
|
+
expect(count).to eq(2)
|
|
56
70
|
end
|
|
57
71
|
end
|
|
58
72
|
end
|
|
@@ -75,9 +75,22 @@ RSpec.describe Moxml::Adapter do
|
|
|
75
75
|
end
|
|
76
76
|
|
|
77
77
|
describe "default adapter / available list invariants" do
|
|
78
|
-
it "
|
|
78
|
+
it "PREFERRED and FALLBACK adapters are members of AVAILABLE_ADAPTERS" do
|
|
79
79
|
expect(Moxml::Adapter::AVAILABLE_ADAPTERS)
|
|
80
|
-
.to include(Moxml::Config::
|
|
80
|
+
.to include(Moxml::Config::PREFERRED_ADAPTER, Moxml::Config::FALLBACK_ADAPTER)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it "the resolved runtime default adapter is loadable" do
|
|
84
|
+
expect(Moxml::Adapter::AVAILABLE_ADAPTERS)
|
|
85
|
+
.to include(Moxml::Config.default_adapter)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it "prefers leptris only when the installed gem supports document construction" do
|
|
89
|
+
if Moxml::Config.leptris_preferred_available?
|
|
90
|
+
expect(Moxml::Config.runtime_default_adapter).to eq(:leptris)
|
|
91
|
+
else
|
|
92
|
+
expect(Moxml::Config.runtime_default_adapter).not_to eq(:leptris)
|
|
93
|
+
end
|
|
81
94
|
end
|
|
82
95
|
|
|
83
96
|
it "OPAL_DEFAULT_ADAPTER is a member of OPAL_AVAILABLE_ADAPTERS" do
|
|
@@ -125,6 +125,186 @@ RSpec.shared_examples "xml adapter" do
|
|
|
125
125
|
value = attr.respond_to?(:to_xml) ? attr&.to_xml : attr.to_s
|
|
126
126
|
expect(value).to match(/< > & ("|") ('|')/)
|
|
127
127
|
end
|
|
128
|
+
|
|
129
|
+
it "keeps attributes that share a local name across namespaces (issues #94/#95)" do
|
|
130
|
+
collision = described_class.parse(
|
|
131
|
+
'<probe xmlns:xmi="http://www.example.com/xmi" ' \
|
|
132
|
+
'xmi:type="uml:Parameter" type="EAnone_void"/>',
|
|
133
|
+
).native
|
|
134
|
+
probe = described_class.root(collision)
|
|
135
|
+
|
|
136
|
+
attrs = described_class.attributes(probe)
|
|
137
|
+
expect(attrs.size).to eq(2)
|
|
138
|
+
expect(attrs.map(&:value).sort).to eq(["EAnone_void", "uml:Parameter"])
|
|
139
|
+
|
|
140
|
+
reversed = described_class.parse(
|
|
141
|
+
'<probe type="EAnone_void" ' \
|
|
142
|
+
'xmlns:xmi="http://www.example.com/xmi" xmi:type="uml:Parameter"/>',
|
|
143
|
+
).native
|
|
144
|
+
expect(described_class.attributes(described_class.root(reversed)).size).to eq(2)
|
|
145
|
+
|
|
146
|
+
# Bare name resolves to the namespace-less attribute; the
|
|
147
|
+
# prefixed name to the namespaced one — on every adapter.
|
|
148
|
+
expect(described_class.get_attribute_value(probe, "type"))
|
|
149
|
+
.to eq("EAnone_void")
|
|
150
|
+
expect(described_class.get_attribute_value(probe, "xmi:type"))
|
|
151
|
+
.to eq("uml:Parameter")
|
|
152
|
+
expect(described_class.get_attribute_value(probe, "bogus:missing"))
|
|
153
|
+
.to be_nil
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
describe "mutation return contract" do
|
|
158
|
+
let(:doc) { described_class.parse(xml).native }
|
|
159
|
+
let(:element) { described_class.children(described_class.root(doc)).first }
|
|
160
|
+
|
|
161
|
+
it "returns the native to track from set_attribute_name" do
|
|
162
|
+
native = described_class.attributes(element).first
|
|
163
|
+
tracked = described_class.set_attribute_name(native, "renamed")
|
|
164
|
+
expect(tracked).to be_a(native.class)
|
|
165
|
+
expect(described_class.attribute_name(tracked)).to eq("renamed")
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
it "returns the native to track from set_namespace on an attribute" do
|
|
169
|
+
native = described_class.attributes(element).first
|
|
170
|
+
ns = described_class.namespace_definitions(
|
|
171
|
+
described_class.root(doc),
|
|
172
|
+
).last
|
|
173
|
+
tracked = described_class.set_namespace(native, ns)
|
|
174
|
+
expect(tracked).to be_a(native.class)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
it "returns the native to track from set_attribute_value" do
|
|
178
|
+
native = described_class.attributes(element).first
|
|
179
|
+
tracked = described_class.set_attribute_value(native, "updated")
|
|
180
|
+
expect(tracked).to be_a(native.class)
|
|
181
|
+
expect(tracked.value).to eq("updated")
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
describe "tree mutation" do
|
|
186
|
+
let(:doc) { described_class.parse(xml).native }
|
|
187
|
+
let(:root) { described_class.root(doc) }
|
|
188
|
+
|
|
189
|
+
def names_of(node)
|
|
190
|
+
described_class.children(node)
|
|
191
|
+
.map { |child| described_class.node_name(child) }
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
it "inserts previous and next siblings" do
|
|
195
|
+
target = described_class.children(root)[1]
|
|
196
|
+
described_class.add_previous_sibling(
|
|
197
|
+
target, described_class.create_element("before")
|
|
198
|
+
)
|
|
199
|
+
described_class.add_next_sibling(
|
|
200
|
+
target, described_class.create_element("after")
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
expect(names_of(root)).to eq(%w[child before child after special])
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
it "removes nodes from the tree" do
|
|
207
|
+
described_class.remove(described_class.children(root)[1])
|
|
208
|
+
expect(names_of(root)).to eq(%w[child special])
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
it "replaces a node in place" do
|
|
212
|
+
described_class.replace(
|
|
213
|
+
described_class.children(root)[1],
|
|
214
|
+
described_class.create_element("replacement"),
|
|
215
|
+
)
|
|
216
|
+
expect(names_of(root)).to eq(%w[child replacement special])
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
it "replaces all children" do
|
|
220
|
+
described_class.replace_children(
|
|
221
|
+
root, [described_class.create_element("only")]
|
|
222
|
+
)
|
|
223
|
+
expect(names_of(root)).to eq(%w[only])
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
describe "content accessors" do
|
|
228
|
+
let(:doc) { described_class.parse(xml).native }
|
|
229
|
+
let(:root) { described_class.root(doc) }
|
|
230
|
+
|
|
231
|
+
it "reads inner text from direct text children" do
|
|
232
|
+
child = described_class.children(root)[0]
|
|
233
|
+
expect(described_class.inner_text(child)).to eq("Text")
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
it "writes text content" do
|
|
237
|
+
element = described_class.create_element("scratch")
|
|
238
|
+
described_class.set_text_content(element, "written")
|
|
239
|
+
expect(described_class.inner_text(element)).to eq("written")
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
it "reads and writes comment content" do
|
|
243
|
+
comment = described_class.create_comment("note")
|
|
244
|
+
expect(described_class.comment_content(comment)).to eq("note")
|
|
245
|
+
described_class.set_comment_content(comment, "changed")
|
|
246
|
+
expect(described_class.comment_content(comment)).to eq("changed")
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
it "reads and writes CDATA content" do
|
|
250
|
+
cdata = described_class.create_cdata("<raw>")
|
|
251
|
+
expect(described_class.cdata_content(cdata)).to eq("<raw>")
|
|
252
|
+
described_class.set_cdata_content(cdata, "<new>")
|
|
253
|
+
expect(described_class.cdata_content(cdata)).to eq("<new>")
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
describe "auxiliary node creation" do
|
|
258
|
+
it "creates doctypes with external identifiers" do
|
|
259
|
+
doctype = described_class.create_doctype("cfg", "pub", "sys")
|
|
260
|
+
expect(described_class.node_type(doctype)).to eq(:doctype)
|
|
261
|
+
expect(described_class.doctype_name(doctype)).to eq("cfg")
|
|
262
|
+
expect(described_class.doctype_external_id(doctype)).to eq("pub")
|
|
263
|
+
expect(described_class.doctype_system_id(doctype)).to eq("sys")
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
it "creates declarations with version, encoding and standalone" do
|
|
267
|
+
declaration = described_class.create_declaration("1.0", "UTF-8", "yes")
|
|
268
|
+
expect(described_class.declaration_attribute(declaration, "version"))
|
|
269
|
+
.to eq("1.0")
|
|
270
|
+
expect(described_class.declaration_attribute(declaration, "standalone"))
|
|
271
|
+
.to eq("yes")
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
it "creates entity references by name" do
|
|
275
|
+
reference = described_class.create_entity_reference("copy")
|
|
276
|
+
expect(described_class.node_type(reference)).to eq(:entity_reference)
|
|
277
|
+
expect(described_class.entity_reference_name(reference)).to eq("copy")
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
it "duplicates nodes with their names" do
|
|
281
|
+
element = described_class.create_element("original")
|
|
282
|
+
duplicate = described_class.duplicate_node(element)
|
|
283
|
+
expect(described_class.node_name(duplicate)).to eq("original")
|
|
284
|
+
expect(duplicate).not_to equal(element)
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
describe "in-scope namespaces" do
|
|
289
|
+
let(:doc) { described_class.parse(xml).native }
|
|
290
|
+
let(:special) { described_class.children(described_class.root(doc)).last }
|
|
291
|
+
|
|
292
|
+
it "returns the ancestor-declared prefixes" do
|
|
293
|
+
prefixes = described_class.in_scope_namespaces(special)
|
|
294
|
+
.map { |ns| described_class.namespace_prefix(ns) }
|
|
295
|
+
# The default declaration is reported as a nil prefix everywhere
|
|
296
|
+
# except Ox, which spells it "xmlns"
|
|
297
|
+
expect(prefixes).to include("x")
|
|
298
|
+
expect(prefixes.size).to eq(2)
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
describe "line numbers" do
|
|
303
|
+
it "returns a 1-based position or nil" do
|
|
304
|
+
doc = described_class.parse(xml).native
|
|
305
|
+
line = described_class.line_number(described_class.root(doc))
|
|
306
|
+
expect(line.nil? || (line.is_a?(Integer) && line >= 1)).to be(true)
|
|
307
|
+
end
|
|
128
308
|
end
|
|
129
309
|
|
|
130
310
|
describe "namespaces" do
|