moxml 0.5.148 → 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/nightly-fuzz.yml +80 -0
- 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 +88 -39
- 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 +69 -3
- data/lib/compat/opal/moxml_boot.rb +4 -0
- data/lib/moxml/adapter/base.rb +73 -0
- data/lib/moxml/adapter/leptris.rb +293 -23
- data/lib/moxml/adapter/libxml.rb +179 -25
- data/lib/moxml/adapter/nokogiri.rb +10 -0
- data/lib/moxml/adapter/oga.rb +71 -9
- data/lib/moxml/adapter/ox.rb +34 -2
- data/lib/moxml/adapter/rexml.rb +82 -17
- data/lib/moxml/c14n/data_model.rb +8 -0
- data/lib/moxml/c14n/processor.rb +17 -8
- data/lib/moxml/context.rb +33 -1
- data/lib/moxml/document.rb +18 -4
- data/lib/moxml/node.rb +75 -8
- data/lib/moxml/node_set.rb +42 -1
- data/lib/moxml/reference/normalizer.rb +63 -0
- data/lib/moxml/reference/parser.rb +417 -0
- data/lib/moxml/reference/writer.rb +121 -0
- data/lib/moxml/reference.rb +205 -0
- 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/compiler.rb +17 -2
- data/lib/moxml/xpath/ordering.rb +97 -0
- data/lib/moxml/xpath.rb +29 -0
- data/lib/moxml.rb +2 -0
- data/spec/consistency/adapter_parity_spec.rb +24 -0
- data/spec/consistency/citation_hygiene_spec.rb +30 -0
- data/spec/consistency/differential_taxonomy_spec.rb +117 -0
- data/spec/consistency/divergence_register_spec.rb +9 -1
- data/spec/consistency/eol_normalization_spec.rb +2 -0
- data/spec/consistency/loud_degradation_spec.rb +2 -0
- data/spec/consistency/metamorphic_properties_spec.rb +195 -0
- data/spec/consistency/metamorphic_spec.rb +138 -0
- data/spec/consistency/model_coverage_audit_spec.rb +144 -61
- data/spec/consistency/published_docs_spec.rb +49 -0
- data/spec/consistency/reference_differential_spec.rb +186 -0
- data/spec/consistency/xpath_idioms_spec.rb +34 -5
- data/spec/consistency/xslt_spec.rb +2 -0
- data/spec/integration/shared_examples/edge_cases.rb +0 -13
- data/spec/moxml/adapter/leptris_attribute_snapshot_spec.rb +10 -7
- data/spec/moxml/adapter/leptris_spec.rb +23 -3
- data/spec/moxml/adapter/ox_spec.rb +1 -1
- data/spec/moxml/adapter/shared_examples/adapter_contract.rb +0 -8
- data/spec/moxml/attribute_resolver_spec.rb +35 -0
- data/spec/moxml/c14n/adapter_delegation_spec.rb +35 -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/doctype_spec.rb +7 -0
- data/spec/moxml/document_parse_diagnostics_spec.rb +2 -0
- data/spec/moxml/element_indent_spec.rb +34 -0
- data/spec/moxml/entity_spec.rb +2 -0
- data/spec/moxml/node_spec.rb +0 -3
- data/spec/moxml/plan_conformance_spec.rb +2 -0
- data/spec/moxml/processing_instruction_spec.rb +9 -0
- data/spec/moxml/reference_opal_spec.rb +29 -0
- data/spec/moxml/reference_spec.rb +162 -0
- data/spec/moxml/relax_ng_spec.rb +67 -0
- data/spec/moxml/sax_spec.rb +47 -74
- data/spec/moxml/text_spec.rb +2 -0
- data/spec/moxml/to_html_spec.rb +2 -0
- data/spec/moxml/xhtml_output_spec.rb +2 -0
- data/spec/moxml/xpath/axes_spec.rb +2 -2
- data/spec/moxml/xpath_capabilities_spec.rb +11 -38
- data/spec/moxml/xsd_spec.rb +38 -0
- data/spec/support/input_generator.rb +154 -0
- data/spec/support/input_taxonomy.rb +219 -0
- data/spec/support/model_coverage.rb +159 -0
- data/spec/support/xml_generator.rb +54 -0
- metadata +32 -2
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "support/input_taxonomy"
|
|
5
|
+
|
|
6
|
+
# Executable-reference differential (TODO.sota/03): every adapter's
|
|
7
|
+
# answer for each taxonomy input is compared against the pure-Ruby
|
|
8
|
+
# reference implementation (Moxml::Reference), not against another
|
|
9
|
+
# adapter. Faces: document order, children listing, access truth
|
|
10
|
+
# table, attributes, PI/comment data, and serialization round-trip.
|
|
11
|
+
# Cells that differ cite a registered divergence (D-NN) or the
|
|
12
|
+
# adapter is fixed.
|
|
13
|
+
RSpec.describe "executable reference differential" do
|
|
14
|
+
let(:references) { {} }
|
|
15
|
+
|
|
16
|
+
def adapters
|
|
17
|
+
Moxml::Adapter::AVAILABLE_ADAPTERS.uniq
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def exempt?(input, face, adapter)
|
|
21
|
+
InputTaxonomy::EXEMPT.dig(input.id, face, adapter)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def reference_for(input)
|
|
25
|
+
references[input.id] ||= Moxml::Reference.parse(input.xml)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def adapter_document(name, xml)
|
|
29
|
+
Moxml.new(name).parse(xml)
|
|
30
|
+
rescue Moxml::Error, NotImplementedError, LoadError
|
|
31
|
+
nil # unavailable in this environment
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def elements(doc)
|
|
35
|
+
doc.xpath("//*").to_a
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def reference_elements(reference)
|
|
39
|
+
rows = []
|
|
40
|
+
reference.each_node { |node| rows << node if node.element? }
|
|
41
|
+
rows
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def pi_rows(doc)
|
|
45
|
+
doc.traverse.filter_map do |node|
|
|
46
|
+
[node.name, node.content] if node.node_type == :processing_instruction
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def comment_rows(doc)
|
|
51
|
+
doc.traverse.filter_map do |node|
|
|
52
|
+
node.content if node.node_type == :comment
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def compact(text)
|
|
57
|
+
text.to_s.gsub(/\s+/, "")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it "answers document order and children listing like the reference" do
|
|
61
|
+
InputTaxonomy.well_formed.each do |input|
|
|
62
|
+
ref = reference_for(input)
|
|
63
|
+
expected_order = Moxml::Reference.element_names(ref)
|
|
64
|
+
expected_children = Moxml::Reference.child_element_names(ref)
|
|
65
|
+
adapters.each do |name|
|
|
66
|
+
doc = adapter_document(name, input.xml)
|
|
67
|
+
next unless doc # unavailable
|
|
68
|
+
next if exempt?(input, :nodes, name)
|
|
69
|
+
|
|
70
|
+
got_order = elements(doc).map(&:name)
|
|
71
|
+
expect(got_order).to eq(expected_order),
|
|
72
|
+
"#{input.id}/#{name}: document order differs " \
|
|
73
|
+
"from the reference: #{got_order.inspect} vs " \
|
|
74
|
+
"#{expected_order.inspect}"
|
|
75
|
+
|
|
76
|
+
got_children = elements(doc).map do |element|
|
|
77
|
+
element.children.grep(Moxml::Element).map(&:name)
|
|
78
|
+
end
|
|
79
|
+
expect(got_children).to eq(expected_children),
|
|
80
|
+
"#{input.id}/#{name}: children listing " \
|
|
81
|
+
"differs from the reference: " \
|
|
82
|
+
"#{got_children.inspect} vs " \
|
|
83
|
+
"#{expected_children.inspect}"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it "answers element text like the reference" do
|
|
89
|
+
InputTaxonomy.well_formed.each do |input|
|
|
90
|
+
expected = Moxml::Reference.element_access_rows(reference_for(input))
|
|
91
|
+
.map { |(name, text, _content)| [name, text] }
|
|
92
|
+
adapters.each do |name|
|
|
93
|
+
doc = adapter_document(name, input.xml)
|
|
94
|
+
next unless doc
|
|
95
|
+
next if exempt?(input, :text, name)
|
|
96
|
+
|
|
97
|
+
got = elements(doc).map { |element| [element.name, element.text] }
|
|
98
|
+
expect(got).to eq(expected),
|
|
99
|
+
"#{input.id}/#{name}: text face differs from the " \
|
|
100
|
+
"reference: #{got.inspect} vs #{expected.inspect}"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it "answers attribute pairs like the reference" do
|
|
106
|
+
InputTaxonomy.well_formed.each do |input|
|
|
107
|
+
expected = reference_elements(reference_for(input)).map(&:attribute_pairs)
|
|
108
|
+
adapters.each do |name|
|
|
109
|
+
doc = adapter_document(name, input.xml)
|
|
110
|
+
next unless doc
|
|
111
|
+
|
|
112
|
+
got = elements(doc).map do |element|
|
|
113
|
+
element.attribute_pairs.map { |(key, value)| [key, value] }
|
|
114
|
+
end
|
|
115
|
+
expect(got).to eq(expected),
|
|
116
|
+
"#{input.id}/#{name}: attribute face differs from " \
|
|
117
|
+
"the reference: #{got.inspect} vs #{expected.inspect}"
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it "answers PI and comment data like the reference" do
|
|
123
|
+
InputTaxonomy.well_formed.each do |input|
|
|
124
|
+
ref = reference_for(input)
|
|
125
|
+
expected_pis = Moxml::Reference.processing_instructions(ref)
|
|
126
|
+
expected_comments = Moxml::Reference.comments(ref)
|
|
127
|
+
adapters.each do |name|
|
|
128
|
+
doc = adapter_document(name, input.xml)
|
|
129
|
+
next unless doc
|
|
130
|
+
|
|
131
|
+
got_pis = pi_rows(doc)
|
|
132
|
+
expect(got_pis.map(&:first)).to eq(expected_pis.map(&:first)),
|
|
133
|
+
"#{input.id}/#{name}: PI targets " \
|
|
134
|
+
"differ from the reference"
|
|
135
|
+
unless exempt?(input, :pi_content, name)
|
|
136
|
+
expect(got_pis).to eq(expected_pis),
|
|
137
|
+
"#{input.id}/#{name}: PI data differs " \
|
|
138
|
+
"from the reference"
|
|
139
|
+
end
|
|
140
|
+
unless exempt?(input, :comment_content, name)
|
|
141
|
+
expect(comment_rows(doc)).to eq(expected_comments),
|
|
142
|
+
"#{input.id}/#{name}: comment data " \
|
|
143
|
+
"differs from the reference"
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
it "serializes to XML that re-parses to the reference information set" do
|
|
150
|
+
InputTaxonomy.well_formed.each do |input|
|
|
151
|
+
ref = reference_for(input)
|
|
152
|
+
expected_order = Moxml::Reference.element_names(ref)
|
|
153
|
+
expected_children = Moxml::Reference.child_element_names(ref)
|
|
154
|
+
expected_text = Moxml::Reference.element_access_rows(ref)
|
|
155
|
+
.map { |(name, text, _content)| [name, compact(text)] }
|
|
156
|
+
adapters.each do |name|
|
|
157
|
+
doc = adapter_document(name, input.xml)
|
|
158
|
+
next unless doc
|
|
159
|
+
next if exempt?(input, :serialize, name)
|
|
160
|
+
|
|
161
|
+
begin
|
|
162
|
+
serialized = doc.to_xml(indent: 0)
|
|
163
|
+
rescue Moxml::Error, NotImplementedError, LoadError
|
|
164
|
+
next # named engine limit, pinned by its own spec
|
|
165
|
+
end
|
|
166
|
+
reparsed = begin
|
|
167
|
+
Moxml::Reference.parse(serialized)
|
|
168
|
+
rescue Moxml::ParseError => e
|
|
169
|
+
raise Moxml::ParseError,
|
|
170
|
+
"#{input.id}/#{name}: serialized XML is not well-formed: " \
|
|
171
|
+
"#{e.message}"
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
expect(Moxml::Reference.element_names(reparsed)).to eq(expected_order),
|
|
175
|
+
"#{input.id}/#{name}: serialized tree lost or reordered elements"
|
|
176
|
+
expect(Moxml::Reference.child_element_names(reparsed)).to eq(expected_children),
|
|
177
|
+
"#{input.id}/#{name}: serialized tree changed nesting"
|
|
178
|
+
got_text = Moxml::Reference.element_access_rows(reparsed)
|
|
179
|
+
.map { |(element, text, _content)| [element, compact(text)] }
|
|
180
|
+
expect(got_text).to eq(expected_text),
|
|
181
|
+
"#{input.id}/#{name}: serialized tree changed " \
|
|
182
|
+
"text: #{got_text.inspect} vs #{expected_text.inspect}"
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# Processing model §Names and namespaces (normative): pinned here.
|
|
4
|
+
# Processing model §XPath evaluation (normative): pinned here.
|
|
5
|
+
|
|
3
6
|
require "spec_helper"
|
|
4
7
|
|
|
5
8
|
# Cross-adapter XPath idioms that nokogiri consumers rely on:
|
|
@@ -39,11 +42,6 @@ RSpec.describe "XPath idioms across adapters" do
|
|
|
39
42
|
Gem::Version.new(Leptris::VERSION) < Gem::Version.new("1.9.359.0")
|
|
40
43
|
skip "binding #{Leptris::VERSION} predates the xmlns: fix (1.9.359)"
|
|
41
44
|
end
|
|
42
|
-
if adapter_name == :oga
|
|
43
|
-
# oga's own Ruby XPath engine has no xmlns: default-ns
|
|
44
|
-
# support (third-party; explicit bindings work).
|
|
45
|
-
skip "D-01: oga's XPath engine does not implement the xmlns: idiom"
|
|
46
|
-
end
|
|
47
45
|
end
|
|
48
46
|
|
|
49
47
|
it "matches default-namespaced elements via the xmlns: prefix" do
|
|
@@ -59,6 +57,14 @@ RSpec.describe "XPath idioms across adapters" do
|
|
|
59
57
|
expect(targets).to be_empty
|
|
60
58
|
end
|
|
61
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
|
+
|
|
62
68
|
it "resolves attribute-terminal steps in every path position" do
|
|
63
69
|
doc = ctx.parse(plain_xml)
|
|
64
70
|
expect(doc.xpath("//a/@id").first&.value).to eq("x")
|
|
@@ -107,6 +113,29 @@ RSpec.describe "XPath idioms across adapters" do
|
|
|
107
113
|
expect(doc.at_xpath("/*")&.name).to eq("r")
|
|
108
114
|
expect(doc.at_xpath("/r")&.name).to eq("r")
|
|
109
115
|
end
|
|
116
|
+
|
|
117
|
+
it "answers scalar XPath functions unwrapped and uniform" do
|
|
118
|
+
doc = ctx.parse(plain_xml)
|
|
119
|
+
expect(doc.xpath("count(//a)")).to eq(1.0)
|
|
120
|
+
expect(doc.xpath("string(//a/@id)")).to eq("x")
|
|
121
|
+
expect(doc.xpath("boolean(//b)")).to be(true)
|
|
122
|
+
expect(doc.at_xpath("count(//a)")).to eq(1.0)
|
|
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
|
|
110
139
|
end
|
|
111
140
|
end
|
|
112
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">
|
|
@@ -5,10 +5,13 @@ require "spec_helper"
|
|
|
5
5
|
# moxml#375 — element hydration through the binding's
|
|
6
6
|
# attributes_snapshot face (libleptris 1.9.353, engine
|
|
7
7
|
# leptris#1689): one crossing for every attribute instead of one
|
|
8
|
-
# per attribute. The
|
|
9
|
-
#
|
|
10
|
-
#
|
|
8
|
+
# per attribute. The facade listing face is bare names (uniform with
|
|
9
|
+
# every adapter, and with Attribute#name), document order, fresh
|
|
10
|
+
# values.
|
|
11
11
|
RSpec.describe "leptris element-attribute snapshot hydration" do
|
|
12
|
+
def bare(pairs)
|
|
13
|
+
pairs.map { |(name, value)| [name.split(":", 2).last, value] }
|
|
14
|
+
end
|
|
12
15
|
let(:ctx) { Moxml.new(:leptris) }
|
|
13
16
|
|
|
14
17
|
context "when the snapshot face is armed (>= 1.9.353.0)" do
|
|
@@ -23,11 +26,11 @@ RSpec.describe "leptris element-attribute snapshot hydration" do
|
|
|
23
26
|
'<root a="1" xml:lang="en" xmlns:ns="urn:u" ns:p="v"/>',
|
|
24
27
|
)
|
|
25
28
|
binding_element = Moxml::Adapter::Leptris.to_binding(doc.root)
|
|
26
|
-
expected = binding_element.attribute_pairs
|
|
29
|
+
expected = bare(binding_element.attribute_pairs)
|
|
27
30
|
|
|
28
31
|
expect(doc.root.attribute_pairs).to eq(expected)
|
|
29
32
|
expect(doc.root.attribute_pairs).to eq(
|
|
30
|
-
[["a", "1"], ["
|
|
33
|
+
[["a", "1"], ["lang", "en"], ["p", "v"]],
|
|
31
34
|
)
|
|
32
35
|
end
|
|
33
36
|
|
|
@@ -36,7 +39,7 @@ RSpec.describe "leptris element-attribute snapshot hydration" do
|
|
|
36
39
|
'<root z="9" xmlns:m="urn:m" m:x="8" k="7"/>',
|
|
37
40
|
)
|
|
38
41
|
expect(doc.root.attribute_pairs.map(&:first)).to eq(
|
|
39
|
-
%w[z
|
|
42
|
+
%w[z x k],
|
|
40
43
|
)
|
|
41
44
|
end
|
|
42
45
|
|
|
@@ -71,7 +74,7 @@ RSpec.describe "leptris element-attribute snapshot hydration" do
|
|
|
71
74
|
)
|
|
72
75
|
binding_element = Moxml::Adapter::Leptris.to_binding(doc.root)
|
|
73
76
|
expect(doc.root.attribute_pairs).to eq(
|
|
74
|
-
binding_element.attribute_pairs,
|
|
77
|
+
bare(binding_element.attribute_pairs),
|
|
75
78
|
)
|
|
76
79
|
end
|
|
77
80
|
end
|
|
@@ -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
|
|
@@ -94,7 +94,7 @@ RSpec.describe Moxml::Adapter::Ox do
|
|
|
94
94
|
|
|
95
95
|
it "fails loudly on ox_mode: :hash (the adapter walks a tree, not a Hash)" do
|
|
96
96
|
expect { described_class.parse("<r><t> x </t></r>", ox_mode: :hash) }
|
|
97
|
-
.to raise_error(
|
|
97
|
+
.to raise_error(Moxml::ParseError, /must be a String or Ox::Node/)
|
|
98
98
|
end
|
|
99
99
|
|
|
100
100
|
it "surfaces an invalid ox_skip as Moxml::ParseError" do
|
|
@@ -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
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# Processing model §Access semantics (normative): pinned here.
|
|
4
|
+
|
|
3
5
|
require "spec_helper"
|
|
4
6
|
|
|
5
7
|
RSpec.describe Moxml::AttributeResolver do
|
|
@@ -29,6 +31,39 @@ RSpec.describe Moxml::AttributeResolver do
|
|
|
29
31
|
expect(described_class.resolve(element, "z:type")).to be_nil
|
|
30
32
|
expect(described_class.resolve(element, "xmlns:q")).to be_nil
|
|
31
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
|
|
32
67
|
end
|
|
33
68
|
|
|
34
69
|
describe ".assign" do
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# Processing model §Canonicalization (normative): pinned here.
|
|
4
|
+
|
|
3
5
|
require "spec_helper"
|
|
4
6
|
require "moxml/c14n"
|
|
5
7
|
|
|
@@ -57,20 +59,47 @@ RSpec.describe "Moxml::C14n cross-adapter canonicalization" do
|
|
|
57
59
|
end
|
|
58
60
|
|
|
59
61
|
describe "document-level nodes keep document order" do
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
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
|
|
65
69
|
|
|
66
70
|
it "canonicalizes a leading PI before the root element" do
|
|
67
|
-
|
|
71
|
+
content_adapters.each do |adapter_name|
|
|
68
72
|
doc = Moxml.new(adapter_name)
|
|
69
73
|
.parse(%(<?xml version="1.0"?><?render mode="x"?><doc><e/></doc>))
|
|
70
74
|
expect(Moxml::C14n.canonicalize(doc))
|
|
71
75
|
.to eq(%(<?render mode="x"?>\n<doc><e></e></doc>))
|
|
72
76
|
end
|
|
73
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
|
|
74
103
|
end
|
|
75
104
|
|
|
76
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
|