moxml 0.1.26 → 0.3.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 +6 -1
- data/lib/moxml/adapter/base.rb +53 -56
- 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 +137 -117
- data/lib/moxml/adapter/nokogiri.rb +29 -8
- data/lib/moxml/adapter/oga.rb +139 -55
- data/lib/moxml/adapter/ox.rb +93 -137
- data/lib/moxml/adapter/rexml.rb +64 -51
- 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/builder.rb +0 -18
- data/lib/moxml/config.rb +29 -5
- data/lib/moxml/context.rb +33 -0
- data/lib/moxml/document.rb +1 -1
- data/lib/moxml/element.rb +29 -12
- data/lib/moxml/entity/reference.rb +28 -0
- data/lib/moxml/entity/restorer.rb +95 -0
- data/lib/moxml/entity.rb +126 -0
- data/lib/moxml/entity_registry.rb +1 -1
- data/lib/moxml/error.rb +0 -42
- data/lib/moxml/node.rb +135 -10
- data/lib/moxml/node_set.rb +1 -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 +143 -8
- data/lib/moxml/xpath/parser.rb +22 -15
- data/lib/moxml.rb +3 -3
- 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 +18 -1
- data/spec/integration/sax_parity_spec.rb +58 -0
- data/spec/integration/shared_examples/edge_cases.rb +0 -10
- data/spec/integration/shared_examples/high_level/{document_builder_behavior.rb → parse_behavior.rb} +1 -2
- 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 +15 -8
- data/spec/moxml/adapter/leptris_spec.rb +75 -0
- data/spec/moxml/adapter/libxml_internals_spec.rb +1 -1
- 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/config_spec.rb +0 -1
- data/spec/moxml/context_spec.rb +22 -0
- data/spec/moxml/doctype_spec.rb +1 -1
- data/spec/moxml/entity_spec.rb +76 -0
- data/spec/moxml/error_spec.rb +0 -50
- 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 +112 -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 +23 -6
- data/lib/moxml/adapter/libxml/entity_restorer.rb +0 -94
- data/lib/moxml/document_builder.rb +0 -167
- data/spec/moxml/document_builder_spec.rb +0 -30
|
@@ -96,23 +96,30 @@ 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
|
|
116
108
|
end
|
|
109
|
+
|
|
110
|
+
describe "duplication" do
|
|
111
|
+
it "returns an independent deep copy" do
|
|
112
|
+
doc = context.parse("<root><a id='1'>text</a></root>")
|
|
113
|
+
original = doc.at_xpath("//a")
|
|
114
|
+
copy = original.dup
|
|
115
|
+
|
|
116
|
+
expect(copy).not_to equal(original)
|
|
117
|
+
expect(copy.name).to eq("a")
|
|
118
|
+
expect(copy["id"]).to eq("1")
|
|
119
|
+
expect(copy.text).to eq("text")
|
|
120
|
+
copy["id"] = "changed"
|
|
121
|
+
expect(original["id"]).to eq("1")
|
|
122
|
+
end
|
|
123
|
+
end
|
|
117
124
|
end
|
|
118
125
|
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
|
|
@@ -154,7 +154,7 @@ RSpec.describe Moxml::Adapter::Libxml do
|
|
|
154
154
|
end
|
|
155
155
|
end
|
|
156
156
|
|
|
157
|
-
describe Moxml::
|
|
157
|
+
describe Moxml::Entity::Restorer do
|
|
158
158
|
let(:context) { Moxml.new(:libxml) }
|
|
159
159
|
|
|
160
160
|
it "restores entities through its public entry point" do
|
|
@@ -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
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe Moxml::AttributeResolver do
|
|
6
|
+
let(:context) { Moxml.new }
|
|
7
|
+
let(:doc) do
|
|
8
|
+
context.parse(<<~XML)
|
|
9
|
+
<root xmlns:p="http://x.org" xmlns:q="http://x.org">
|
|
10
|
+
<e q:type="namespaced" type="bare"/>
|
|
11
|
+
</root>
|
|
12
|
+
XML
|
|
13
|
+
end
|
|
14
|
+
let(:element) { doc.root.children.find(&:element?) }
|
|
15
|
+
|
|
16
|
+
describe ".resolve" do
|
|
17
|
+
it "matches bare names against no-namespace attributes only" do
|
|
18
|
+
expect(described_class.resolve(element, "type")&.value).to eq("bare")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it "matches prefixed names by expanded name" do
|
|
22
|
+
expect(described_class.resolve(element, "p:type")&.value)
|
|
23
|
+
.to eq("namespaced")
|
|
24
|
+
expect(described_class.resolve(element, "q:type")&.value)
|
|
25
|
+
.to eq("namespaced")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "returns nil for undeclared prefixes and declarations" do
|
|
29
|
+
expect(described_class.resolve(element, "z:type")).to be_nil
|
|
30
|
+
expect(described_class.resolve(element, "xmlns:q")).to be_nil
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
describe ".assign" do
|
|
35
|
+
it "delegates xmlns declarations to the adapter" do
|
|
36
|
+
described_class.assign(element, "xmlns:r", "http://r.org")
|
|
37
|
+
expect(doc.to_xml).to include('xmlns:r="http://r.org"')
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it "replaces the matching attribute in place" do
|
|
41
|
+
described_class.assign(element, "p:type", "rewritten")
|
|
42
|
+
expect(element["q:type"]).to eq("rewritten")
|
|
43
|
+
expect(element.attributes.size).to eq(2)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "stores undeclared prefixes raw until they come into scope" do
|
|
47
|
+
described_class.assign(element, "z:type", "value")
|
|
48
|
+
expect(element["z:type"]).to be_nil
|
|
49
|
+
|
|
50
|
+
element.add_namespace("z", "http://z.org")
|
|
51
|
+
expect(element["z:type"]).to eq("value")
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
describe ".remove" do
|
|
56
|
+
it "removes by expanded name and returns the attribute" do
|
|
57
|
+
removed = described_class.remove(element, "p:type")
|
|
58
|
+
expect(removed.value).to eq("namespaced")
|
|
59
|
+
expect(element["q:type"]).to be_nil
|
|
60
|
+
expect(element["type"]).to eq("bare")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it "returns nil when nothing matches" do
|
|
64
|
+
expect(described_class.remove(element, "z:type")).to be_nil
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
describe ".attribute_test?" do
|
|
69
|
+
let(:attributes) { element.attributes }
|
|
70
|
+
|
|
71
|
+
it "applies Namespaces 1.0 §5.2 to bare names" do
|
|
72
|
+
bare = attributes.find { |a| a.value == "bare" }
|
|
73
|
+
namespaced = attributes.find { |a| a.value == "namespaced" }
|
|
74
|
+
expect(described_class.attribute_test?(element, bare, nil, "type"))
|
|
75
|
+
.to be(true)
|
|
76
|
+
expect(described_class.attribute_test?(element, namespaced, nil, "type"))
|
|
77
|
+
.to be(false)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it "matches prefixed tests by URI and accepts any local for nil" do
|
|
81
|
+
namespaced = attributes.find { |a| a.value == "namespaced" }
|
|
82
|
+
expect(described_class.attribute_test?(element, namespaced, "p", "type"))
|
|
83
|
+
.to be(true)
|
|
84
|
+
expect(described_class.attribute_test?(element, namespaced, "p", nil))
|
|
85
|
+
.to be(true)
|
|
86
|
+
expect(described_class.attribute_test?(element, namespaced, "z", "type"))
|
|
87
|
+
.to be(false)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it "treats :any as any namespace including none" do
|
|
91
|
+
bare = attributes.find { |a| a.value == "bare" }
|
|
92
|
+
expect(described_class.attribute_test?(element, bare, :any, "type"))
|
|
93
|
+
.to be(true)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
describe ".attribute_test_uri?" do
|
|
98
|
+
it "matches the compile-time URI regardless of document prefixes" do
|
|
99
|
+
namespaced = element.attributes.find { |a| a.value == "namespaced" }
|
|
100
|
+
expect(described_class.attribute_test_uri?(
|
|
101
|
+
element, namespaced, "http://x.org", "type"
|
|
102
|
+
)).to be(true)
|
|
103
|
+
expect(described_class.attribute_test_uri?(
|
|
104
|
+
element, namespaced, "http://other.org", "type"
|
|
105
|
+
)).to be(false)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
data/spec/moxml/config_spec.rb
CHANGED
|
@@ -11,7 +11,6 @@ RSpec.describe Moxml::Config do
|
|
|
11
11
|
expect(config.default_encoding).to eq("UTF-8")
|
|
12
12
|
expect(config.default_indent).to eq(2)
|
|
13
13
|
expect(config.default_line_ending).to eq("\n")
|
|
14
|
-
expect(config.entity_encoding).to eq(:basic)
|
|
15
14
|
end
|
|
16
15
|
|
|
17
16
|
it "sets default entity_load_mode to :required" do
|
data/spec/moxml/context_spec.rb
CHANGED
|
@@ -39,4 +39,26 @@ RSpec.describe Moxml::Context do
|
|
|
39
39
|
expect(doc.root.name).to eq("root")
|
|
40
40
|
end
|
|
41
41
|
end
|
|
42
|
+
|
|
43
|
+
describe "wrapper identity map" do
|
|
44
|
+
it "hands back the same wrapper for the same native" do
|
|
45
|
+
doc = context.parse("<root><item id='1'/></root>")
|
|
46
|
+
first = doc.xpath("//item").first
|
|
47
|
+
second = doc.xpath("//item").first
|
|
48
|
+
expect(first).to equal(second)
|
|
49
|
+
|
|
50
|
+
again = doc.root.children.last
|
|
51
|
+
expect(again).to equal(first)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it "re-keys when refresh_native! swaps the native" do
|
|
55
|
+
doc = context.create_document
|
|
56
|
+
element = doc.create_element("x")
|
|
57
|
+
doc.add_child(element)
|
|
58
|
+
|
|
59
|
+
refreshed = doc.root
|
|
60
|
+
expect(refreshed.name).to eq("x")
|
|
61
|
+
expect(context.wrapper_for(refreshed.native)).to equal(refreshed)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
42
64
|
end
|
data/spec/moxml/doctype_spec.rb
CHANGED
|
@@ -48,7 +48,7 @@ RSpec.describe Moxml::Doctype do
|
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
describe "parsing" do
|
|
51
|
-
%i[nokogiri oga rexml ox].each do |adapter_name|
|
|
51
|
+
%i[nokogiri oga rexml ox libxml].each do |adapter_name|
|
|
52
52
|
context "with #{adapter_name} adapter" do
|
|
53
53
|
let(:ctx) { Moxml.new(adapter_name) }
|
|
54
54
|
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe Moxml::Entity do
|
|
6
|
+
describe ".preprocess_entities" do
|
|
7
|
+
it "marks non-standard entities and leaves the five standard ones" do
|
|
8
|
+
marked = described_class.preprocess_entities("a © b & c <")
|
|
9
|
+
expect(marked).to include("#{described_class::MARKER}copy;")
|
|
10
|
+
expect(marked).to include("&")
|
|
11
|
+
expect(marked).to include("<")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it "returns the input untouched when it contains no ampersand" do
|
|
15
|
+
input = "plain text"
|
|
16
|
+
expect(described_class.preprocess_entities(input)).to equal(input)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "always returns UTF-8" do
|
|
20
|
+
binary = "caf\xC3\xA9 ©".b
|
|
21
|
+
result = described_class.preprocess_entities(binary)
|
|
22
|
+
expect(result.encoding).to eq(Encoding::UTF_8)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "returns an empty string for nil" do
|
|
26
|
+
expect(described_class.preprocess_entities(nil)).to eq("")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
describe ".decode_entities" do
|
|
31
|
+
it "decodes numeric and standard named references in one pass" do
|
|
32
|
+
expect(described_class.decode_entities("AB&<"))
|
|
33
|
+
.to eq("AB&<")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it "preserves invalid numeric codepoints verbatim" do
|
|
37
|
+
expect(described_class.decode_entities("��")).to eq("��")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it "skips strings without ampersands" do
|
|
41
|
+
input = "plain"
|
|
42
|
+
expect(described_class.decode_entities(input)).to equal(input)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
describe ".restore_entities" do
|
|
47
|
+
it "maps markers back to named references" do
|
|
48
|
+
restored = described_class.restore_entities(
|
|
49
|
+
"a#{described_class::MARKER}copy;b",
|
|
50
|
+
)
|
|
51
|
+
expect(restored).to eq("a©b")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it "restores markers a native serializer rendered as character references" do
|
|
55
|
+
expect(described_class.restore_entities("acopy;b"))
|
|
56
|
+
.to eq("a©b")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it "returns marker-free strings without a regex pass" do
|
|
60
|
+
input = "no markers here"
|
|
61
|
+
expect(described_class.restore_entities(input)).to equal(input)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
describe Moxml::Entity::Reference do
|
|
66
|
+
it "round-trips as a serialized entity reference" do
|
|
67
|
+
expect(described_class.new("copy").to_xml).to eq("©")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it "compares by name" do
|
|
71
|
+
reference = described_class.new("copy")
|
|
72
|
+
expect(reference).to eq(described_class.new("copy"))
|
|
73
|
+
expect(reference).not_to eq(described_class.new("nbsp"))
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
data/spec/moxml/error_spec.rb
CHANGED
|
@@ -150,31 +150,6 @@ RSpec.describe "Moxml errors" do
|
|
|
150
150
|
end
|
|
151
151
|
end
|
|
152
152
|
|
|
153
|
-
describe Moxml::SerializationError do
|
|
154
|
-
it "includes node, adapter, and format information" do
|
|
155
|
-
error = described_class.new(
|
|
156
|
-
"Failed to serialize",
|
|
157
|
-
adapter: "LibXML",
|
|
158
|
-
format: "xml",
|
|
159
|
-
)
|
|
160
|
-
expect(error.adapter).to eq("LibXML")
|
|
161
|
-
expect(error.format).to eq("xml")
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
it "provides helpful error message" do
|
|
165
|
-
error = described_class.new(
|
|
166
|
-
"Failed to serialize",
|
|
167
|
-
adapter: "Ox",
|
|
168
|
-
format: "xml",
|
|
169
|
-
)
|
|
170
|
-
message = error.to_s
|
|
171
|
-
expect(message).to include("Failed to serialize")
|
|
172
|
-
expect(message).to include("Adapter: Ox")
|
|
173
|
-
expect(message).to include("Format: xml")
|
|
174
|
-
expect(message).to include("Hint:")
|
|
175
|
-
end
|
|
176
|
-
end
|
|
177
|
-
|
|
178
153
|
describe Moxml::DocumentStructureError do
|
|
179
154
|
it "includes attempted operation and state" do
|
|
180
155
|
error = described_class.new(
|
|
@@ -200,31 +175,6 @@ RSpec.describe "Moxml errors" do
|
|
|
200
175
|
end
|
|
201
176
|
end
|
|
202
177
|
|
|
203
|
-
describe Moxml::AttributeError do
|
|
204
|
-
it "includes attribute name, element, and value" do
|
|
205
|
-
error = described_class.new(
|
|
206
|
-
"Invalid attribute",
|
|
207
|
-
name: "id",
|
|
208
|
-
value: 123,
|
|
209
|
-
)
|
|
210
|
-
expect(error.attribute_name).to eq("id")
|
|
211
|
-
expect(error.value).to eq(123)
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
it "provides helpful error message" do
|
|
215
|
-
error = described_class.new(
|
|
216
|
-
"Invalid attribute name",
|
|
217
|
-
name: "123invalid",
|
|
218
|
-
value: "test",
|
|
219
|
-
)
|
|
220
|
-
message = error.to_s
|
|
221
|
-
expect(message).to include("Invalid attribute name")
|
|
222
|
-
expect(message).to include("Attribute: 123invalid")
|
|
223
|
-
expect(message).to include("Value: \"test\"")
|
|
224
|
-
expect(message).to include("Hint:")
|
|
225
|
-
end
|
|
226
|
-
end
|
|
227
|
-
|
|
228
178
|
describe Moxml::NotImplementedError do
|
|
229
179
|
it "includes feature and adapter information" do
|
|
230
180
|
error = described_class.new(
|