moxml 0.5.149 → 0.5.151

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 (63) 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/performance-release.yml +79 -0
  5. data/.github/workflows/performance.yml +59 -0
  6. data/Rakefile +23 -0
  7. data/benchmark/README.md +106 -0
  8. data/benchmark/scoreboard.json +115 -0
  9. data/benchmark/scoreboard.rb +556 -0
  10. data/bin/generate-model-coverage +21 -0
  11. data/docs/_pages/adapter-compatibility.adoc +3 -3
  12. data/docs/_pages/divergences.adoc +32 -41
  13. data/docs/_pages/ecosystem.adoc +62 -0
  14. data/docs/_pages/headed-ox-limitations.adoc +1 -1
  15. data/docs/_pages/headed-ox.adoc +2 -2
  16. data/docs/_pages/index.adoc +4 -0
  17. data/docs/_pages/model-coverage.adoc +57 -0
  18. data/docs/_pages/performance.adoc +29 -0
  19. data/docs/_pages/why-moxml.adoc +87 -0
  20. data/docs/_references/processing-model.adoc +42 -3
  21. data/lib/moxml/adapter/base.rb +80 -0
  22. data/lib/moxml/adapter/leptris.rb +238 -4
  23. data/lib/moxml/adapter/libxml.rb +34 -2
  24. data/lib/moxml/adapter/nokogiri.rb +10 -0
  25. data/lib/moxml/adapter/oga.rb +39 -7
  26. data/lib/moxml/adapter/ox.rb +8 -0
  27. data/lib/moxml/adapter/rexml.rb +17 -2
  28. data/lib/moxml/attribute_resolver.rb +4 -0
  29. data/lib/moxml/c14n/data_model.rb +8 -0
  30. data/lib/moxml/c14n/processor.rb +17 -8
  31. data/lib/moxml/context.rb +53 -2
  32. data/lib/moxml/document.rb +28 -4
  33. data/lib/moxml/element.rb +13 -1
  34. data/lib/moxml/node.rb +85 -11
  35. data/lib/moxml/node_set.rb +31 -1
  36. data/lib/moxml/relaxng.rb +62 -0
  37. data/lib/moxml/sax/block_handler.rb +7 -0
  38. data/lib/moxml/sax/handler.rb +20 -0
  39. data/lib/moxml/version.rb +1 -1
  40. data/lib/moxml/xpath.rb +28 -0
  41. data/lib/moxml.rb +1 -0
  42. data/spec/consistency/adapter_parity_spec.rb +22 -0
  43. data/spec/consistency/citation_hygiene_spec.rb +30 -0
  44. data/spec/consistency/model_coverage_audit_spec.rb +61 -86
  45. data/spec/consistency/published_docs_spec.rb +49 -0
  46. data/spec/consistency/xpath_idioms_spec.rb +23 -5
  47. data/spec/integration/shared_examples/edge_cases.rb +0 -13
  48. data/spec/moxml/adapter/leptris_spec.rb +113 -3
  49. data/spec/moxml/adapter/shared_examples/adapter_contract.rb +0 -8
  50. data/spec/moxml/attribute_resolver_spec.rb +33 -0
  51. data/spec/moxml/c14n/adapter_delegation_spec.rb +33 -6
  52. data/spec/moxml/c14n/character_encoding_spec.rb +7 -13
  53. data/spec/moxml/c14n/comments_pis_spec.rb +35 -8
  54. data/spec/moxml/c14n/subset_spec.rb +2 -12
  55. data/spec/moxml/c14n/w3c_examples_spec.rb +2 -5
  56. data/spec/moxml/element_indent_spec.rb +34 -0
  57. data/spec/moxml/node_spec.rb +0 -3
  58. data/spec/moxml/relax_ng_spec.rb +67 -0
  59. data/spec/moxml/sax_spec.rb +45 -74
  60. data/spec/moxml/xpath_capabilities_spec.rb +11 -38
  61. data/spec/support/input_taxonomy.rb +7 -0
  62. data/spec/support/model_coverage.rb +159 -0
  63. metadata +17 -2
@@ -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 
 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
