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.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/dependent-repos.json +7 -2
  3. data/.github/workflows/dependent-tests.yml +4 -1
  4. data/.github/workflows/nightly-fuzz.yml +80 -0
  5. data/.github/workflows/performance-release.yml +79 -0
  6. data/.github/workflows/performance.yml +59 -0
  7. data/Rakefile +23 -0
  8. data/benchmark/README.md +106 -0
  9. data/benchmark/scoreboard.json +115 -0
  10. data/benchmark/scoreboard.rb +556 -0
  11. data/bin/generate-model-coverage +21 -0
  12. data/docs/_pages/adapter-compatibility.adoc +3 -3
  13. data/docs/_pages/divergences.adoc +88 -39
  14. data/docs/_pages/ecosystem.adoc +62 -0
  15. data/docs/_pages/headed-ox-limitations.adoc +1 -1
  16. data/docs/_pages/headed-ox.adoc +2 -2
  17. data/docs/_pages/index.adoc +4 -0
  18. data/docs/_pages/model-coverage.adoc +57 -0
  19. data/docs/_pages/performance.adoc +29 -0
  20. data/docs/_pages/why-moxml.adoc +87 -0
  21. data/docs/_references/processing-model.adoc +69 -3
  22. data/lib/compat/opal/moxml_boot.rb +4 -0
  23. data/lib/moxml/adapter/base.rb +73 -0
  24. data/lib/moxml/adapter/leptris.rb +293 -23
  25. data/lib/moxml/adapter/libxml.rb +179 -25
  26. data/lib/moxml/adapter/nokogiri.rb +10 -0
  27. data/lib/moxml/adapter/oga.rb +71 -9
  28. data/lib/moxml/adapter/ox.rb +34 -2
  29. data/lib/moxml/adapter/rexml.rb +82 -17
  30. data/lib/moxml/c14n/data_model.rb +8 -0
  31. data/lib/moxml/c14n/processor.rb +17 -8
  32. data/lib/moxml/context.rb +33 -1
  33. data/lib/moxml/document.rb +18 -4
  34. data/lib/moxml/node.rb +75 -8
  35. data/lib/moxml/node_set.rb +42 -1
  36. data/lib/moxml/reference/normalizer.rb +63 -0
  37. data/lib/moxml/reference/parser.rb +417 -0
  38. data/lib/moxml/reference/writer.rb +121 -0
  39. data/lib/moxml/reference.rb +205 -0
  40. data/lib/moxml/relaxng.rb +62 -0
  41. data/lib/moxml/sax/block_handler.rb +7 -0
  42. data/lib/moxml/sax/handler.rb +20 -0
  43. data/lib/moxml/version.rb +1 -1
  44. data/lib/moxml/xpath/compiler.rb +17 -2
  45. data/lib/moxml/xpath/ordering.rb +97 -0
  46. data/lib/moxml/xpath.rb +29 -0
  47. data/lib/moxml.rb +2 -0
  48. data/spec/consistency/adapter_parity_spec.rb +24 -0
  49. data/spec/consistency/citation_hygiene_spec.rb +30 -0
  50. data/spec/consistency/differential_taxonomy_spec.rb +117 -0
  51. data/spec/consistency/divergence_register_spec.rb +9 -1
  52. data/spec/consistency/eol_normalization_spec.rb +2 -0
  53. data/spec/consistency/loud_degradation_spec.rb +2 -0
  54. data/spec/consistency/metamorphic_properties_spec.rb +195 -0
  55. data/spec/consistency/metamorphic_spec.rb +138 -0
  56. data/spec/consistency/model_coverage_audit_spec.rb +144 -61
  57. data/spec/consistency/published_docs_spec.rb +49 -0
  58. data/spec/consistency/reference_differential_spec.rb +186 -0
  59. data/spec/consistency/xpath_idioms_spec.rb +34 -5
  60. data/spec/consistency/xslt_spec.rb +2 -0
  61. data/spec/integration/shared_examples/edge_cases.rb +0 -13
  62. data/spec/moxml/adapter/leptris_attribute_snapshot_spec.rb +10 -7
  63. data/spec/moxml/adapter/leptris_spec.rb +23 -3
  64. data/spec/moxml/adapter/ox_spec.rb +1 -1
  65. data/spec/moxml/adapter/shared_examples/adapter_contract.rb +0 -8
  66. data/spec/moxml/attribute_resolver_spec.rb +35 -0
  67. data/spec/moxml/c14n/adapter_delegation_spec.rb +35 -6
  68. data/spec/moxml/c14n/character_encoding_spec.rb +7 -13
  69. data/spec/moxml/c14n/comments_pis_spec.rb +35 -8
  70. data/spec/moxml/c14n/subset_spec.rb +2 -12
  71. data/spec/moxml/c14n/w3c_examples_spec.rb +2 -5
  72. data/spec/moxml/doctype_spec.rb +7 -0
  73. data/spec/moxml/document_parse_diagnostics_spec.rb +2 -0
  74. data/spec/moxml/element_indent_spec.rb +34 -0
  75. data/spec/moxml/entity_spec.rb +2 -0
  76. data/spec/moxml/node_spec.rb +0 -3
  77. data/spec/moxml/plan_conformance_spec.rb +2 -0
  78. data/spec/moxml/processing_instruction_spec.rb +9 -0
  79. data/spec/moxml/reference_opal_spec.rb +29 -0
  80. data/spec/moxml/reference_spec.rb +162 -0
  81. data/spec/moxml/relax_ng_spec.rb +67 -0
  82. data/spec/moxml/sax_spec.rb +47 -74
  83. data/spec/moxml/text_spec.rb +2 -0
  84. data/spec/moxml/to_html_spec.rb +2 -0
  85. data/spec/moxml/xhtml_output_spec.rb +2 -0
  86. data/spec/moxml/xpath/axes_spec.rb +2 -2
  87. data/spec/moxml/xpath_capabilities_spec.rb +11 -38
  88. data/spec/moxml/xsd_spec.rb +38 -0
  89. data/spec/support/input_generator.rb +154 -0
  90. data/spec/support/input_taxonomy.rb +219 -0
  91. data/spec/support/model_coverage.rb +159 -0
  92. data/spec/support/xml_generator.rb +54 -0
  93. 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
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Processing model §XSLT (normative): pinned here.
4
+
3
5
  require "spec_helper"
