moxml 0.5.146 → 0.5.148
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/rake.yml +35 -0
- data/README.adoc +19 -1
- data/docs/_pages/adapter-compatibility.adoc +7 -0
- data/docs/_pages/divergences.adoc +90 -0
- data/docs/_references/index.adoc +9 -0
- data/docs/_references/processing-model.adoc +211 -0
- data/docs/index.adoc +9 -0
- data/lib/moxml/adapter/leptris.rb +100 -3
- data/lib/moxml/adapter/libxml.rb +5 -0
- data/lib/moxml/adapter/oga.rb +10 -0
- data/lib/moxml/document.rb +27 -4
- data/lib/moxml/node.rb +12 -0
- data/lib/moxml/version.rb +1 -1
- data/spec/consistency/divergence_register_spec.rb +109 -0
- data/spec/consistency/document_text_spec.rb +30 -0
- data/spec/consistency/eol_normalization_spec.rb +8 -8
- data/spec/consistency/loud_degradation_spec.rb +44 -0
- data/spec/consistency/model_coverage_audit_spec.rb +113 -0
- data/spec/consistency/spec_constant_uniqueness_spec.rb +29 -0
- data/spec/consistency/xpath_idioms_spec.rb +1 -1
- data/spec/moxml/adapter/leptris_contract_reads_spec.rb +29 -0
- data/spec/moxml/adapter/leptris_detached_text_spec.rb +41 -0
- data/spec/moxml/adapter/leptris_parse_lint_spec.rb +47 -0
- data/spec/moxml/adapter/shared_examples/adapter_contract.rb +3 -3
- data/spec/moxml/c14n/adapter_delegation_spec.rb +3 -3
- data/spec/moxml/c14n/comments_pis_spec.rb +2 -2
- data/spec/moxml/c14n/subset_spec.rb +4 -4
- data/spec/moxml/c14n/w3c_examples_spec.rb +3 -1
- data/spec/moxml/declaration_preservation_spec.rb +2 -2
- data/spec/moxml/element_add_element_spec.rb +3 -3
- data/spec/moxml/element_attribute_pairs_spec.rb +2 -2
- data/spec/moxml/node_spec.rb +1 -1
- data/spec/moxml/plan_conformance_spec.rb +3 -3
- data/spec/moxml/sax_entity_parity_spec.rb +3 -3
- data/spec/moxml/sax_spec.rb +7 -7
- data/spec/moxml/struct_plan_spec.rb +2 -2
- data/spec/moxml/xpath_capabilities_spec.rb +13 -13
- metadata +11 -2
data/lib/moxml/node.rb
CHANGED
|
@@ -847,6 +847,18 @@ module Moxml
|
|
|
847
847
|
def self.wrap_with(node, context, adapter)
|
|
848
848
|
return nil if node.nil?
|
|
849
849
|
|
|
850
|
+
# Identity natives ARE the canonical wrapper (leptris C-mint):
|
|
851
|
+
# @native is self, so canonicalization, the cache probes and
|
|
852
|
+
# the re-mint below all answer the receiver. One native ivar
|
|
853
|
+
# read decides; shells (@native != self) take the full path.
|
|
854
|
+
# The C mint leaves the node un-primed (@context unset) — prime
|
|
855
|
+
# on first wrap. Conditionally: an already-primed identity (a
|
|
856
|
+
# walked node) must not lose @parent_node to a re-prime.
|
|
857
|
+
if node.is_a?(Node) && node.instance_variable_get(:@native).equal?(node)
|
|
858
|
+
node.prime_contract(node, context, adapter) unless node.instance_variable_defined?(:@context)
|
|
859
|
+
return node
|
|
860
|
+
end
|
|
861
|
+
|
|
850
862
|
# One C node may have several Ruby representations (leptris
|
|
851
863
|
# native/binding duality, moxml#311); every wrap resolves to
|
|
852
864
|
# the canonical one so memo holders share one invalidation
|
data/lib/moxml/version.rb
CHANGED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# Divergence register linkage (TODO.model/02): the register
|
|
6
|
+
# (docs/_pages/divergences.adoc) and the consistency suite must stay
|
|
7
|
+
# mechanically consistent — every capability-related skip cites a
|
|
8
|
+
# register ID, every register entry is cited by at least one spec, and
|
|
9
|
+
# no spec cites an unregistered ID.
|
|
10
|
+
RSpec.describe "divergence register linkage" do
|
|
11
|
+
REGISTER_PATH = "docs/_pages/divergences.adoc" # rubocop:disable Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration
|
|
12
|
+
SCAN_GLOBS = %w[spec/consistency/**/*.rb spec/moxml/**/*.rb].freeze # rubocop:disable Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration
|
|
13
|
+
NEXT_GATE = [ # rubocop:disable Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration
|
|
14
|
+
/same engine as/, # engine dedup, not a divergence
|
|
15
|
+
/source_adapter == target_adapter/, # self round-trip
|
|
16
|
+
/MAX_SIZE/, # size guard
|
|
17
|
+
/AVAILABLE_ADAPTERS\.include/, # availability filter
|
|
18
|
+
/skip_shapes/, # shape routing (IDs cited above)
|
|
19
|
+
/leptris_fixed\?/, # version gate
|
|
20
|
+
/source_elements/, # data-presence guard
|
|
21
|
+
/next unless match/, # scanner-internal guard
|
|
22
|
+
].freeze
|
|
23
|
+
|
|
24
|
+
# Version/availability gates are not divergences: a binding too old
|
|
25
|
+
# or an adapter not installed is a deployment fact, not an
|
|
26
|
+
# engine-vs-model difference at equal versions.
|
|
27
|
+
GATE = [ # rubocop:disable Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration
|
|
28
|
+
/unavailable/i, /not available/, /predates/, /not installed/i,
|
|
29
|
+
/not present/i, /requires/i, /required\z/, /has no \S+ face/,
|
|
30
|
+
/lacks the /, /carries /, /degraded below/, /lockstep/i,
|
|
31
|
+
/unreleased/i, /stood down/, /recovered without/,
|
|
32
|
+
/Only \S+ available in CI/i, /adapter-specific/i, /noblanks/,
|
|
33
|
+
/SKIP_ parse flags/, /engine < \d/
|
|
34
|
+
].freeze
|
|
35
|
+
|
|
36
|
+
def register_ids
|
|
37
|
+
File.readlines(REGISTER_PATH)
|
|
38
|
+
.filter_map { |line| line[/\A\|\s*(D-\d+)\s*$/, 1] }
|
|
39
|
+
.uniq
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def spec_files
|
|
43
|
+
SCAN_GLOBS.flat_map { |g| Dir[g] }.uniq
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def cited_ids
|
|
47
|
+
spec_files.flat_map { |path| File.readlines(path) }
|
|
48
|
+
.flat_map { |line| line.scan(/D-\d+/) }
|
|
49
|
+
.uniq
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it "has a register with entries" do
|
|
53
|
+
expect(register_ids).not_to be_empty
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "requires capability-related skips to cite a register ID" do
|
|
57
|
+
offenders = []
|
|
58
|
+
spec_files.each do |path|
|
|
59
|
+
File.readlines(path).each_with_index do |line, i|
|
|
60
|
+
next unless (msg = line[/skip\s+[&(]?\s*["'](.*?)["']/, 1])
|
|
61
|
+
|
|
62
|
+
next if GATE.any? { |re| re.match?(msg) }
|
|
63
|
+
next if msg.match?(/D-\d+/)
|
|
64
|
+
|
|
65
|
+
offenders << "#{path}:#{i + 1} #{msg.strip[0, 60]}"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
expect(offenders).to be_empty,
|
|
69
|
+
"capability skips must cite a divergence-register " \
|
|
70
|
+
"ID (D-NN): #{offenders.join('; ')}"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it "requires adapter-exempting next-guards to cite a nearby register ID" do
|
|
74
|
+
offenders = []
|
|
75
|
+
spec_files.each do |path|
|
|
76
|
+
next if path.end_with?("divergence_register_spec.rb")
|
|
77
|
+
|
|
78
|
+
lines = File.readlines(path)
|
|
79
|
+
lines.each_with_index do |line, i|
|
|
80
|
+
next unless line.match?(/^\s*next\s+(if|unless)\s/)
|
|
81
|
+
next if NEXT_GATE.any? { |re| re.match?(line) }
|
|
82
|
+
next if line.match?(/D-\d+/)
|
|
83
|
+
|
|
84
|
+
context = lines[[i - 4, 0].max...i] + lines[(i + 1)..(i + 4)].to_a
|
|
85
|
+
next if context.any? { |l| l.match?(/D-\d+/) }
|
|
86
|
+
|
|
87
|
+
offenders << "#{path}:#{i + 1} #{line.strip[0, 60]}"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
expect(offenders).to be_empty,
|
|
91
|
+
"adapter-exempting next-guards must cite a " \
|
|
92
|
+
"divergence-register ID (D-NN) on or above the " \
|
|
93
|
+
"line: #{offenders.join('; ')}"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it "cites every register entry from at least one spec" do
|
|
97
|
+
missing = register_ids - cited_ids
|
|
98
|
+
expect(missing).to be_empty,
|
|
99
|
+
"register entries never cited by a spec: " \
|
|
100
|
+
"#{missing.join(', ')}"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it "never cites an unregistered divergence ID" do
|
|
104
|
+
orphans = cited_ids - register_ids
|
|
105
|
+
expect(orphans).to be_empty,
|
|
106
|
+
"specs cite IDs missing from the register: " \
|
|
107
|
+
"#{orphans.join(', ')}"
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# Issue #400: documents are container nodes — Document#text
|
|
6
|
+
# concatenates descendant text (Nokogiri's Document#text contract).
|
|
7
|
+
# The base Node#text answered "" and silently emptied consumers'
|
|
8
|
+
# content-preservation checks (canon's formatter asserted through
|
|
9
|
+
# doc.text).
|
|
10
|
+
RSpec.describe "Document#text concatenates descendant text" do
|
|
11
|
+
Moxml::Adapter::AVAILABLE_ADAPTERS.each do |adapter_name|
|
|
12
|
+
context "with #{adapter_name}" do
|
|
13
|
+
let(:ctx) { Moxml.new(adapter_name) }
|
|
14
|
+
|
|
15
|
+
it "answers the concatenated descendant text" do
|
|
16
|
+
doc = ctx.parse("<r><a>x</a><b>y</b></r>")
|
|
17
|
+
expect(doc.text).to eq("xy")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it "matches root.text (both containers recurse)" do
|
|
21
|
+
doc = ctx.parse("<r><a>x</a><b>y</b></r>")
|
|
22
|
+
expect(doc.text).to eq(doc.root.text)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "answers an empty string for element-free documents" do
|
|
26
|
+
expect(ctx.parse("<r/>").text).to eq("")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -20,7 +20,7 @@ require "spec_helper"
|
|
|
20
20
|
# binding release vending that engine rides
|
|
21
21
|
# (gate: binding 1.9.240).
|
|
22
22
|
RSpec.describe "XML line-ending normalization" do
|
|
23
|
-
|
|
23
|
+
EOL_NORMALIZATION_ADAPTERS = %i[leptris nokogiri ox oga rexml libxml].freeze # rubocop:disable Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration
|
|
24
24
|
|
|
25
25
|
def with_adapter(name)
|
|
26
26
|
ctx = Moxml.new(name)
|
|
@@ -37,10 +37,10 @@ RSpec.describe "XML line-ending normalization" do
|
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
it "normalizes CRLF and lone CR to LF in text content" do
|
|
40
|
-
|
|
40
|
+
EOL_NORMALIZATION_ADAPTERS.each do |name|
|
|
41
41
|
next unless leptris_fixed?(name)
|
|
42
42
|
|
|
43
|
-
skip "oga preserves raw CR (upstream)" if name == :oga
|
|
43
|
+
skip "D-02: oga preserves raw CR (upstream)" if name == :oga
|
|
44
44
|
|
|
45
45
|
with_adapter(name) do |ctx|
|
|
46
46
|
text = ctx.parse("<r>a\r\nb\rc</r>").root.children.first
|
|
@@ -50,10 +50,10 @@ RSpec.describe "XML line-ending normalization" do
|
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
it "normalizes EOL inside CDATA content" do
|
|
53
|
-
|
|
53
|
+
EOL_NORMALIZATION_ADAPTERS.each do |name|
|
|
54
54
|
next unless leptris_fixed?(name)
|
|
55
55
|
|
|
56
|
-
skip "oga preserves raw CR (upstream)" if name == :oga
|
|
56
|
+
skip "D-02: oga preserves raw CR (upstream)" if name == :oga
|
|
57
57
|
|
|
58
58
|
with_adapter(name) do |ctx|
|
|
59
59
|
cdata = ctx.parse("<r><![CDATA[a\r\nb]]></r>").root.children.first
|
|
@@ -63,11 +63,11 @@ RSpec.describe "XML line-ending normalization" do
|
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
it "preserves CR from character references" do
|
|
66
|
-
|
|
66
|
+
EOL_NORMALIZATION_ADAPTERS.each do |name|
|
|
67
67
|
next unless leptris_fixed?(name)
|
|
68
68
|
|
|
69
|
-
skip "ox collapses to LF (upstream)" if name == :ox
|
|
70
|
-
skip "oga preserves raw CR (upstream)" if name == :oga
|
|
69
|
+
skip "D-03: ox collapses to LF (upstream)" if name == :ox
|
|
70
|
+
skip "D-02: oga preserves raw CR (upstream)" if name == :oga
|
|
71
71
|
|
|
72
72
|
with_adapter(name) do |ctx|
|
|
73
73
|
text = ctx.parse("<r>a b</r>").root.children.first
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# Loud degradation (processing model, "Errors and loud degradation"):
|
|
6
|
+
# a capability an engine cannot deliver must answer the contract or
|
|
7
|
+
# raise the named error — never a divergent answer. Probed across all
|
|
8
|
+
# available adapters; the capability lists mirror the adapters'
|
|
9
|
+
# actual engine backing.
|
|
10
|
+
RSpec.describe "loud capability contracts" do
|
|
11
|
+
available = Moxml::Adapter::AVAILABLE_ADAPTERS
|
|
12
|
+
xslt_capable = %i[nokogiri leptris] & available
|
|
13
|
+
html_capable = %i[nokogiri leptris] & available
|
|
14
|
+
|
|
15
|
+
let(:sheet) do
|
|
16
|
+
%(<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">) \
|
|
17
|
+
"<xsl:template match='/'><o/></xsl:template></xsl:stylesheet>"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
(available - xslt_capable).each do |name|
|
|
21
|
+
it "raises NotImplementedError for XSLT on #{name}" do
|
|
22
|
+
ctx = Moxml.new(name)
|
|
23
|
+
expect { ctx.xslt(sheet) }.to raise_error(Moxml::NotImplementedError)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
(available - html_capable).each do |name|
|
|
28
|
+
it "raises NotImplementedError for to_xhtml on #{name}" do
|
|
29
|
+
doc = Moxml.new(name).parse("<r><a/></r>")
|
|
30
|
+
expect { doc.to_xhtml }.to raise_error(Moxml::NotImplementedError)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "raises AdapterError for to_html on #{name}" do
|
|
34
|
+
doc = Moxml.new(name).parse("<r><a/></r>")
|
|
35
|
+
expect { doc.to_html }.to raise_error(Moxml::AdapterError)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
xslt_capable.each do |name|
|
|
40
|
+
it "compiles a stylesheet on #{name}" do
|
|
41
|
+
expect(Moxml.new(name).xslt(sheet)).to be_a(Moxml::XSLT::Stylesheet)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# Model coverage matrix (TODO.model/03): every face family of the
|
|
6
|
+
# processing model is governed for every adapter — covered by a pin
|
|
7
|
+
# (the consistency/contract spec that asserts equivalence), exempt via
|
|
8
|
+
# a registered divergence (adapter => D-ID), or loud (adapter =>
|
|
9
|
+
# the named error class). An unpinned family is an ungoverned cell:
|
|
10
|
+
# this audit fails until it has one of the three.
|
|
11
|
+
# See docs/_references/processing-model.adoc for the model itself.
|
|
12
|
+
RSpec.describe "model coverage matrix" do
|
|
13
|
+
MODEL_MATRIX = { # rubocop:disable Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration
|
|
14
|
+
"document order and children listing" => {
|
|
15
|
+
pin: "spec/consistency/adapter_parity_spec.rb",
|
|
16
|
+
},
|
|
17
|
+
"name resolution" => {
|
|
18
|
+
pin: "spec/consistency/xpath_idioms_spec.rb",
|
|
19
|
+
},
|
|
20
|
+
"namespace resolution and bindings normalization" => {
|
|
21
|
+
pin: "spec/consistency/xpath_idioms_spec.rb",
|
|
22
|
+
},
|
|
23
|
+
"attribute access" => {
|
|
24
|
+
pin: "spec/moxml/attribute_resolver_spec.rb",
|
|
25
|
+
},
|
|
26
|
+
"text/content/raw_content" => {
|
|
27
|
+
pin: "spec/moxml/text_spec.rb",
|
|
28
|
+
},
|
|
29
|
+
"parsing modes and diagnostics" => {
|
|
30
|
+
pin: "spec/moxml/document_parse_diagnostics_spec.rb",
|
|
31
|
+
},
|
|
32
|
+
"EOL normalization" => {
|
|
33
|
+
pin: "spec/consistency/eol_normalization_spec.rb",
|
|
34
|
+
exempt: { oga: "D-02", ox: "D-03" },
|
|
35
|
+
},
|
|
36
|
+
"entity modes" => {
|
|
37
|
+
pin: "spec/moxml/entity_spec.rb",
|
|
38
|
+
},
|
|
39
|
+
"XPath evaluation and declared idioms" => {
|
|
40
|
+
pin: "spec/consistency/xpath_idioms_spec.rb",
|
|
41
|
+
exempt: { oga: "D-01" },
|
|
42
|
+
},
|
|
43
|
+
"serialization shapes (xml/html/xhtml)" => {
|
|
44
|
+
pin: "spec/moxml/xhtml_output_spec.rb",
|
|
45
|
+
},
|
|
46
|
+
"indentation knobs" => {
|
|
47
|
+
pin: "spec/moxml/to_html_spec.rb",
|
|
48
|
+
exempt: { oga: "D-09", ox: "D-09", headed_ox: "D-09",
|
|
49
|
+
libxml: "D-09", leptris: "D-09" },
|
|
50
|
+
},
|
|
51
|
+
"canonicalization" => {
|
|
52
|
+
pin: "spec/moxml/c14n/adapter_delegation_spec.rb",
|
|
53
|
+
exempt: { oga: "D-04", ox: "D-05", headed_ox: "D-05" },
|
|
54
|
+
},
|
|
55
|
+
"XSLT" => {
|
|
56
|
+
pin: "spec/consistency/xslt_spec.rb",
|
|
57
|
+
loud: { oga: "Moxml::NotImplementedError",
|
|
58
|
+
ox: "Moxml::NotImplementedError",
|
|
59
|
+
headed_ox: "Moxml::NotImplementedError",
|
|
60
|
+
rexml: "Moxml::NotImplementedError",
|
|
61
|
+
libxml: "Moxml::NotImplementedError" },
|
|
62
|
+
},
|
|
63
|
+
"SAX" => {
|
|
64
|
+
pin: "spec/moxml/sax_spec.rb",
|
|
65
|
+
},
|
|
66
|
+
"materialization" => {
|
|
67
|
+
pin: "spec/moxml/plan_conformance_spec.rb",
|
|
68
|
+
},
|
|
69
|
+
"loud capability contracts" => {
|
|
70
|
+
pin: "spec/consistency/loud_degradation_spec.rb",
|
|
71
|
+
},
|
|
72
|
+
}.freeze
|
|
73
|
+
|
|
74
|
+
def register_ids
|
|
75
|
+
File.readlines("docs/_pages/divergences.adoc")
|
|
76
|
+
.filter_map { |line| line[/\A\|\s*(D-\d+)\s*$/, 1] }
|
|
77
|
+
.uniq
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it "governs every face family with a pin" do
|
|
81
|
+
unpinned = MODEL_MATRIX.reject { |_, cell| cell[:pin] }.keys
|
|
82
|
+
expect(unpinned).to be_empty,
|
|
83
|
+
"ungoverned face families (no pin): " \
|
|
84
|
+
"#{unpinned.join(', ')}"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it "points every pin at an existing spec" do
|
|
88
|
+
missing = MODEL_MATRIX.filter_map do |family, cell|
|
|
89
|
+
family unless cell[:pin] && File.exist?(cell[:pin])
|
|
90
|
+
end
|
|
91
|
+
expect(missing).to be_empty, "pins missing on disk: #{missing.join(', ')}"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it "exempts adapters only via registered divergences" do
|
|
95
|
+
unknown = MODEL_MATRIX.flat_map do |family, cell|
|
|
96
|
+
(cell[:exempt] || {}).filter_map do |adapter, id|
|
|
97
|
+
"#{family}: #{adapter} -> #{id}" unless register_ids.include?(id)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
expect(unknown).to be_empty,
|
|
101
|
+
"exemptions cite unregistered IDs: #{unknown.join('; ')}"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it "declares loud cells only with named Moxml errors" do
|
|
105
|
+
bad = MODEL_MATRIX.flat_map do |family, cell|
|
|
106
|
+
(cell[:loud] || {}).filter_map do |adapter, error|
|
|
107
|
+
"#{family}: #{adapter} -> #{error}" unless error.start_with?("Moxml::")
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
expect(bad).to be_empty,
|
|
111
|
+
"loud cells must name Moxml errors: #{bad.join('; ')}"
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# Checker integrity (TODO.model/06): top-level constant names in spec
|
|
6
|
+
# files all resolve through Object inside example blocks, so the same
|
|
7
|
+
# name defined by two files makes the last-loaded file win — the 0.5.143
|
|
8
|
+
# CORPUS collision made the battery fail ~50% of runs with a foreign
|
|
9
|
+
# corpus. This guard kills the class.
|
|
10
|
+
RSpec.describe "spec-file top-level constant uniqueness" do
|
|
11
|
+
it "defines each constant name in at most one spec/support file" do
|
|
12
|
+
seen = {}
|
|
13
|
+
|
|
14
|
+
Dir["spec/**/*.rb"].each do |path|
|
|
15
|
+
File.readlines(path).each_with_index do |line, i|
|
|
16
|
+
match = line.match(/\A\s*([A-Z][A-Z0-9_]*)\s*=[^=]/)
|
|
17
|
+
next unless match
|
|
18
|
+
|
|
19
|
+
name = match[1]
|
|
20
|
+
expect(seen)
|
|
21
|
+
.not_to have_key(name),
|
|
22
|
+
"#{name} defined at #{seen[name]} and #{path}:#{i + 1} " \
|
|
23
|
+
"— constants inside example blocks collide through " \
|
|
24
|
+
"Object; give one file a unique name"
|
|
25
|
+
seen[name] = "#{path}:#{i + 1}"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -42,7 +42,7 @@ RSpec.describe "XPath idioms across adapters" do
|
|
|
42
42
|
if adapter_name == :oga
|
|
43
43
|
# oga's own Ruby XPath engine has no xmlns: default-ns
|
|
44
44
|
# support (third-party; explicit bindings work).
|
|
45
|
-
skip "oga's XPath engine does not implement the xmlns: idiom"
|
|
45
|
+
skip "D-01: oga's XPath engine does not implement the xmlns: idiom"
|
|
46
46
|
end
|
|
47
47
|
end
|
|
48
48
|
|
|
@@ -130,4 +130,33 @@ RSpec.describe "leptris Identity contract reads" do
|
|
|
130
130
|
expect(el.text).to eq("v")
|
|
131
131
|
end
|
|
132
132
|
end
|
|
133
|
+
|
|
134
|
+
describe "children" do
|
|
135
|
+
it "answers without the adapter children crossing (children_read)" do
|
|
136
|
+
unless Leptris::XML::NativeNode.method_defined?(:children_read)
|
|
137
|
+
skip "binding #{Leptris::VERSION} has no children_read face"
|
|
138
|
+
end
|
|
139
|
+
doc = ctx.parse("<root><item>v</item></root>")
|
|
140
|
+
el = doc.root.children.first
|
|
141
|
+
|
|
142
|
+
singleton = Moxml::Adapter::Leptris.singleton_class
|
|
143
|
+
singleton.send(:alias_method, :children_without_walk, :children)
|
|
144
|
+
singleton.send(:define_method, :children) { |*| raise "adapter children crossing" }
|
|
145
|
+
begin
|
|
146
|
+
kids = el.children
|
|
147
|
+
expect(kids.to_a.map(&:name)).to eq(["text"])
|
|
148
|
+
ensure
|
|
149
|
+
singleton.send(:alias_method, :children, :children_without_walk)
|
|
150
|
+
singleton.send(:remove_method, :children_without_walk)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
it "returns a NodeSet carrying the contract wrappers" do
|
|
155
|
+
doc = ctx.parse("<root><item>v</item></root>")
|
|
156
|
+
kids = doc.root.children.first.children
|
|
157
|
+
expect(kids).to be_a(Moxml::NodeSet)
|
|
158
|
+
expect(kids.to_a.first.name).to eq("text")
|
|
159
|
+
expect(kids.to_a.first.content).to eq("v")
|
|
160
|
+
end
|
|
161
|
+
end
|
|
133
162
|
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# Issue #401: Text#content= on a detached (parentless) node answered
|
|
6
|
+
# the engine's bare "Invalid argument" with no indication of the cause.
|
|
7
|
+
# The leptris adapter now re-raises it as a named Moxml error carrying
|
|
8
|
+
# the detachment. Created-unattached nodes work (probe), so only the
|
|
9
|
+
# failing detachment is re-raised.
|
|
10
|
+
RSpec.describe "detached text content= names the cause" do
|
|
11
|
+
let(:ctx) { Moxml.new(:leptris) }
|
|
12
|
+
|
|
13
|
+
it "answers removed text nodes: mutable on fixed engines (1.9.374+), named error before" do
|
|
14
|
+
doc = ctx.parse("<r><a>x</a><b>y</b></r>")
|
|
15
|
+
text = doc.root.children[0].children[0]
|
|
16
|
+
text.remove
|
|
17
|
+
|
|
18
|
+
if Gem::Version.new(Leptris::VERSION) >= Gem::Version.new("1.9.374")
|
|
19
|
+
# leptris/leptris#1785 fixed the engine root cause: detached
|
|
20
|
+
# text resolves its owning document, so the mutation succeeds.
|
|
21
|
+
text.content = "z"
|
|
22
|
+
expect(text.content).to eq("z")
|
|
23
|
+
else
|
|
24
|
+
expect { text.content = "z" }
|
|
25
|
+
.to raise_error(Moxml::Error, /detached \(parentless\).*issue #401/)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "keeps created-unattached nodes mutable" do
|
|
30
|
+
text = ctx.create_document.create_text("x")
|
|
31
|
+
text.content = "y"
|
|
32
|
+
expect(text.content).to eq("y")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "keeps attached nodes mutable" do
|
|
36
|
+
doc = ctx.parse("<r><a>x</a></r>")
|
|
37
|
+
text = doc.root.children[0].children[0]
|
|
38
|
+
text.content = "z"
|
|
39
|
+
expect(text.content).to eq("z")
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# Issue #404: Context#parse_html forwards collect_errors (and mode) to
|
|
6
|
+
# the leptris flags entry (leptris#1771's §13.2.5 parse-lint) — the
|
|
7
|
+
# recorded error tokens surface through parse_diagnostics. Face-probed:
|
|
8
|
+
# bindings predating the kwarg skip.
|
|
9
|
+
RSpec.describe "parse_html collect_errors forwarding" do
|
|
10
|
+
let(:ctx) { Moxml.new(:leptris) }
|
|
11
|
+
let(:src) { %(<p class="x>unclosed</p><a href=><p>a<b</p>) }
|
|
12
|
+
|
|
13
|
+
before do
|
|
14
|
+
adapter = ctx.config.adapter
|
|
15
|
+
supported = adapter.singleton_class.const_get(:COLLECT_ERRORS_NATIVE)
|
|
16
|
+
skip "binding lacks the collect_errors kwarg (leptris#1771)" unless supported
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "surfaces parse-lint errors through parse_diagnostics" do
|
|
20
|
+
doc = ctx.parse_html(src, collect_errors: true)
|
|
21
|
+
diags = doc.parse_diagnostics
|
|
22
|
+
expect(diags).not_to be_empty
|
|
23
|
+
expect(diags.first).to have_key(:message)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Issue #408: the #1771 reader fields ride the diag hash — the
|
|
27
|
+
# token code and position canon's HtmlValidator maps onto
|
|
28
|
+
# validation errors (nil where the engine has no token code).
|
|
29
|
+
it "carries the token code and position (issue #408)" do
|
|
30
|
+
doc = ctx.parse_html(src, collect_errors: true)
|
|
31
|
+
eof = doc.parse_diagnostics.find { |d| d[:message].include?("EOF") }
|
|
32
|
+
expect(eof).not_to be_nil
|
|
33
|
+
expect(eof[:code]).to eq("eof-in-tag")
|
|
34
|
+
expect(eof[:line]).to eq(1)
|
|
35
|
+
expect(eof[:column]).to eq(1)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it "keeps the default lane silent (no option, no diagnostics)" do
|
|
39
|
+
doc = ctx.parse_html(src)
|
|
40
|
+
expect(doc.parse_diagnostics).to be_empty
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "forwards through Context#parse_html options" do
|
|
44
|
+
doc = ctx.parse_html(src, { collect_errors: true })
|
|
45
|
+
expect(doc.parse_diagnostics).not_to be_empty
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -343,13 +343,13 @@ RSpec.shared_examples "xml adapter" do
|
|
|
343
343
|
it "respects indentation settings" do
|
|
344
344
|
if described_class.name.include?("Oga") &&
|
|
345
345
|
!described_class.indents_output?
|
|
346
|
-
pending("Oga does not support indentation settings")
|
|
346
|
+
pending("D-09: Oga does not support indentation settings")
|
|
347
347
|
end
|
|
348
348
|
if described_class.name.include?("Libxml")
|
|
349
|
-
skip("LibXML serialization does not support indentation (documented limitation)")
|
|
349
|
+
skip("D-09: LibXML serialization does not support indentation (documented limitation)")
|
|
350
350
|
end
|
|
351
351
|
if described_class.name.include?("Ox")
|
|
352
|
-
skip("Ox does not support indentation settings (documented limitation)")
|
|
352
|
+
skip("D-09: Ox does not support indentation settings (documented limitation)")
|
|
353
353
|
end
|
|
354
354
|
|
|
355
355
|
unindented = described_class.serialize(doc, indent: 0)
|
|
@@ -58,9 +58,9 @@ RSpec.describe "Moxml::C14n cross-adapter canonicalization" do
|
|
|
58
58
|
|
|
59
59
|
describe "document-level nodes keep document order" do
|
|
60
60
|
# oga surfaces the XML declaration as a <?xml?> PI and keeps the
|
|
61
|
-
# separator space in PI content; ox/headed_ox drop PI
|
|
62
|
-
# parse. Both are pre-existing parse-layer
|
|
63
|
-
# scope here.
|
|
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
64
|
let(:adapters) { Moxml::Adapter::AVAILABLE_ADAPTERS - %i[oga ox headed_ox] }
|
|
65
65
|
|
|
66
66
|
it "canonicalizes a leading PI before the root element" do
|
|
@@ -53,13 +53,13 @@ RSpec.describe "C14N comments and PIs" do
|
|
|
53
53
|
# PIs, and the DataModel.from_document path drops them. Fixing requires
|
|
54
54
|
# adapter-level work and is tracked separately.
|
|
55
55
|
it "renders PIs outside the document element (KNOWN LIMITATION)" do
|
|
56
|
-
skip "document-level PI rendering not yet implemented"
|
|
56
|
+
skip "D-08: document-level PI rendering not yet implemented"
|
|
57
57
|
end
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
describe "comments at document level" do
|
|
61
61
|
it "renders document-level comments when with_comments: true (KNOWN LIMITATION)" do
|
|
62
|
-
skip "document-level comment rendering not yet implemented"
|
|
62
|
+
skip "D-08: document-level comment rendering not yet implemented"
|
|
63
63
|
end
|
|
64
64
|
end
|
|
65
65
|
end
|
|
@@ -63,7 +63,7 @@ RSpec.describe "Moxml::C14n subset canonicalization" do
|
|
|
63
63
|
next if label == "text selection" && adapter_name == :rexml
|
|
64
64
|
|
|
65
65
|
# rexml's adapter xpath does not return text nodes for
|
|
66
|
-
# text() node tests (pre-existing capability gap).
|
|
66
|
+
# text() node tests (D-07, pre-existing capability gap).
|
|
67
67
|
|
|
68
68
|
it "canonicalizes #{label}" do
|
|
69
69
|
doc = ctx.parse(xml)
|
|
@@ -72,9 +72,9 @@ RSpec.describe "Moxml::C14n subset canonicalization" do
|
|
|
72
72
|
end
|
|
73
73
|
|
|
74
74
|
# ox/headed_ox strip comment content padding at the parse
|
|
75
|
-
# layer (pre-existing, same family as their PI content
|
|
76
|
-
# only the rendered-comment case depends on it. rexml's
|
|
77
|
-
# comment() selection gap also only affects this case.
|
|
75
|
+
# 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
78
|
unless COMMENTS_PADDING_LOSS.include?(adapter_name) ||
|
|
79
79
|
COMMENT_SELECTION_GAP.include?(adapter_name)
|
|
80
80
|
it COMMENTS_CASE[0] do
|
|
@@ -55,7 +55,8 @@ RSpec.describe "Moxml::C14n W3C REC-xml-c14n examples" do
|
|
|
55
55
|
|
|
56
56
|
W3C_FULL_FORMS.each_with_index do |(label, xml, expected), idx|
|
|
57
57
|
# ox/headed_ox strip inter-element whitespace, expand 
|
|
58
|
-
# and pad comments at parse; oga keeps the PI
|
|
58
|
+
# and pad comments at parse (D-05, D-06); oga keeps the PI
|
|
59
|
+
# separator space (D-04).
|
|
59
60
|
skip_shapes = { ox: [0, CR_SHAPE, PI_SHAPE],
|
|
60
61
|
headed_ox: [0, CR_SHAPE, PI_SHAPE],
|
|
61
62
|
oga: [PI_SHAPE] }[adapter_name] || []
|
|
@@ -72,6 +73,7 @@ RSpec.describe "Moxml::C14n W3C REC-xml-c14n examples" do
|
|
|
72
73
|
|
|
73
74
|
describe "W3C §3 document-subset whitespace" do
|
|
74
75
|
Moxml::Adapter::AVAILABLE_ADAPTERS.each do |adapter_name|
|
|
76
|
+
# ox/headed_ox strip inter-element whitespace at parse (D-06)
|
|
75
77
|
next if WHITESPACE_SHAPES.include?(adapter_name)
|
|
76
78
|
|
|
77
79
|
it "renders selected nodes only, whitespace runs in place (#{adapter_name})" do
|
|
@@ -4,9 +4,9 @@ require "spec_helper"
|
|
|
4
4
|
|
|
5
5
|
RSpec.describe "XML Declaration Preservation" do
|
|
6
6
|
# Test with all available adapters
|
|
7
|
-
|
|
7
|
+
DECLARATION_SPECS_ADAPTERS = %i[nokogiri oga rexml ox libxml headed_ox].freeze
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
DECLARATION_SPECS_ADAPTERS.each do |adapter_name|
|
|
10
10
|
context "with #{adapter_name} adapter" do
|
|
11
11
|
let(:context) { Moxml.new(adapter_name) }
|
|
12
12
|
|