@@ -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
@@ -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
@@ -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 "documents CDATA limitation" do
416
- # Ox SAX does not have separate CDATA events
417
- # All CDATA content is delivered as text() events
418
- skip "D-10: Ox SAX does not support separate CDATA events"
419
-
420
- xml_with_cdata = "<root><![CDATA[special content]]></root>"
421
-
422
- cdata_found = false
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 "documents comment limitation" do
432
- # Ox SAX does not have separate comment events
433
- skip "D-10: Ox SAX does not support separate comment events"
434
-
435
- xml_with_comment = "<root><!-- comment --><data>content</data></root>"
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 "documents processing instruction limitation" do
447
- # Ox SAX does not have separate PI events
448
- skip "D-10: Ox SAX does not support separate processing instruction events"
449
-
450
- xml_with_pi = '<?xml-stylesheet type="text/xsl" href="style.xsl"?><root/>'
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
- pi_found = false
453
- context.sax_parse(xml_with_pi) do
454
- processing_instruction { |_target, _data| pi_found = true }
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
- # This will fail because Ox doesn't support PI events
458
- expect(pi_found).to be true
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 "documents CDATA limitation (inherited from Ox)" do
1028
- # HeadedOx inherits Ox's SAX implementation, which does not have separate CDATA events
1029
- skip "D-10: HeadedOx SAX (inherited from Ox) does not support separate CDATA events"
1030
-
1031
- xml_with_cdata = "<root><![CDATA[special content]]></root>"
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 "documents comment limitation (inherited from Ox)" do
1042
- # HeadedOx inherits Ox's SAX implementation, which does not have separate comment events
1043
- skip "D-10: HeadedOx SAX (inherited from Ox) does not support separate comment events"
1044
-
1045
- xml_with_comment = "<root><!-- comment --><data>content</data></root>"
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 "documents processing instruction limitation (inherited from Ox)" do
1056
- # HeadedOx inherits Ox's SAX implementation, which does not have separate PI events
1057
- skip "D-10: HeadedOx SAX (inherited from Ox) does not support separate processing instruction events"
1058
-
1059
- xml_with_pi = '<?xml-stylesheet type="text/xsl" href="style.xsl"?><root/>'
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
@@ -81,9 +81,7 @@ RSpec.describe "XPath Capabilities" do
81
81
  it "supports attribute value matching ([@attr='value'])" do
82
82
  doc = Moxml.new.parse(simple_xml)
83
83
  programming = doc.xpath("//book[@category='programming']")
84
- expect(programming.length).to be >= 0 # May not work on all
85
- rescue Moxml::XPathError
86
- skip "D-12: Attribute value predicates not supported on #{adapter_name}"
84
+ expect(programming.length).to eq(1)
87
85
  end
88
86
  end
89
87
 
@@ -96,9 +94,7 @@ RSpec.describe "XPath Capabilities" do
96
94
  }
97
95
 
98
96
  items = doc.xpath("//book:item", namespaces)
99
- expect(items.length).to be >= 0
100
- rescue Moxml::XPathError, NoMethodError
101
- skip "D-12: Namespace XPath not supported on #{adapter_name}"
97
+ expect(items.length).to eq(2)
102
98
  end
103
99
 
104
100
  it "handles default namespaces" do
@@ -106,9 +102,7 @@ RSpec.describe "XPath Capabilities" do
106
102
  namespaces = { "lib" => "http://library.org" }
107
103
 
108
104
  library = doc.xpath("//lib:library", namespaces)
109
- expect(library.length).to be >= 0
110
- rescue Moxml::XPathError, NoMethodError
111
- skip "D-12: Default namespace XPath not supported on #{adapter_name}"
105
+ expect(library.length).to eq(1)
112
106
  end
113
107
  end
114
108
 
@@ -116,17 +110,13 @@ RSpec.describe "XPath Capabilities" do
116
110
  it "supports numeric positions ([1])" do
117
111
  doc = Moxml.new.parse(simple_xml)
118
112
  first_book = doc.xpath("//book[1]")