4
6
  require "moxml/c14n"
5
7
 
@@ -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 output contract is stride-identical to the
9
- # binding's per-attribute face: qualified names, document order,
10
- # fresh values.
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"], ["xml:lang", "en"], ["ns:p", "v"]],
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 m:x k],
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 element serialization with tab units (leptris-ruby#115)" do
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.root.to_xml(indent: 1, indent_text: "\t"))
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(RuntimeError, /must be a String or Ox::Node/)
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
- # oga surfaces the XML declaration as a <?xml?> PI and keeps the
61
- # separator space in PI content (D-04); ox/headed_ox drop PI
62
- # content on parse (D-05). Both are pre-existing parse-layer
63
- # divergences, out of scope here.
64
- let(:adapters) { Moxml::Adapter::AVAILABLE_ADAPTERS - %i[oga ox headed_ox] }
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
- adapters.each do |adapter_name|
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
- unless OGA_LEAK.include?(adapter_name)
25
- it "transcodes declared Latin-1 to UTF-8 bytes" do
26
- doc = ctx.parse(LATIN1_XML.dup)
27
- expect(Moxml::C14n.canonicalize(doc).bytes).to eq(CAF_E_UTF8)
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
- it "strips a leading UTF-8 BOM" do
31
- doc = ctx.parse(BOM_XML.dup)
32
- expect(Moxml::C14n.canonicalize(doc)).to eq("<r></r>")
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
- # TODO.c14n/09: Document-level PIs (outside the document element) are
52
- # not yet rendered correctly. The moxml PI parser mis-parses document-level
53
- # PIs, and the DataModel.from_document path drops them. Fixing requires
54
- # adapter-level work and is tracked separately.
55
- it "renders PIs outside the document element (KNOWN LIMITATION)" do
56
- skip "D-08: document-level PI rendering not yet implemented"
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 (KNOWN LIMITATION)" do
62
- skip "D-08: document-level comment rendering not yet implemented"
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. rexml's
77
- # comment() selection gap (D-07) also only affects this case.
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 &#xD; 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 &#xD;
58
- # and pad comments at parse (D-05, D-06); oga keeps the PI
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