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.
- checksums.yaml +4 -4
- data/.github/workflows/dependent-repos.json +7 -2
- data/.github/workflows/dependent-tests.yml +4 -1
- data/.github/workflows/performance-release.yml +79 -0
- data/.github/workflows/performance.yml +59 -0
- data/Rakefile +23 -0
- data/benchmark/README.md +106 -0
- data/benchmark/scoreboard.json +115 -0
- data/benchmark/scoreboard.rb +556 -0
- data/bin/generate-model-coverage +21 -0
- data/docs/_pages/adapter-compatibility.adoc +3 -3
- data/docs/_pages/divergences.adoc +32 -41
- data/docs/_pages/ecosystem.adoc +62 -0
- data/docs/_pages/headed-ox-limitations.adoc +1 -1
- data/docs/_pages/headed-ox.adoc +2 -2
- data/docs/_pages/index.adoc +4 -0
- data/docs/_pages/model-coverage.adoc +57 -0
- data/docs/_pages/performance.adoc +29 -0
- data/docs/_pages/why-moxml.adoc +87 -0
- data/docs/_references/processing-model.adoc +42 -3
- data/lib/moxml/adapter/base.rb +80 -0
- data/lib/moxml/adapter/leptris.rb +238 -4
- data/lib/moxml/adapter/libxml.rb +34 -2
- data/lib/moxml/adapter/nokogiri.rb +10 -0
- data/lib/moxml/adapter/oga.rb +39 -7
- data/lib/moxml/adapter/ox.rb +8 -0
- data/lib/moxml/adapter/rexml.rb +17 -2
- data/lib/moxml/attribute_resolver.rb +4 -0
- data/lib/moxml/c14n/data_model.rb +8 -0
- data/lib/moxml/c14n/processor.rb +17 -8
- data/lib/moxml/context.rb +53 -2
- data/lib/moxml/document.rb +28 -4
- data/lib/moxml/element.rb +13 -1
- data/lib/moxml/node.rb +85 -11
- data/lib/moxml/node_set.rb +31 -1
- data/lib/moxml/relaxng.rb +62 -0
- data/lib/moxml/sax/block_handler.rb +7 -0
- data/lib/moxml/sax/handler.rb +20 -0
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xpath.rb +28 -0
- data/lib/moxml.rb +1 -0
- data/spec/consistency/adapter_parity_spec.rb +22 -0
- data/spec/consistency/citation_hygiene_spec.rb +30 -0
- data/spec/consistency/model_coverage_audit_spec.rb +61 -86
- data/spec/consistency/published_docs_spec.rb +49 -0
- data/spec/consistency/xpath_idioms_spec.rb +23 -5
- data/spec/integration/shared_examples/edge_cases.rb +0 -13
- data/spec/moxml/adapter/leptris_spec.rb +113 -3
- data/spec/moxml/adapter/shared_examples/adapter_contract.rb +0 -8
- data/spec/moxml/attribute_resolver_spec.rb +33 -0
- data/spec/moxml/c14n/adapter_delegation_spec.rb +33 -6
- data/spec/moxml/c14n/character_encoding_spec.rb +7 -13
- data/spec/moxml/c14n/comments_pis_spec.rb +35 -8
- data/spec/moxml/c14n/subset_spec.rb +2 -12
- data/spec/moxml/c14n/w3c_examples_spec.rb +2 -5
- data/spec/moxml/element_indent_spec.rb +34 -0
- data/spec/moxml/node_spec.rb +0 -3
- data/spec/moxml/relax_ng_spec.rb +67 -0
- data/spec/moxml/sax_spec.rb +45 -74
- data/spec/moxml/xpath_capabilities_spec.rb +11 -38
- data/spec/support/input_taxonomy.rb +7 -0
- data/spec/support/model_coverage.rb +159 -0
- metadata +17 -2
|
@@ -43,9 +43,6 @@ COMMENTS_EXCLUDED_CASE = [
|
|
|
43
43
|
].freeze
|
|
44
44
|
|
|
45
45
|
COMMENTS_PADDING_LOSS = %i[ox headed_ox].freeze
|
|
46
|
-
# rexml's adapter xpath does not return comment()/text() node results
|
|
47
|
-
# (pre-existing capability gap, same family for both axes).
|
|
48
|
-
COMMENT_SELECTION_GAP = %i[rexml].freeze
|
|
49
46
|
|
|
50
47
|
RSpec.describe "Moxml::C14n subset canonicalization" do
|
|
51
48
|
# Node-set semantics follow the enveloped-signature interop
|
|
@@ -60,11 +57,6 @@ RSpec.describe "Moxml::C14n subset canonicalization" do
|
|
|
60
57
|
let(:ctx) { Moxml.new(adapter_name) }
|
|
61
58
|
|
|
62
59
|
C14N_SUBSET_CORPUS.each do |label, xml, xpath, expected|
|
|
63
|
-
next if label == "text selection" && adapter_name == :rexml
|
|
64
|
-
|
|
65
|
-
# rexml's adapter xpath does not return text nodes for
|
|
66
|
-
# text() node tests (D-07, pre-existing capability gap).
|
|
67
|
-
|
|
68
60
|
it "canonicalizes #{label}" do
|
|
69
61
|
doc = ctx.parse(xml)
|
|
70
62
|
expect(Moxml::C14n.canonicalize_subset(doc, xpath)).to eq(expected)
|
|
@@ -73,10 +65,8 @@ RSpec.describe "Moxml::C14n subset canonicalization" do
|
|
|
73
65
|
|
|
74
66
|
# ox/headed_ox strip comment content padding at the parse
|
|
75
67
|
# layer (D-06, pre-existing, same family as their PI content
|
|
76
|
-
# loss); only the rendered-comment case depends on it.
|
|
77
|
-
|
|
78
|
-
unless COMMENTS_PADDING_LOSS.include?(adapter_name) ||
|
|
79
|
-
COMMENT_SELECTION_GAP.include?(adapter_name)
|
|
68
|
+
# loss); only the rendered-comment case depends on it.
|
|
69
|
+
unless COMMENTS_PADDING_LOSS.include?(adapter_name)
|
|
80
70
|
it COMMENTS_CASE[0] do
|
|
81
71
|
doc = ctx.parse(COMMENTS_CASE[1])
|
|
82
72
|
expect(Moxml::C14n.canonicalize_subset(doc, COMMENTS_CASE[2],
|
|
@@ -13,7 +13,6 @@ require "moxml/c14n"
|
|
|
13
13
|
# Documented parse-layer divergences (skipped rows):
|
|
14
14
|
# - ox/headed_ox expand 
 to LF, strip inter-element whitespace
|
|
15
15
|
# and comment padding at parse (out of scope here)
|
|
16
|
-
# - oga keeps the target/content separator space in PI data
|
|
17
16
|
W3C_FULL_FORMS = [
|
|
18
17
|
["attribute-value whitespace collapses to single spaces",
|
|
19
18
|
"<doc>\n <e attr1=\"v1\"\n attr2 = \"v2\"\n > content </e>\n</doc>",
|
|
@@ -55,11 +54,9 @@ RSpec.describe "Moxml::C14n W3C REC-xml-c14n examples" do
|
|
|
55
54
|
|
|
56
55
|
W3C_FULL_FORMS.each_with_index do |(label, xml, expected), idx|
|
|
57
56
|
# ox/headed_ox strip inter-element whitespace, expand 
|
|
58
|
-
# and pad comments at parse (D-05, D-06)
|
|
59
|
-
# separator space (D-04).
|
|
57
|
+
# and pad comments at parse (D-05, D-06).
|
|
60
58
|
skip_shapes = { ox: [0, CR_SHAPE, PI_SHAPE],
|
|
61
|
-
headed_ox: [0, CR_SHAPE, PI_SHAPE]
|
|
62
|
-
oga: [PI_SHAPE] }[adapter_name] || []
|
|
59
|
+
headed_ox: [0, CR_SHAPE, PI_SHAPE] }[adapter_name] || []
|
|
63
60
|
next if skip_shapes.include?(idx)
|
|
64
61
|
|
|
65
62
|
it "canonicalizes #{label}" do
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# Element-level indentation is a declared capability (model, Serialization;
|
|
6
|
+
# D-09): engines that cannot honor the knob raise the named AdapterError
|
|
7
|
+
# instead of answering unindented output. indent: 0 and the no-option
|
|
8
|
+
# path stay common — consumers use them for canonical shapes.
|
|
9
|
+
RSpec.describe "element-level serialization indentation" do
|
|
10
|
+
it "honors a positive indent on engines that support it" do
|
|
11
|
+
%i[nokogiri rexml].each do |name|
|
|
12
|
+
doc = Moxml.new(name).parse("<r><a/></r>")
|
|
13
|
+
expect(doc.root.to_xml(indent: 2)).to include("\n"),
|
|
14
|
+
"#{name}: indent not honored"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it "raises the named AdapterError on engines that ignore the knob" do
|
|
19
|
+
%i[oga ox headed_ox libxml leptris].each do |name|
|
|
20
|
+
doc = Moxml.new(name).parse("<r><a/></r>")
|
|
21
|
+
expect { doc.root.to_xml(indent: 2) }
|
|
22
|
+
.to raise_error(Moxml::AdapterError, /indentation/),
|
|
23
|
+
"#{name}: silent unindented answer"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "keeps indent: 0 and the no-option path common on every adapter" do
|
|
28
|
+
Moxml::Adapter::AVAILABLE_ADAPTERS.each do |name|
|
|
29
|
+
doc = Moxml.new(name).parse("<r><a/></r>")
|
|
30
|
+
expect(doc.root.to_xml(indent: 0)).to include("<a")
|
|
31
|
+
expect(doc.root.to_xml).to include("<a")
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
data/spec/moxml/node_spec.rb
CHANGED
|
@@ -156,9 +156,6 @@ RSpec.describe Moxml::Node do
|
|
|
156
156
|
end
|
|
157
157
|
|
|
158
158
|
it "locates the node via its path" do
|
|
159
|
-
# Ox translates only a limited XPath subset (no positional predicates)
|
|
160
|
-
skip "D-12: ox xpath translation does not support positional predicates" if adapter_name == :ox
|
|
161
|
-
|
|
162
159
|
expect(doc.at_xpath(title.path)).to eq(title)
|
|
163
160
|
end
|
|
164
161
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "tempfile"
|
|
5
|
+
|
|
6
|
+
# RELAX NG facade (TODO.sota/05): engines with an RNG validator
|
|
7
|
+
# answer valid?/validate/validate_errors; engines without one raise
|
|
8
|
+
# the named NotImplementedError at compile and answer supported?
|
|
9
|
+
# without raising.
|
|
10
|
+
RSpec.describe Moxml::RelaxNG do
|
|
11
|
+
RNG_SCHEMA = <<~RNG # rubocop:disable Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration
|
|
12
|
+
<element name="r" xmlns="http://relaxng.org/ns/structure/1.0">
|
|
13
|
+
<attribute name="a"><text/></attribute>
|
|
14
|
+
</element>
|
|
15
|
+
RNG
|
|
16
|
+
|
|
17
|
+
let(:valid_doc) { Moxml.new(:leptris).parse('<r a="x"/>') }
|
|
18
|
+
let(:invalid_doc) { Moxml.new(:leptris).parse("<r/>") }
|
|
19
|
+
|
|
20
|
+
it "answers supported? without raising on every adapter" do
|
|
21
|
+
Moxml::Adapter::AVAILABLE_ADAPTERS.each do |name|
|
|
22
|
+
ctx = Moxml.new(name)
|
|
23
|
+
expect { described_class.supported?(ctx) }.not_to raise_error
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "validates against a compiled schema and reports findings" do
|
|
28
|
+
ctx = Moxml.new(:leptris)
|
|
29
|
+
skip "leptris binding has no RNG face" unless described_class.supported?(ctx)
|
|
30
|
+
|
|
31
|
+
schema = ctx.relaxng(RNG_SCHEMA)
|
|
32
|
+
expect(schema.valid?(valid_doc)).to be(true)
|
|
33
|
+
expect(schema.validate(valid_doc)).to eq([])
|
|
34
|
+
expect(schema.valid?(invalid_doc)).to be(false)
|
|
35
|
+
error = schema.validate_errors(invalid_doc).first
|
|
36
|
+
expect(error[:message]).to include("missing required attribute")
|
|
37
|
+
expect(error[:line]).to eq(1)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it "compiles a schema from a file" do
|
|
41
|
+
ctx = Moxml.new(:leptris)
|
|
42
|
+
skip "leptris binding has no RNG face" unless described_class.supported?(ctx)
|
|
43
|
+
|
|
44
|
+
Tempfile.create(["schema", ".rng"]) do |file|
|
|
45
|
+
file.write(RNG_SCHEMA)
|
|
46
|
+
file.flush
|
|
47
|
+
schema = ctx.relaxng_file(file.path)
|
|
48
|
+
expect(schema.valid?(valid_doc)).to be(true)
|
|
49
|
+
expect(schema.valid?(invalid_doc)).to be(false)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it "raises a named NotImplementedError on engines without an RNG validator" do
|
|
54
|
+
ctx = Moxml.new(:nokogiri)
|
|
55
|
+
expect(described_class.supported?(ctx)).to be(false)
|
|
56
|
+
expect { ctx.relaxng(RNG_SCHEMA) }
|
|
57
|
+
.to raise_error(Moxml::NotImplementedError, /RELAX NG/)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it "names a malformed schema as a ValidationError" do
|
|
61
|
+
ctx = Moxml.new(:leptris)
|
|
62
|
+
skip "leptris binding has no RNG face" unless described_class.supported?(ctx)
|
|
63
|
+
|
|
64
|
+
expect { ctx.relaxng("<not-a-schema/>") }
|
|
65
|
+
.to raise_error(Moxml::ValidationError, /RELAX NG/)
|
|
66
|
+
end
|
|
67
|
+
end
|
data/spec/moxml/sax_spec.rb
CHANGED
|
@@ -412,50 +412,39 @@ RSpec.describe Moxml::SAX do
|
|
|
412
412
|
expect(handler.error_message).to match(/invalid|unclosed|premature|mismatch|not closed/i)
|
|
413
413
|
end
|
|
414
414
|
|
|
415
|
-
it "
|
|
416
|
-
# Ox SAX
|
|
417
|
-
#
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
context.sax_parse(xml_with_cdata) do
|
|
424
|
-
cdata { |_text| cdata_found = true }
|
|
425
|
-
end
|
|
426
|
-
|
|
427
|
-
# This will fail because Ox doesn't support separate CDATA events
|
|
428
|
-
expect(cdata_found).to be true
|
|
415
|
+
it "declares CDATA event subscriptions loudly (D-10)" do
|
|
416
|
+
# Ox SAX has no separate CDATA events; a subscriber gets the
|
|
417
|
+
# named error rather than an incomplete stream.
|
|
418
|
+
expect do
|
|
419
|
+
context.sax_parse("<root><![CDATA[special content]]></root>") do
|
|
420
|
+
cdata { |_text| nil }
|
|
421
|
+
end
|
|
422
|
+
end.to raise_error(Moxml::NotImplementedError, /cdata/)
|
|
429
423
|
end
|
|
430
424
|
|
|
431
|
-
it "
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
comment_found = false
|
|
438
|
-
context.sax_parse(xml_with_comment) do
|
|
439
|
-
comment { |_text| comment_found = true }
|
|
440
|
-
end
|
|
441
|
-
|
|
442
|
-
# This will fail because Ox doesn't support comment events
|
|
443
|
-
expect(comment_found).to be true
|
|
425
|
+
it "declares comment event subscriptions loudly (D-10)" do
|
|
426
|
+
expect do
|
|
427
|
+
context.sax_parse("<root><!-- comment --><data>content</data></root>") do
|
|
428
|
+
comment { |_text| nil }
|
|
429
|
+
end
|
|
430
|
+
end.to raise_error(Moxml::NotImplementedError, /comment/)
|
|
444
431
|
end
|
|
445
432
|
|
|
446
|
-
it "
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
433
|
+
it "declares PI event subscriptions loudly (D-10)" do
|
|
434
|
+
expect do
|
|
435
|
+
context.sax_parse('<?xml-stylesheet type="text/xsl" href="style.xsl"?><root/>') do
|
|
436
|
+
processing_instruction { |_target, _data| nil }
|
|
437
|
+
end
|
|
438
|
+
end.to raise_error(Moxml::NotImplementedError, /processing_instruction/)
|
|
439
|
+
end
|
|
451
440
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
end
|
|
441
|
+
it "declares unsupported families loudly for class handlers too" do
|
|
442
|
+
handler = Class.new(Moxml::SAX::Handler) do
|
|
443
|
+
def on_cdata(_text); end
|
|
444
|
+
end.new
|
|
456
445
|
|
|
457
|
-
|
|
458
|
-
|
|
446
|
+
expect { context.sax_parse("<root/>", handler) }
|
|
447
|
+
.to raise_error(Moxml::NotImplementedError, /cdata/)
|
|
459
448
|
end
|
|
460
449
|
end
|
|
461
450
|
|
|
@@ -1024,46 +1013,28 @@ RSpec.describe Moxml::SAX do
|
|
|
1024
1013
|
expect(handler.error_message).to match(/invalid|unclosed|premature|mismatch|not closed/i)
|
|
1025
1014
|
end
|
|
1026
1015
|
|
|
1027
|
-
it "
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
cdata_found = false
|
|
1034
|
-
context.sax_parse(xml_with_cdata) do
|
|
1035
|
-
cdata { |_text| cdata_found = true }
|
|
1036
|
-
end
|
|
1037
|
-
|
|
1038
|
-
expect(cdata_found).to be true
|
|
1016
|
+
it "declares CDATA event subscriptions loudly (inherited from Ox, D-10)" do
|
|
1017
|
+
expect do
|
|
1018
|
+
context.sax_parse("<root><![CDATA[special content]]></root>") do
|
|
1019
|
+
cdata { |_text| nil }
|
|
1020
|
+
end
|
|
1021
|
+
end.to raise_error(Moxml::NotImplementedError, /cdata/)
|
|
1039
1022
|
end
|
|
1040
1023
|
|
|
1041
|
-
it "
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
comment_found = false
|
|
1048
|
-
context.sax_parse(xml_with_comment) do
|
|
1049
|
-
comment { |_text| comment_found = true }
|
|
1050
|
-
end
|
|
1051
|
-
|
|
1052
|
-
expect(comment_found).to be true
|
|
1024
|
+
it "declares comment event subscriptions loudly (inherited from Ox, D-10)" do
|
|
1025
|
+
expect do
|
|
1026
|
+
context.sax_parse("<root><!-- comment --><data>content</data></root>") do
|
|
1027
|
+
comment { |_text| nil }
|
|
1028
|
+
end
|
|
1029
|
+
end.to raise_error(Moxml::NotImplementedError, /comment/)
|
|
1053
1030
|
end
|
|
1054
1031
|
|
|
1055
|
-
it "
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
pi_found = false
|
|
1062
|
-
context.sax_parse(xml_with_pi) do
|
|
1063
|
-
processing_instruction { |_target, _data| pi_found = true }
|
|
1064
|
-
end
|
|
1065
|
-
|
|
1066
|
-
expect(pi_found).to be true
|
|
1032
|
+
it "declares PI event subscriptions loudly (inherited from Ox, D-10)" do
|
|
1033
|
+
expect do
|
|
1034
|
+
context.sax_parse('<?xml-stylesheet type="text/xsl" href="style.xsl"?><root/>') do
|
|
1035
|
+
processing_instruction { |_target, _data| nil }
|
|
1036
|
+
end
|
|
1037
|
+
end.to raise_error(Moxml::NotImplementedError, /processing_instruction/)
|
|
1067
1038
|
end
|
|
1068
1039
|
end
|
|
1069
1040
|
end
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|