119
- expect(first_book.length).to be >= 0
120
- rescue Moxml::XPathError
121
- skip "D-12: Position predicates not supported on #{adapter_name}"
113
+ expect(first_book.length).to eq(1)
122
114
  end
123
115
 
124
116
  it "supports position() function" do
125
117
  doc = Moxml.new.parse(simple_xml)
126
118
  first_books = doc.xpath("//book[position()=1]")
127
- expect(first_books.length).to be >= 0
128
- rescue Moxml::XPathError
129
- skip "D-12: position() function not supported on #{adapter_name}"
119
+ expect(first_books.length).to eq(1)
130
120
  end
131
121
  end
132
122
 
@@ -134,20 +124,13 @@ RSpec.describe "XPath Capabilities" do
134
124
  it "supports count() function, returning the scalar unwrapped" do
135
125
  doc = Moxml.new.parse(simple_xml)
136
126
  result = doc.xpath("count(//book)")
137
- # REXML's XPath has no function support and yields an empty set
138
- skip "D-12: count() function not supported on #{adapter_name}" if result.is_a?(Moxml::NodeSet)
139
-
140
127
  expect(result).to eq(2)
141
- rescue Moxml::XPathError, NoMethodError
142
- skip "D-12: count() function not supported on #{adapter_name}"
143
128
  end
144
129
 
145
130
  it "supports text() node selection" do
146
131
  doc = Moxml.new.parse(simple_xml)
147
132
  texts = doc.xpath("//title/text()")
148
- expect(texts.length).to be >= 0
149
- rescue Moxml::XPathError
150
- skip "D-12: text() not supported on #{adapter_name}"
133
+ expect(texts.length).to eq(3)
151
134
  end
152
135
  end
153
136
 
@@ -155,17 +138,13 @@ RSpec.describe "XPath Capabilities" do
155
138
  it "supports numeric comparisons ([@attr < value])" do
156
139
  doc = Moxml.new.parse(simple_xml)
157
140
  cheap_books = doc.xpath("//book[@id < 2]")
158
- expect(cheap_books.length).to be >= 0
159
- rescue Moxml::XPathError
160
- skip "D-12: Numeric predicates not supported on #{adapter_name}"
141
+ expect(cheap_books.length).to eq(1)
161
142
  end
162
143
 
163
144
  it "supports boolean expressions ([cond1 and cond2])" do
164
145
  doc = Moxml.new.parse(simple_xml)
165
146
  result = doc.xpath("//book[@id and @category]")
166
- expect(result.length).to be >= 0
167
- rescue Moxml::XPathError
168
- skip "D-12: Boolean expressions not supported on #{adapter_name}"
147
+ expect(result.length).to eq(2)
169
148
  end
170
149
  end
171
150
 
@@ -173,17 +152,13 @@ RSpec.describe "XPath Capabilities" do
173
152
  it "supports child axis" do
174
153
  doc = Moxml.new.parse(simple_xml)
175
154
  children = doc.xpath("/library/child::book")
176
- expect(children.length).to be >= 0
177
- rescue Moxml::XPathError
178
- skip "D-12: child:: axis not supported on #{adapter_name}"
155
+ expect(children.length).to eq(2)
179
156
  end
180
157
 
181
158
  it "supports descendant axis" do
182
159
  doc = Moxml.new.parse(simple_xml)
183
160
  descendants = doc.xpath("//library/descendant::title")
184
- expect(descendants.length).to be >= 0
185
- rescue Moxml::XPathError
186
- skip "D-12: descendant:: axis not supported on #{adapter_name}"
161
+ expect(descendants.length).to eq(3)
187
162
  end
188
163
  end
189
164
 
@@ -191,9 +166,7 @@ RSpec.describe "XPath Capabilities" do
191
166
  it "supports union (|)" do
192
167
  doc = Moxml.new.parse(simple_xml)
193
168
  items = doc.xpath("//book | //magazine")
194
- expect(items.length).to be >= 0
195
- rescue Moxml::XPathError
196
- skip "D-12: Union operator not supported on #{adapter_name}"
169
+ expect(items.length).to eq(3)
197
170
  end
198
171
  end
199
172
  end
