moxml 0.5.149 → 0.5.150
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/dependent-repos.json +7 -2
- data/.github/workflows/dependent-tests.yml +4 -1
- data/.github/workflows/performance-release.yml +79 -0
- data/.github/workflows/performance.yml +59 -0
- data/Rakefile +23 -0
- data/benchmark/README.md +106 -0
- data/benchmark/scoreboard.json +115 -0
- data/benchmark/scoreboard.rb +556 -0
- data/bin/generate-model-coverage +21 -0
- data/docs/_pages/adapter-compatibility.adoc +3 -3
- data/docs/_pages/divergences.adoc +32 -41
- data/docs/_pages/ecosystem.adoc +62 -0
- data/docs/_pages/headed-ox-limitations.adoc +1 -1
- data/docs/_pages/headed-ox.adoc +2 -2
- data/docs/_pages/index.adoc +4 -0
- data/docs/_pages/model-coverage.adoc +57 -0
- data/docs/_pages/performance.adoc +29 -0
- data/docs/_pages/why-moxml.adoc +87 -0
- data/docs/_references/processing-model.adoc +42 -3
- data/lib/moxml/adapter/base.rb +73 -0
- data/lib/moxml/adapter/leptris.rb +183 -4
- data/lib/moxml/adapter/libxml.rb +34 -2
- data/lib/moxml/adapter/nokogiri.rb +10 -0
- data/lib/moxml/adapter/oga.rb +39 -7
- data/lib/moxml/adapter/ox.rb +8 -0
- data/lib/moxml/adapter/rexml.rb +17 -2
- data/lib/moxml/c14n/data_model.rb +8 -0
- data/lib/moxml/c14n/processor.rb +17 -8
- data/lib/moxml/context.rb +28 -0
- data/lib/moxml/document.rb +17 -3
- data/lib/moxml/node.rb +56 -5
- data/lib/moxml/node_set.rb +31 -1
- data/lib/moxml/relaxng.rb +62 -0
- data/lib/moxml/sax/block_handler.rb +7 -0
- data/lib/moxml/sax/handler.rb +20 -0
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xpath.rb +28 -0
- data/lib/moxml.rb +1 -0
- data/spec/consistency/adapter_parity_spec.rb +22 -0
- data/spec/consistency/citation_hygiene_spec.rb +30 -0
- data/spec/consistency/model_coverage_audit_spec.rb +61 -86
- data/spec/consistency/published_docs_spec.rb +49 -0
- data/spec/consistency/xpath_idioms_spec.rb +23 -5
- data/spec/integration/shared_examples/edge_cases.rb +0 -13
- data/spec/moxml/adapter/leptris_spec.rb +23 -3
- data/spec/moxml/adapter/shared_examples/adapter_contract.rb +0 -8
- data/spec/moxml/attribute_resolver_spec.rb +33 -0
- data/spec/moxml/c14n/adapter_delegation_spec.rb +33 -6
- data/spec/moxml/c14n/character_encoding_spec.rb +7 -13
- data/spec/moxml/c14n/comments_pis_spec.rb +35 -8
- data/spec/moxml/c14n/subset_spec.rb +2 -12
- data/spec/moxml/c14n/w3c_examples_spec.rb +2 -5
- data/spec/moxml/element_indent_spec.rb +34 -0
- data/spec/moxml/node_spec.rb +0 -3
- data/spec/moxml/relax_ng_spec.rb +67 -0
- data/spec/moxml/sax_spec.rb +45 -74
- data/spec/moxml/xpath_capabilities_spec.rb +11 -38
- data/spec/support/input_taxonomy.rb +7 -0
- data/spec/support/model_coverage.rb +159 -0
- metadata +17 -2
|
@@ -42,11 +42,6 @@ RSpec.describe "XPath idioms across adapters" do
|
|
|
42
42
|
Gem::Version.new(Leptris::VERSION) < Gem::Version.new("1.9.359.0")
|
|
43
43
|
skip "binding #{Leptris::VERSION} predates the xmlns: fix (1.9.359)"
|
|
44
44
|
end
|
|
45
|
-
if adapter_name == :oga
|
|
46
|
-
# oga's own Ruby XPath engine has no xmlns: default-ns
|
|
47
|
-
# support (third-party; explicit bindings work).
|
|
48
|
-
skip "D-01: oga's XPath engine does not implement the xmlns: idiom"
|
|
49
|
-
end
|
|
50
45
|
end
|
|
51
46
|
|
|
52
47
|
it "matches default-namespaced elements via the xmlns: prefix" do
|
|
@@ -62,6 +57,14 @@ RSpec.describe "XPath idioms across adapters" do
|
|
|
62
57
|
expect(targets).to be_empty
|
|
63
58
|
end
|
|
64
59
|
|
|
60
|
+
it "addresses the no-namespace default with an empty xmlns binding" do
|
|
61
|
+
doc = ctx.parse(
|
|
62
|
+
'<root xmlns="urn:a"><child xmlns="urn:b"><leaf xmlns=""/></child></root>',
|
|
63
|
+
)
|
|
64
|
+
leaf = doc.at_xpath("//xmlns:leaf", "xmlns" => "")
|
|
65
|
+
expect(leaf&.name).to eq("leaf")
|
|
66
|
+
end
|
|
67
|
+
|
|
65
68
|
it "resolves attribute-terminal steps in every path position" do
|
|
66
69
|
doc = ctx.parse(plain_xml)
|
|
67
70
|
expect(doc.xpath("//a/@id").first&.value).to eq("x")
|
|
@@ -118,6 +121,21 @@ RSpec.describe "XPath idioms across adapters" do
|
|
|
118
121
|
expect(doc.xpath("boolean(//b)")).to be(true)
|
|
119
122
|
expect(doc.at_xpath("count(//a)")).to eq(1.0)
|
|
120
123
|
end
|
|
124
|
+
|
|
125
|
+
# The live-tree contract: a sibling inserted after an XPath
|
|
126
|
+
# evaluation must be visible to at_xpath and xpath. leptris's
|
|
127
|
+
# node-set lane caches a document index that sibling insertions
|
|
128
|
+
# do not rebuild (engine bug, tracked upstream); the adapter
|
|
129
|
+
# routes such documents to the reference engine until a child
|
|
130
|
+
# insertion rebuilds the index.
|
|
131
|
+
it "answers at_xpath and xpath from the live tree after a sibling insertion" do
|
|
132
|
+
doc = ctx.parse("<r><a><b/></a></r>")
|
|
133
|
+
doc.xpath("//a")
|
|
134
|
+
doc.at("//a").next = "<c/>"
|
|
135
|
+
expect(doc.at("//c")&.name).to eq("c")
|
|
136
|
+
expect(doc.xpath("//c").map(&:name)).to eq(["c"])
|
|
137
|
+
expect(doc.at_xpath("count(//c)")).to eq(1.0)
|
|
138
|
+
end
|
|
121
139
|
end
|
|
122
140
|
end
|
|
123
141
|
end
|
|
@@ -80,15 +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 == :headed_ox
|
|
84
|
-
skip "HeadedOx limitation: Namespace methods not implemented in adapter. Requires Ox namespace API enhancement. See docs/_pages/headed-ox-limitations.adoc"
|
|
85
|
-
end
|
|
86
|
-
if context.config.adapter_name == :libxml
|
|
87
|
-
skip "LibXML cannot query empty default namespace with XPath (documented limitation)"
|
|
88
|
-
end
|
|
89
|
-
if context.config.adapter_name == :nokogiri
|
|
90
|
-
skip "Nokogiri XPath does not support querying empty namespace with xmlns prefix mapping"
|
|
91
|
-
end
|
|
92
83
|
xml = <<~XML
|
|
93
84
|
<root xmlns="http://default1.org">
|
|
94
85
|
<child xmlns="http://default2.org">
|
|
@@ -103,10 +94,6 @@ RSpec.shared_examples "Moxml Edge Cases" do
|
|
|
103
94
|
end
|
|
104
95
|
|
|
105
96
|
it "handles recursive namespace definitions" do
|
|
106
|
-
if context.config.adapter_name == :headed_ox
|
|
107
|
-
skip "HeadedOx limitation: Namespace methods not implemented in adapter. Requires Ox namespace API enhancement. See docs/_pages/headed-ox-limitations.adoc"
|
|
108
|
-
end
|
|
109
|
-
|
|
110
97
|
xml = <<~XML
|
|
111
98
|
<root xmlns:a="http://a.org">
|
|
112
99
|
<a:child xmlns:a="http://b.org">
|
|
@@ -184,12 +184,14 @@ RSpec.describe Moxml::Adapter::Leptris do
|
|
|
184
184
|
end
|
|
185
185
|
end
|
|
186
186
|
|
|
187
|
-
it "keeps comments in
|
|
187
|
+
it "keeps comments in indented serialization with tab units (leptris-ruby#115)" do
|
|
188
188
|
skip "requires the element unit fix (leptris 1.9.50+)" unless described_class::ELEMENT_UNIT_COMMENTS
|
|
189
189
|
|
|
190
|
+
# Element-level indentation is loud on this engine (D-09); the
|
|
191
|
+
# document lane exercises the same generator's unit handling.
|
|
190
192
|
doc = ctx.parse(%(<r><a/><!-- c --></r>))
|
|
191
|
-
expect(doc.
|
|
192
|
-
.to eq("<r>\n\t<a></a>\n\t<!-- c -->\n</r
|
|
193
|
+
expect(doc.to_xml(indent: 1, indent_text: "\t"))
|
|
194
|
+
.to eq("<r>\n\t<a></a>\n\t<!-- c -->\n</r>\n")
|
|
193
195
|
end
|
|
194
196
|
|
|
195
197
|
it "round-trips document-level PI mutations through serialization" do
|
|
@@ -846,4 +848,22 @@ RSpec.describe Moxml::Adapter::Leptris do
|
|
|
846
848
|
expect(first.next_sibling.parent).to eq(doc.root)
|
|
847
849
|
end
|
|
848
850
|
end
|
|
851
|
+
|
|
852
|
+
describe "stale-index guard retirement (leptris-ruby#519)" do
|
|
853
|
+
let(:ctx) { Moxml.new(:leptris) }
|
|
854
|
+
|
|
855
|
+
it "auto-retires the guard when the engine invalidates its index" do
|
|
856
|
+
skip "requires an engine whose node-set index survives sibling insertion (leptris-ruby#519)" unless described_class.native_xpath_index_fixed?
|
|
857
|
+
|
|
858
|
+
doc = ctx.parse("<r><a/><b/></r>")
|
|
859
|
+
root = doc.root
|
|
860
|
+
_ = root.xpath("child::*") # populate the engine node-set index
|
|
861
|
+
b = root.children.to_a[1]
|
|
862
|
+
b.add_previous_sibling(doc.create_element("c"))
|
|
863
|
+
|
|
864
|
+
native = described_class.native_xpath(root.native, "child::*", nil)
|
|
865
|
+
expect(native).not_to be_nil
|
|
866
|
+
expect(native.to_a.map { |n| n.name.to_s }).to eq(%w[a c b])
|
|
867
|
+
end
|
|
868
|
+
end
|
|
849
869
|
end
|
|
@@ -457,10 +457,6 @@ RSpec.shared_examples "xml adapter" do
|
|
|
457
457
|
end
|
|
458
458
|
|
|
459
459
|
it "preserves and correctly handles multiple namespaces" do
|
|
460
|
-
if described_class.name.include?("Ox")
|
|
461
|
-
skip("Ox does not support namespace-aware XPath queries (documented limitation)")
|
|
462
|
-
end
|
|
463
|
-
|
|
464
460
|
# Parse original XML
|
|
465
461
|
doc = described_class.parse(xml).native
|
|
466
462
|
|
|
@@ -513,10 +509,6 @@ RSpec.shared_examples "xml adapter" do
|
|
|
513
509
|
end
|
|
514
510
|
|
|
515
511
|
it "preserves and correctly handles multiple namespaces" do
|
|
516
|
-
if described_class.name.include?("Ox")
|
|
517
|
-
skip("Ox does not support namespace-aware XPath queries (documented limitation)")
|
|
518
|
-
end
|
|
519
|
-
|
|
520
512
|
# Parse original XML
|
|
521
513
|
doc = described_class.parse(xml).native
|
|
522
514
|
|
|
@@ -31,6 +31,39 @@ RSpec.describe Moxml::AttributeResolver do
|
|
|
31
31
|
expect(described_class.resolve(element, "z:type")).to be_nil
|
|
32
32
|
expect(described_class.resolve(element, "xmlns:q")).to be_nil
|
|
33
33
|
end
|
|
34
|
+
|
|
35
|
+
# Namespaces 1.0 §5.2: the default namespace never applies to
|
|
36
|
+
# attributes. Engines that resolve unprefixed attributes against
|
|
37
|
+
# the default namespace must answer nil for it, or bare-name reads
|
|
38
|
+
# miss on every element under a default namespace.
|
|
39
|
+
it "never treats bare attributes as default-namespace members" do
|
|
40
|
+
Moxml::Adapter::AVAILABLE_ADAPTERS.each do |adapter_name|
|
|
41
|
+
d = Moxml.new(adapter_name).parse(%(<r xmlns="urn:a"><e id="1"/></r>))
|
|
42
|
+
e = d.root.children.find(&:element?)
|
|
43
|
+
expect(e["id"]).to eq("1"), "#{adapter_name}: bare read"
|
|
44
|
+
expect(e.attribute("id")&.value).to eq("1"),
|
|
45
|
+
"#{adapter_name}: resolver"
|
|
46
|
+
expect(e.attribute("id")&.namespace).to be_nil,
|
|
47
|
+
"#{adapter_name}: attribute namespace"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it "keeps bare reads on programmatically built default-namespaced trees" do
|
|
52
|
+
Moxml::Adapter::AVAILABLE_ADAPTERS.each do |adapter_name|
|
|
53
|
+
ctx = Moxml.new(adapter_name)
|
|
54
|
+
d = ctx.create_document
|
|
55
|
+
root = d.create_element("root")
|
|
56
|
+
root.add_namespace(nil, "urn:a")
|
|
57
|
+
d.add_child(root)
|
|
58
|
+
e = d.create_element("item")
|
|
59
|
+
e["id"] = "123"
|
|
60
|
+
root.add_child(e)
|
|
61
|
+
|
|
62
|
+
expect(e["id"]).to eq("123"), "#{adapter_name}: bare read"
|
|
63
|
+
expect(d.xpath('//xmlns:item[@id="123"]').to_a.size).to eq(1),
|
|
64
|
+
"#{adapter_name}: xmlns predicate"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
34
67
|
end
|
|
35
68
|
|
|
36
69
|
describe ".assign" do
|
|
@@ -59,20 +59,47 @@ RSpec.describe "Moxml::C14n cross-adapter canonicalization" do
|
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
describe "document-level nodes keep document order" do
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
let(:
|
|
62
|
+
# ox/headed_ox drop PI content at parse (D-05), so content-bearing
|
|
63
|
+
# document-level PIs are pinned on the remaining adapters; the
|
|
64
|
+
# separator/newline rule itself is pinned on every adapter below
|
|
65
|
+
# (comments and empty-data PIs are common across all of them).
|
|
66
|
+
let(:content_adapters) do
|
|
67
|
+
Moxml::Adapter::AVAILABLE_ADAPTERS - %i[ox headed_ox]
|
|
68
|
+
end
|
|
67
69
|
|
|
68
70
|
it "canonicalizes a leading PI before the root element" do
|
|
69
|
-
|
|
71
|
+
content_adapters.each do |adapter_name|
|
|
70
72
|
doc = Moxml.new(adapter_name)
|
|
71
73
|
.parse(%(<?xml version="1.0"?><?render mode="x"?><doc><e/></doc>))
|
|
72
74
|
expect(Moxml::C14n.canonicalize(doc))
|
|
73
75
|
.to eq(%(<?render mode="x"?>\n<doc><e></e></doc>))
|
|
74
76
|
end
|
|
75
77
|
end
|
|
78
|
+
|
|
79
|
+
it "uses exactly one newline between document-level nodes" do
|
|
80
|
+
Moxml::Adapter::AVAILABLE_ADAPTERS.each do |adapter_name|
|
|
81
|
+
doc = Moxml.new(adapter_name)
|
|
82
|
+
.parse(%(<?pi?><?pi2?><!--a--><doc/><!--b--><?pi3?>))
|
|
83
|
+
expect(Moxml::C14n.canonicalize(doc, with_comments: true))
|
|
84
|
+
.to eq(%(<?pi?>\n<?pi2?>\n<!--a-->\n<doc></doc>\n<!--b-->\n<?pi3?>))
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it "never emits the XML declaration" do
|
|
89
|
+
Moxml::Adapter::AVAILABLE_ADAPTERS.each do |adapter_name|
|
|
90
|
+
doc = Moxml.new(adapter_name)
|
|
91
|
+
.parse(%(<?xml version="1.0" encoding="UTF-8"?><doc/>))
|
|
92
|
+
expect(Moxml::C14n.canonicalize(doc)).to eq("<doc></doc>")
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it "collapses PI separator whitespace to one space" do
|
|
97
|
+
Moxml::Adapter::AVAILABLE_ADAPTERS.each do |adapter_name|
|
|
98
|
+
doc = Moxml.new(adapter_name).parse(%(<?pi a b?><doc/>))
|
|
99
|
+
expect(Moxml::C14n.canonicalize(doc))
|
|
100
|
+
.to eq(%(<?pi a b?>\n<doc></doc>))
|
|
101
|
+
end
|
|
102
|
+
end
|
|
76
103
|
end
|
|
77
104
|
|
|
78
105
|
describe "native delegation" do
|
|
@@ -12,25 +12,19 @@ LATIN1_XML = (+"<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?><r>caf\xE9</r>").
|
|
|
12
12
|
BOM_XML = (+"\xEF\xBB\xBF<r/>").force_encoding("UTF-8")
|
|
13
13
|
NFC_XML = (+"<r>caf\xC3\xA9</r>").force_encoding("UTF-8")
|
|
14
14
|
|
|
15
|
-
# oga's parse layer leaks the declaration as a PI node and the BOM as
|
|
16
|
-
# text (pre-existing, documented in the W3C examples spec).
|
|
17
|
-
OGA_LEAK = %i[oga].freeze
|
|
18
|
-
|
|
19
15
|
RSpec.describe "Moxml character encoding at the parse boundary" do
|
|
20
16
|
Moxml::Adapter::AVAILABLE_ADAPTERS.each do |adapter_name|
|
|
21
17
|
context "with the #{adapter_name} adapter" do
|
|
22
18
|
let(:ctx) { Moxml.new(adapter_name) }
|
|
23
19
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
end
|
|
20
|
+
it "transcodes declared Latin-1 to UTF-8 bytes" do
|
|
21
|
+
doc = ctx.parse(LATIN1_XML.dup)
|
|
22
|
+
expect(Moxml::C14n.canonicalize(doc).bytes).to eq(CAF_E_UTF8)
|
|
23
|
+
end
|
|
29
24
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
end
|
|
25
|
+
it "strips a leading UTF-8 BOM" do
|
|
26
|
+
doc = ctx.parse(BOM_XML.dup)
|
|
27
|
+
expect(Moxml::C14n.canonicalize(doc)).to eq("<r></r>")
|
|
34
28
|
end
|
|
35
29
|
|
|
36
30
|
it "passes NFC content through without normalization" do
|
|
@@ -48,18 +48,45 @@ RSpec.describe "C14N comments and PIs" do
|
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
describe "PIs at document level" do
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
it "renders PIs outside the document element in document order" do
|
|
52
|
+
doc = ctx.parse(%(<?pi before?><root><?pi inner?></root><?pi after?>))
|
|
53
|
+
expect(Moxml::C14n.canonicalize(doc))
|
|
54
|
+
.to eq(%(<?pi before?>\n<root><?pi inner?></root>\n<?pi after?>))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it "uses exactly one newline between document-level nodes" do
|
|
58
|
+
doc = ctx.parse(%(<?pi a?><?pi b?><root/><?pi c?>))
|
|
59
|
+
expect(Moxml::C14n.canonicalize(doc))
|
|
60
|
+
.to eq(%(<?pi a?>\n<?pi b?>\n<root></root>\n<?pi c?>))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it "ignores whitespace between document-level nodes" do
|
|
64
|
+
# rexml retains the text node (D-15); the canonical form never
|
|
65
|
+
# includes whitespace outside the document element.
|
|
66
|
+
Moxml::Adapter::AVAILABLE_ADAPTERS.each do |adapter_name|
|
|
67
|
+
doc = Moxml.new(adapter_name).parse(%(<?pi?>\n<root/>\n<!--c-->))
|
|
68
|
+
expect(Moxml::C14n.canonicalize(doc, with_comments: true))
|
|
69
|
+
.to eq(%(<?pi?>\n<root></root>\n<!--c-->)), adapter_name.to_s
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it "omits the XML declaration from the canonical form" do
|
|
74
|
+
doc = ctx.parse(%(<?xml version="1.0" encoding="UTF-8"?><root/>))
|
|
75
|
+
expect(Moxml::C14n.canonicalize(doc)).to eq("<root></root>")
|
|
57
76
|
end
|
|
58
77
|
end
|
|
59
78
|
|
|
60
79
|
describe "comments at document level" do
|
|
61
|
-
it "renders document-level comments when with_comments: true
|
|
62
|
-
|
|
80
|
+
it "renders document-level comments when with_comments: true" do
|
|
81
|
+
doc = ctx.parse(%(<!--lead--><root><!--inner--></root><!--trail-->))
|
|
82
|
+
expect(Moxml::C14n.canonicalize(doc, with_comments: true))
|
|
83
|
+
.to eq(%(<!--lead-->\n<root><!--inner--></root>\n<!--trail-->))
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it "keeps exactly one newline between a document-level PI and comment" do
|
|
87
|
+
doc = ctx.parse(%(<?pi x?><!--lead--><root/>))
|
|
88
|
+
expect(Moxml::C14n.canonicalize(doc, with_comments: true))
|
|
89
|
+
.to eq(%(<?pi x?>\n<!--lead-->\n<root></root>))
|
|
63
90
|
end
|
|
64
91
|
end
|
|
65
92
|
end
|
|
@@ -43,9 +43,6 @@ COMMENTS_EXCLUDED_CASE = [
|
|
|
43
43
|
].freeze
|
|
44
44
|
|
|
45
45
|
COMMENTS_PADDING_LOSS = %i[ox headed_ox].freeze
|
|
46
|
-
# rexml's adapter xpath does not return comment()/text() node results
|
|
47
|
-
# (pre-existing capability gap, same family for both axes).
|
|
48
|
-
COMMENT_SELECTION_GAP = %i[rexml].freeze
|
|
49
46
|
|
|
50
47
|
RSpec.describe "Moxml::C14n subset canonicalization" do
|
|
51
48
|
# Node-set semantics follow the enveloped-signature interop
|
|
@@ -60,11 +57,6 @@ RSpec.describe "Moxml::C14n subset canonicalization" do
|
|
|
60
57
|
let(:ctx) { Moxml.new(adapter_name) }
|
|
61
58
|
|
|
62
59
|
C14N_SUBSET_CORPUS.each do |label, xml, xpath, expected|
|
|
63
|
-
next if label == "text selection" && adapter_name == :rexml
|
|
64
|
-
|
|
65
|
-
# rexml's adapter xpath does not return text nodes for
|
|
66
|
-
# text() node tests (D-07, pre-existing capability gap).
|
|
67
|
-
|
|
68
60
|
it "canonicalizes #{label}" do
|
|
69
61
|
doc = ctx.parse(xml)
|
|
70
62
|
expect(Moxml::C14n.canonicalize_subset(doc, xpath)).to eq(expected)
|
|
@@ -73,10 +65,8 @@ RSpec.describe "Moxml::C14n subset canonicalization" do
|
|
|
73
65
|
|
|
74
66
|
# ox/headed_ox strip comment content padding at the parse
|
|
75
67
|
# layer (D-06, pre-existing, same family as their PI content
|
|
76
|
-
# loss); only the rendered-comment case depends on it.
|
|
77
|
-
|
|
78
|
-
unless COMMENTS_PADDING_LOSS.include?(adapter_name) ||
|
|
79
|
-
COMMENT_SELECTION_GAP.include?(adapter_name)
|
|
68
|
+
# loss); only the rendered-comment case depends on it.
|
|
69
|
+
unless COMMENTS_PADDING_LOSS.include?(adapter_name)
|
|
80
70
|
it COMMENTS_CASE[0] do
|
|
81
71
|
doc = ctx.parse(COMMENTS_CASE[1])
|
|
82
72
|
expect(Moxml::C14n.canonicalize_subset(doc, COMMENTS_CASE[2],
|
|
@@ -13,7 +13,6 @@ require "moxml/c14n"
|
|
|
13
13
|
# Documented parse-layer divergences (skipped rows):
|
|
14
14
|
# - ox/headed_ox expand 
 to LF, strip inter-element whitespace
|
|
15
15
|
# and comment padding at parse (out of scope here)
|
|
16
|
-
# - oga keeps the target/content separator space in PI data
|
|
17
16
|
W3C_FULL_FORMS = [
|
|
18
17
|
["attribute-value whitespace collapses to single spaces",
|
|
19
18
|
"<doc>\n <e attr1=\"v1\"\n attr2 = \"v2\"\n > content </e>\n</doc>",
|
|
@@ -55,11 +54,9 @@ RSpec.describe "Moxml::C14n W3C REC-xml-c14n examples" do
|
|
|
55
54
|
|
|
56
55
|
W3C_FULL_FORMS.each_with_index do |(label, xml, expected), idx|
|
|
57
56
|
# ox/headed_ox strip inter-element whitespace, expand 
|
|
58
|
-
# and pad comments at parse (D-05, D-06)
|
|
59
|
-
# separator space (D-04).
|
|
57
|
+
# and pad comments at parse (D-05, D-06).
|
|
60
58
|
skip_shapes = { ox: [0, CR_SHAPE, PI_SHAPE],
|
|
61
|
-
headed_ox: [0, CR_SHAPE, PI_SHAPE]
|
|
62
|
-
oga: [PI_SHAPE] }[adapter_name] || []
|
|
59
|
+
headed_ox: [0, CR_SHAPE, PI_SHAPE] }[adapter_name] || []
|
|
63
60
|
next if skip_shapes.include?(idx)
|
|
64
61
|
|
|
65
62
|
it "canonicalizes #{label}" do
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# Element-level indentation is a declared capability (model, Serialization;
|
|
6
|
+
# D-09): engines that cannot honor the knob raise the named AdapterError
|
|
7
|
+
# instead of answering unindented output. indent: 0 and the no-option
|
|
8
|
+
# path stay common — consumers use them for canonical shapes.
|
|
9
|
+
RSpec.describe "element-level serialization indentation" do
|
|
10
|
+
it "honors a positive indent on engines that support it" do
|
|
11
|
+
%i[nokogiri rexml].each do |name|
|
|
12
|
+
doc = Moxml.new(name).parse("<r><a/></r>")
|
|
13
|
+
expect(doc.root.to_xml(indent: 2)).to include("\n"),
|
|
14
|
+
"#{name}: indent not honored"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it "raises the named AdapterError on engines that ignore the knob" do
|
|
19
|
+
%i[oga ox headed_ox libxml leptris].each do |name|
|
|
20
|
+
doc = Moxml.new(name).parse("<r><a/></r>")
|
|
21
|
+
expect { doc.root.to_xml(indent: 2) }
|
|
22
|
+
.to raise_error(Moxml::AdapterError, /indentation/),
|
|
23
|
+
"#{name}: silent unindented answer"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "keeps indent: 0 and the no-option path common on every adapter" do
|
|
28
|
+
Moxml::Adapter::AVAILABLE_ADAPTERS.each do |name|
|
|
29
|
+
doc = Moxml.new(name).parse("<r><a/></r>")
|
|
30
|
+
expect(doc.root.to_xml(indent: 0)).to include("<a")
|
|
31
|
+
expect(doc.root.to_xml).to include("<a")
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
data/spec/moxml/node_spec.rb
CHANGED
|
@@ -156,9 +156,6 @@ RSpec.describe Moxml::Node do
|
|
|
156
156
|
end
|
|
157
157
|
|
|
158
158
|
it "locates the node via its path" do
|
|
159
|
-
# Ox translates only a limited XPath subset (no positional predicates)
|
|
160
|
-
skip "D-12: ox xpath translation does not support positional predicates" if adapter_name == :ox
|
|
161
|
-
|
|
162
159
|
expect(doc.at_xpath(title.path)).to eq(title)
|
|
163
160
|
end
|
|
164
161
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "tempfile"
|
|
5
|
+
|
|
6
|
+
# RELAX NG facade (TODO.sota/05): engines with an RNG validator
|
|
7
|
+
# answer valid?/validate/validate_errors; engines without one raise
|
|
8
|
+
# the named NotImplementedError at compile and answer supported?
|
|
9
|
+
# without raising.
|
|
10
|
+
RSpec.describe Moxml::RelaxNG do
|
|
11
|
+
RNG_SCHEMA = <<~RNG # rubocop:disable Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration
|
|
12
|
+
<element name="r" xmlns="http://relaxng.org/ns/structure/1.0">
|
|
13
|
+
<attribute name="a"><text/></attribute>
|
|
14
|
+
</element>
|
|
15
|
+
RNG
|
|
16
|
+
|
|
17
|
+
let(:valid_doc) { Moxml.new(:leptris).parse('<r a="x"/>') }
|
|
18
|
+
let(:invalid_doc) { Moxml.new(:leptris).parse("<r/>") }
|
|
19
|
+
|
|
20
|
+
it "answers supported? without raising on every adapter" do
|
|
21
|
+
Moxml::Adapter::AVAILABLE_ADAPTERS.each do |name|
|
|
22
|
+
ctx = Moxml.new(name)
|
|
23
|
+
expect { described_class.supported?(ctx) }.not_to raise_error
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "validates against a compiled schema and reports findings" do
|
|
28
|
+
ctx = Moxml.new(:leptris)
|
|
29
|
+
skip "leptris binding has no RNG face" unless described_class.supported?(ctx)
|
|
30
|
+
|
|
31
|
+
schema = ctx.relaxng(RNG_SCHEMA)
|
|
32
|
+
expect(schema.valid?(valid_doc)).to be(true)
|
|
33
|
+
expect(schema.validate(valid_doc)).to eq([])
|
|
34
|
+
expect(schema.valid?(invalid_doc)).to be(false)
|
|
35
|
+
error = schema.validate_errors(invalid_doc).first
|
|
36
|
+
expect(error[:message]).to include("missing required attribute")
|
|
37
|
+
expect(error[:line]).to eq(1)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it "compiles a schema from a file" do
|
|
41
|
+
ctx = Moxml.new(:leptris)
|
|
42
|
+
skip "leptris binding has no RNG face" unless described_class.supported?(ctx)
|
|
43
|
+
|
|
44
|
+
Tempfile.create(["schema", ".rng"]) do |file|
|
|
45
|
+
file.write(RNG_SCHEMA)
|
|
46
|
+
file.flush
|
|
47
|
+
schema = ctx.relaxng_file(file.path)
|
|
48
|
+
expect(schema.valid?(valid_doc)).to be(true)
|
|
49
|
+
expect(schema.valid?(invalid_doc)).to be(false)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it "raises a named NotImplementedError on engines without an RNG validator" do
|
|
54
|
+
ctx = Moxml.new(:nokogiri)
|
|
55
|
+
expect(described_class.supported?(ctx)).to be(false)
|
|
56
|
+
expect { ctx.relaxng(RNG_SCHEMA) }
|
|
57
|
+
.to raise_error(Moxml::NotImplementedError, /RELAX NG/)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it "names a malformed schema as a ValidationError" do
|
|
61
|
+
ctx = Moxml.new(:leptris)
|
|
62
|
+
skip "leptris binding has no RNG face" unless described_class.supported?(ctx)
|
|
63
|
+
|
|
64
|
+
expect { ctx.relaxng("<not-a-schema/>") }
|
|
65
|
+
.to raise_error(Moxml::ValidationError, /RELAX NG/)
|
|
66
|
+
end
|
|
67
|
+
end
|
data/spec/moxml/sax_spec.rb
CHANGED
|
@@ -412,50 +412,39 @@ RSpec.describe Moxml::SAX do
|
|
|
412
412
|
expect(handler.error_message).to match(/invalid|unclosed|premature|mismatch|not closed/i)
|
|
413
413
|
end
|
|
414
414
|
|
|
415
|
-
it "
|
|
416
|
-
# Ox SAX
|
|
417
|
-
#
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
context.sax_parse(xml_with_cdata) do
|
|
424
|
-
cdata { |_text| cdata_found = true }
|
|
425
|
-
end
|
|
426
|
-
|
|
427
|
-
# This will fail because Ox doesn't support separate CDATA events
|
|
428
|
-
expect(cdata_found).to be true
|
|
415
|
+
it "declares CDATA event subscriptions loudly (D-10)" do
|
|
416
|
+
# Ox SAX has no separate CDATA events; a subscriber gets the
|
|
417
|
+
# named error rather than an incomplete stream.
|
|
418
|
+
expect do
|
|
419
|
+
context.sax_parse("<root><![CDATA[special content]]></root>") do
|
|
420
|
+
cdata { |_text| nil }
|
|
421
|
+
end
|
|
422
|
+
end.to raise_error(Moxml::NotImplementedError, /cdata/)
|
|
429
423
|
end
|
|
430
424
|
|
|
431
|
-
it "
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
comment_found = false
|
|
438
|
-
context.sax_parse(xml_with_comment) do
|
|
439
|
-
comment { |_text| comment_found = true }
|
|
440
|
-
end
|
|
441
|
-
|
|
442
|
-
# This will fail because Ox doesn't support comment events
|
|
443
|
-
expect(comment_found).to be true
|
|
425
|
+
it "declares comment event subscriptions loudly (D-10)" do
|
|
426
|
+
expect do
|
|
427
|
+
context.sax_parse("<root><!-- comment --><data>content</data></root>") do
|
|
428
|
+
comment { |_text| nil }
|
|
429
|
+
end
|
|
430
|
+
end.to raise_error(Moxml::NotImplementedError, /comment/)
|
|
444
431
|
end
|
|
445
432
|
|
|
446
|
-
it "
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
433
|
+
it "declares PI event subscriptions loudly (D-10)" do
|
|
434
|
+
expect do
|
|
435
|
+
context.sax_parse('<?xml-stylesheet type="text/xsl" href="style.xsl"?><root/>') do
|
|
436
|
+
processing_instruction { |_target, _data| nil }
|
|
437
|
+
end
|
|
438
|
+
end.to raise_error(Moxml::NotImplementedError, /processing_instruction/)
|
|
439
|
+
end
|
|
451
440
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
end
|
|
441
|
+
it "declares unsupported families loudly for class handlers too" do
|
|
442
|
+
handler = Class.new(Moxml::SAX::Handler) do
|
|
443
|
+
def on_cdata(_text); end
|
|
444
|
+
end.new
|
|
456
445
|
|
|
457
|
-
|
|
458
|
-
|
|
446
|
+
expect { context.sax_parse("<root/>", handler) }
|
|
447
|
+
.to raise_error(Moxml::NotImplementedError, /cdata/)
|
|
459
448
|
end
|
|
460
449
|
end
|
|
461
450
|
|
|
@@ -1024,46 +1013,28 @@ RSpec.describe Moxml::SAX do
|
|
|
1024
1013
|
expect(handler.error_message).to match(/invalid|unclosed|premature|mismatch|not closed/i)
|
|
1025
1014
|
end
|
|
1026
1015
|
|
|
1027
|
-
it "
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
cdata_found = false
|
|
1034
|
-
context.sax_parse(xml_with_cdata) do
|
|
1035
|
-
cdata { |_text| cdata_found = true }
|
|
1036
|
-
end
|
|
1037
|
-
|
|
1038
|
-
expect(cdata_found).to be true
|
|
1016
|
+
it "declares CDATA event subscriptions loudly (inherited from Ox, D-10)" do
|
|
1017
|
+
expect do
|
|
1018
|
+
context.sax_parse("<root><![CDATA[special content]]></root>") do
|
|
1019
|
+
cdata { |_text| nil }
|
|
1020
|
+
end
|
|
1021
|
+
end.to raise_error(Moxml::NotImplementedError, /cdata/)
|
|
1039
1022
|
end
|
|
1040
1023
|
|
|
1041
|
-
it "
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
comment_found = false
|
|
1048
|
-
context.sax_parse(xml_with_comment) do
|
|
1049
|
-
comment { |_text| comment_found = true }
|
|
1050
|
-
end
|
|
1051
|
-
|
|
1052
|
-
expect(comment_found).to be true
|
|
1024
|
+
it "declares comment event subscriptions loudly (inherited from Ox, D-10)" do
|
|
1025
|
+
expect do
|
|
1026
|
+
context.sax_parse("<root><!-- comment --><data>content</data></root>") do
|
|
1027
|
+
comment { |_text| nil }
|
|
1028
|
+
end
|
|
1029
|
+
end.to raise_error(Moxml::NotImplementedError, /comment/)
|
|
1053
1030
|
end
|
|
1054
1031
|
|
|
1055
|
-
it "
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
pi_found = false
|
|
1062
|
-
context.sax_parse(xml_with_pi) do
|
|
1063
|
-
processing_instruction { |_target, _data| pi_found = true }
|
|
1064
|
-
end
|
|
1065
|
-
|
|
1066
|
-
expect(pi_found).to be true
|
|
1032
|
+
it "declares PI event subscriptions loudly (inherited from Ox, D-10)" do
|
|
1033
|
+
expect do
|
|
1034
|
+
context.sax_parse('<?xml-stylesheet type="text/xsl" href="style.xsl"?><root/>') do
|
|
1035
|
+
processing_instruction { |_target, _data| nil }
|
|
1036
|
+
end
|
|
1037
|
+
end.to raise_error(Moxml::NotImplementedError, /processing_instruction/)
|
|
1067
1038
|
end
|
|
1068
1039
|
end
|
|
1069
1040
|
end
|