@@ -67,6 +67,10 @@ module InputTaxonomy
67
67
  text: { oga: "D-14" },
68
68
  pi_content: { ox: "D-05", headed_ox: "D-05" },
69
69
  },
70
+ "pi/comment/cdata: pi trailing whitespace" => {
71
+ text: { oga: "D-14" },
72
+ pi_content: { ox: "D-05", headed_ox: "D-05", rexml: "D-20" },
73
+ },
70
74
  "huge text with entities" => {
71
75
  text: { rexml: "D-16" },
72
76
  serialize: { rexml: "D-16" },
@@ -191,6 +195,9 @@ module InputTaxonomy
191
195
  Input.new("pi/comment/cdata: pi multi-space separator", "pi/comment/cdata edges",
192
196
  "<r><?pi data?></r>", true,
193
197
  "separator whitespace is not PI content (uniform)"),
198
+ Input.new("pi/comment/cdata: pi trailing whitespace", "pi/comment/cdata edges",
199
+ "<r><?pi data ?></r>", true,
200
+ "trailing whitespace is PI data (D-20 on rexml)"),
194
201
  Input.new("pi/comment/cdata: empty pi", "pi/comment/cdata edges",
195
202
  "<r><?pi?></r>", true, nil),
196
203
 
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The model-coverage matrix (TODO.sota/01, TODO.sota/07): every face
4
+ # family of the processing model mapped to the normative section it
5
+ # cites, the pin that enforces it, and per-adapter dispositions
6
+ # (registered divergence or named loud error). One source of truth for
7
+ # the audit, the model-document pin lists, and the published coverage
8
+ # page.
9
+ module ModelCoverage
10
+ MODEL_COVERAGE_MATRIX = {
11
+ "document order and children listing" => {
12
+ section: "Document order and traversal",
13
+ pin: "spec/consistency/adapter_parity_spec.rb",
14
+ },
15
+ "name resolution" => {
16
+ section: "Names and namespaces",
17
+ pin: "spec/consistency/xpath_idioms_spec.rb",
18
+ },
19
+ "namespace resolution and bindings normalization" => {
20
+ section: "Names and namespaces",
21
+ pin: "spec/consistency/xpath_idioms_spec.rb",
22
+ },
23
+ "attribute access" => {
24
+ section: "Access semantics",
25
+ pin: "spec/moxml/attribute_resolver_spec.rb",
26
+ },
27
+ "text/content/raw_content" => {
28
+ section: "Access semantics",
29
+ pin: "spec/moxml/text_spec.rb",
30
+ },
31
+ "parsing modes and diagnostics" => {
32
+ section: "Parsing",
33
+ pin: "spec/moxml/document_parse_diagnostics_spec.rb",
34
+ },
35
+ "EOL normalization" => {
36
+ section: "Parsing",
37
+ pin: "spec/consistency/eol_normalization_spec.rb",
38
+ exempt: { oga: "D-02", ox: "D-03" },
39
+ },
40
+ "entity modes" => {
41
+ section: "Parsing",
42
+ pin: "spec/moxml/entity_spec.rb",
43
+ },
44
+ "XPath evaluation and declared idioms" => {
45
+ section: "XPath evaluation",
46
+ pin: "spec/consistency/xpath_idioms_spec.rb",
47
+ },
48
+ "serialization shapes (xml/html/xhtml)" => {
49
+ section: "Serialization",
50
+ pin: "spec/moxml/xhtml_output_spec.rb",
51
+ },
52
+ "indentation knobs" => {
53
+ section: "Serialization",
54
+ pin: "spec/moxml/to_html_spec.rb",
55
+ exempt: { oga: "D-09", ox: "D-09", headed_ox: "D-09",
56
+ libxml: "D-09", leptris: "D-09" },
57
+ },
58
+ "canonicalization" => {
59
+ section: "Canonicalization",
60
+ pin: "spec/moxml/c14n/adapter_delegation_spec.rb",
61
+ exempt: { ox: "D-05", headed_ox: "D-05" },
62
+ },
63
+ "XSLT" => {
64
+ section: "XSLT",
65
+ pin: "spec/consistency/xslt_spec.rb",
66
+ loud: { oga: "Moxml::NotImplementedError",
67
+ ox: "Moxml::NotImplementedError",
68
+ headed_ox: "Moxml::NotImplementedError",
69
+ rexml: "Moxml::NotImplementedError",
70
+ libxml: "Moxml::NotImplementedError" },
71
+ },
72
+ "SAX" => {
73
+ section: "SAX",
74
+ pin: "spec/moxml/sax_spec.rb",
75
+ },
76
+ "materialization" => {
77
+ section: "Materialization",
78
+ pin: "spec/moxml/plan_conformance_spec.rb",
79
+ },
80
+ "loud capability contracts" => {
81
+ section: "Errors and loud degradation",
82
+ pin: "spec/consistency/loud_degradation_spec.rb",
83
+ },
84
+ }.freeze
85
+
86
+ # Normative sections that are not face families (meta/apparatus).
87
+ MODEL_COVERAGE_NON_FACE = [
88
+ "Status and precedence",
89
+ "Mission and invariant",
90
+ "Object model",
91
+ "Performance: a declared, non-semantic channel",
92
+ ].freeze
93
+
94
+ MODEL_COVERAGE_ADAPTERS = %i[nokogiri oga rexml ox headed_ox libxml leptris].freeze
95
+
96
+ module_function
97
+
98
+ def families
99
+ MODEL_COVERAGE_MATRIX.keys
100
+ end
101
+
102
+ def sections
103
+ MODEL_COVERAGE_MATRIX.values.map { |cell| cell[:section] }.uniq
104
+ end
105
+
106
+ def pins_for(section)
107
+ MODEL_COVERAGE_MATRIX.values.select { |cell| cell[:section] == section }
108
+ .filter_map { |cell| cell[:pin] }.uniq.sort
109
+ end
110
+
111
+ def disposition(family, adapter)
112
+ cell = MODEL_COVERAGE_MATRIX.fetch(family)
113
+ if (id = (cell[:exempt] || {})[adapter])
114
+ [:specd, id]
115
+ elsif (error = (cell[:loud] || {})[adapter])
116
+ [:loud, error]
117
+ else
118
+ [:equivalent, nil]
119
+ end
120
+ end
121
+
122
+ def cell_text(family, adapter)
123
+ kind, detail = disposition(family, adapter)
124
+ case kind
125
+ when :specd then detail
126
+ when :loud then "loud (#{detail.split('::').last})"
127
+ else "equivalent"
128
+ end
129
+ end
130
+
131
+ # The published page block, deterministic (no clock, no engine
132
+ # versions): the page is a view of the committed matrix and register,
133
+ # so a data change without regeneration fails CI.
134
+ def render_page_block
135
+ lines = []
136
+ lines << "Generated by `bundle exec rake docs:coverage` from the " \
137
+ "coverage matrix and the divergence register. Do not edit " \
138
+ "by hand: `spec/consistency/published_docs_spec.rb` fails " \
139
+ "when this block is stale."
140
+ lines << ""
141
+ lines << %([cols="4#{',1' * MODEL_COVERAGE_ADAPTERS.size}",options="header"])
142
+ lines << "|==="
143
+ lines << "| Face family | " \
144
+ "#{MODEL_COVERAGE_ADAPTERS.map { |adapter| adapter.to_s.tr('_', ' ') }.join(' | ')}"
145
+ families.each do |family|
146
+ lines << "| #{family} | " \
147
+ "#{MODEL_COVERAGE_ADAPTERS.map { |adapter| cell_text(family, adapter) }.join(' | ')}"
148
+ end
149
+ lines << "|==="
150
+ lines << ""
151
+ lines << "*equivalent* — all adapters answer the same observable " \
152
+ "result. *loud (...)* — the adapter raises the named error " \
153
+ "for that face. *D-NN* — a registered divergence in " \
154
+ "link:divergences.html[the register], with its rationale. " \
155
+ "Every cell cites a pin; see the processing model's " \
156
+ "`Pins::` line for the enforcing specs."
157
+ lines.join("\n")
158
+ end
159
+ end