moxml 0.5.145 → 0.5.147
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 +129 -2
- data/lib/moxml/adapter/libxml.rb +5 -0
- data/lib/moxml/adapter/oga.rb +10 -0
- data/lib/moxml/adapter/rexml.rb +5 -0
- data/lib/moxml/document.rb +9 -0
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xpath/compiler.rb +14 -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 +32 -1
- data/spec/moxml/adapter/leptris_contract_reads_spec.rb +133 -0
- data/spec/moxml/adapter/leptris_detached_text_spec.rb +34 -0
- data/spec/moxml/adapter/leptris_parse_lint_spec.rb +35 -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 -1
data/lib/moxml/document.rb
CHANGED
|
@@ -73,6 +73,15 @@ module Moxml
|
|
|
73
73
|
root_element ? Moxml::Node.wrap(root_element, context) : nil
|
|
74
74
|
end
|
|
75
75
|
|
|
76
|
+
# Documents are container nodes: #text concatenates descendant
|
|
77
|
+
# text, Nokogiri's Document#text contract (issue #400) — the base
|
|
78
|
+
# Node#text answered "" and silently emptied consumers'
|
|
79
|
+
# content-preservation checks.
|
|
80
|
+
def text
|
|
81
|
+
val = adapter.text_content(@native)
|
|
82
|
+
entity_bearing? ? adapter.restore_entities(val) : val
|
|
83
|
+
end
|
|
84
|
+
|
|
76
85
|
# Materialize the root subtree — see Moxml::Materializer.
|
|
77
86
|
def materialize(&block)
|
|
78
87
|
return to_enum(:materialize) unless block
|
data/lib/moxml/version.rb
CHANGED
data/lib/moxml/xpath/compiler.rb
CHANGED
|
@@ -40,6 +40,7 @@ module Moxml
|
|
|
40
40
|
# @param namespaces [Hash, nil] Optional namespace prefix mappings
|
|
41
41
|
# @return [Proc] Compiled Proc that accepts a document
|
|
42
42
|
def self.compile_with_cache(ast, namespaces: nil)
|
|
43
|
+
namespaces = normalize_namespaces(namespaces)
|
|
43
44
|
cache_key = namespaces ? [ast, namespaces] : ast
|
|
44
45
|
CACHE.get_or_set(cache_key) { new(namespaces: namespaces).compile(ast) }
|
|
45
46
|
end
|
|
@@ -48,12 +49,24 @@ module Moxml
|
|
|
48
49
|
#
|
|
49
50
|
# @param namespaces [Hash, nil] Optional namespace prefix mappings
|
|
50
51
|
def initialize(namespaces: nil)
|
|
51
|
-
@namespaces = namespaces
|
|
52
|
+
@namespaces = self.class.normalize_namespaces(namespaces)
|
|
52
53
|
@literal_id = 0
|
|
53
54
|
@predicate_nodesets = []
|
|
54
55
|
@predicate_indexes = []
|
|
55
56
|
end
|
|
56
57
|
|
|
58
|
+
# Prefix lookups are string-keyed (the parser yields prefix
|
|
59
|
+
# strings); symbol-keyed bindings — the common Nokogiri spelling
|
|
60
|
+
# (`xpath("//cml:molecule", cml: URI)`) — must resolve too, or
|
|
61
|
+
# the miss falls through to prefix-spelling matching and
|
|
62
|
+
# namespaced elements never match (issue #395).
|
|
63
|
+
def self.normalize_namespaces(namespaces)
|
|
64
|
+
return nil if namespaces.nil?
|
|
65
|
+
return namespaces unless namespaces.is_a?(Hash)
|
|
66
|
+
|
|
67
|
+
namespaces.transform_keys(&:to_s)
|
|
68
|
+
end
|
|
69
|
+
|
|
57
70
|
# Compiles an XPath AST into a Ruby Proc
|
|
58
71
|
#
|
|
59
72
|
# @param ast [AST::Node] XPath AST to compile
|
|
@@ -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
|
|
@@ -11,11 +11,22 @@ require "spec_helper"
|
|
|
11
11
|
# - Attribute-terminal steps work in every path position
|
|
12
12
|
# (leptris-ruby#458's table: //a/@id, //@id, r/a/@id,
|
|
13
13
|
# //*[f()]/@id — fixed by the engine's VM path work).
|
|
14
|
+
CML_NS = "http://www.xml-cml.org/schema"
|
|
15
|
+
ACI_NS = "http://www.xml-cml.org/schema/asciichem"
|
|
16
|
+
|
|
14
17
|
RSpec.describe "XPath idioms across adapters" do
|
|
15
18
|
let(:default_ns_xml) do
|
|
16
19
|
'<svgmap xmlns="http://www.example.org/doc"><target href="a"/></svgmap>'
|
|
17
20
|
end
|
|
18
21
|
let(:plain_xml) { '<r><a id="x"><b/></a></r>' }
|
|
22
|
+
let(:cml_xml) do
|
|
23
|
+
<<~XML
|
|
24
|
+
<molecule xmlns="#{CML_NS}" xmlns:aci="#{ACI_NS}" id="m1">
|
|
25
|
+
<atomArray><atom id="a1"/></atomArray>
|
|
26
|
+
<aci:group multiplicity="2" bracket="paren" atomRefs="a1"/>
|
|
27
|
+
</molecule>
|
|
28
|
+
XML
|
|
29
|
+
end
|
|
19
30
|
|
|
20
31
|
Moxml::Adapter::AVAILABLE_ADAPTERS.each do |adapter_name|
|
|
21
32
|
next if adapter_name == :headed_ox # same engine as :ox
|
|
@@ -31,7 +42,7 @@ RSpec.describe "XPath idioms across adapters" do
|
|
|
31
42
|
if adapter_name == :oga
|
|
32
43
|
# oga's own Ruby XPath engine has no xmlns: default-ns
|
|
33
44
|
# support (third-party; explicit bindings work).
|
|
34
|
-
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"
|
|
35
46
|
end
|
|
36
47
|
end
|
|
37
48
|
|
|
@@ -56,6 +67,26 @@ RSpec.describe "XPath idioms across adapters" do
|
|
|
56
67
|
expect(doc.xpath("//*[local-name()='a']/@id").first&.value).to eq("x")
|
|
57
68
|
end
|
|
58
69
|
|
|
70
|
+
# Issue #395: bindings hashes arrive in both key spellings
|
|
71
|
+
# (symbols are the common Nokogiri idiom); the engines must
|
|
72
|
+
# resolve both — a symbol-key miss falls through to
|
|
73
|
+
# prefix-spelling matching and namespaced elements never match.
|
|
74
|
+
# asciichem's CML group extraction rides exactly this shape.
|
|
75
|
+
it "resolves symbol-keyed namespace bindings" do
|
|
76
|
+
doc = ctx.parse(cml_xml)
|
|
77
|
+
mols = doc.xpath("//cml:molecule", cml: CML_NS)
|
|
78
|
+
expect(mols.to_a.size).to eq(1)
|
|
79
|
+
|
|
80
|
+
groups = mols.first.xpath("./aci:group", aci: ACI_NS)
|
|
81
|
+
expect(groups.to_a.size).to eq(1)
|
|
82
|
+
expect(groups.first["multiplicity"]).to eq("2")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it "resolves string-keyed namespace bindings identically" do
|
|
86
|
+
doc = ctx.parse(cml_xml)
|
|
87
|
+
expect(doc.xpath("//cml:molecule", "cml" => CML_NS).to_a.size).to eq(1)
|
|
88
|
+
end
|
|
89
|
+
|
|
59
90
|
# moxml#393: a relative name-test path on the DOCUMENT resolves
|
|
60
91
|
# laxly, descendant-anchored at the root — Nokogiri's own
|
|
61
92
|
# Document#at/#search semantics, now pinned for every adapter.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# leptris-ruby#460 — the Identity contract reads ride the C faces
|
|
6
|
+
# by plain dispatch (NativeNode subclasses inherit the C-defined
|
|
7
|
+
# name/content since binding 1.9.359). The wrapper memo stays; the
|
|
8
|
+
# ADAPTER CROSSING (bind_call through node_name/text_content) goes.
|
|
9
|
+
RSpec.describe "leptris Identity contract reads" do
|
|
10
|
+
let(:ctx) { Moxml.new(:leptris) }
|
|
11
|
+
|
|
12
|
+
before do
|
|
13
|
+
unless Moxml::Adapter::Leptris::NATIVE_READ_LAYER &&
|
|
14
|
+
Leptris::XML::NativeNode.method_defined?(:name) &&
|
|
15
|
+
Leptris::XML::NativeNode.method_defined?(:content)
|
|
16
|
+
skip "binding #{defined?(Leptris) && Leptris::VERSION} lacks the C contract faces"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def bust(node, *ivars)
|
|
21
|
+
ivars.each do |iv|
|
|
22
|
+
node.remove_instance_variable(iv) if node.instance_variable_defined?(iv)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def forbid_crossing!(method)
|
|
27
|
+
singleton = Moxml::Adapter::Leptris.singleton_class
|
|
28
|
+
singleton.send(:alias_method, :"#{method}_without_460", method)
|
|
29
|
+
singleton.send(:define_method, method) { |*| raise "adapter #{method} crossing" }
|
|
30
|
+
method
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def restore_crossing!(method)
|
|
34
|
+
singleton = Moxml::Adapter::Leptris.singleton_class
|
|
35
|
+
singleton.send(:alias_method, method, :"#{method}_without_460")
|
|
36
|
+
singleton.send(:remove_method, :"#{method}_without_460")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
describe "element#name" do
|
|
40
|
+
it "answers without the adapter node_name crossing" do
|
|
41
|
+
unless Leptris::XML::NativeNode.method_defined?(:name_read)
|
|
42
|
+
skip "binding #{Leptris::VERSION} has no name_read face (adapter path is correct there)"
|
|
43
|
+
end
|
|
44
|
+
doc = ctx.parse('<root><item id="1">v</item></root>')
|
|
45
|
+
el = doc.root.children.first
|
|
46
|
+
expect(el.native).to be_a(Leptris::XML::NativeNode)
|
|
47
|
+
|
|
48
|
+
bust(el, :@name)
|
|
49
|
+
forbid_crossing!(:node_name)
|
|
50
|
+
begin
|
|
51
|
+
expect(el.name).to eq("item")
|
|
52
|
+
ensure
|
|
53
|
+
restore_crossing!(:node_name)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it "answers the local name for prefixed elements (cross-adapter contract)" do
|
|
58
|
+
doc = ctx.parse('<root xmlns:ns="urn:u"><ns:p/></root>')
|
|
59
|
+
el = doc.root.children.first
|
|
60
|
+
bust(el, :@name)
|
|
61
|
+
expect(el.name).to eq("p")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it "memoizes: warm reads return the same String" do
|
|
65
|
+
doc = ctx.parse("<root><item/></root>")
|
|
66
|
+
el = doc.root.children.first
|
|
67
|
+
first = el.name
|
|
68
|
+
expect(el.name).to equal(first)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it "re-reads after name=" do
|
|
72
|
+
doc = ctx.parse("<root><item/></root>")
|
|
73
|
+
el = doc.root.children.first
|
|
74
|
+
el.name = "renamed"
|
|
75
|
+
expect(el.name).to eq("renamed")
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
describe "text#content" do
|
|
80
|
+
it "answers without the adapter text_content crossing" do
|
|
81
|
+
unless Leptris::XML::NativeNode.method_defined?(:text_read)
|
|
82
|
+
skip "binding #{Leptris::VERSION} has no text_read face (adapter path is correct there)"
|
|
83
|
+
end
|
|
84
|
+
doc = ctx.parse("<root><item>v</item></root>")
|
|
85
|
+
tx = doc.root.children.first.children.first
|
|
86
|
+
expect(tx.native).to be_a(Leptris::XML::NativeNode)
|
|
87
|
+
|
|
88
|
+
bust(tx, :@content)
|
|
89
|
+
forbid_crossing!(:text_content)
|
|
90
|
+
begin
|
|
91
|
+
expect(tx.content).to eq("v")
|
|
92
|
+
ensure
|
|
93
|
+
restore_crossing!(:text_content)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it "memoizes: warm reads return the same String" do
|
|
98
|
+
doc = ctx.parse("<root><item>v</item></root>")
|
|
99
|
+
tx = doc.root.children.first.children.first
|
|
100
|
+
first = tx.content
|
|
101
|
+
expect(tx.content).to equal(first)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it "re-reads after content=" do
|
|
105
|
+
doc = ctx.parse("<root><item>v</item></root>")
|
|
106
|
+
tx = doc.root.children.first.children.first
|
|
107
|
+
tx.content = "changed"
|
|
108
|
+
expect(tx.content).to eq("changed")
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it "restores entities on entity-bearing documents" do
|
|
112
|
+
ctx2 = Moxml.new(:leptris) { |c| c.restore_entities = true }
|
|
113
|
+
doc = ctx2.parse("<root><item>a & b</item></root>")
|
|
114
|
+
tx = doc.root.children.first.children.first
|
|
115
|
+
bust(tx, :@content)
|
|
116
|
+
expect(tx.content).to eq("a & b")
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
describe "the untouched faces" do
|
|
121
|
+
it "keeps text wrappers named text" do
|
|
122
|
+
doc = ctx.parse("<root><item>v</item></root>")
|
|
123
|
+
tx = doc.root.children.first.children.first
|
|
124
|
+
expect(tx.name).to eq("text")
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it "keeps element#text on the existing path" do
|
|
128
|
+
doc = ctx.parse("<root><item>v</item></root>")
|
|
129
|
+
el = doc.root.children.first
|
|
130
|
+
expect(el.text).to eq("v")
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
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 "raises a named Moxml error for a removed text node" 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
|
+
expect { text.content = "z" }
|
|
19
|
+
.to raise_error(Moxml::Error, /detached \(parentless\).*issue #401/)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it "keeps created-unattached nodes mutable" do
|
|
23
|
+
text = ctx.create_document.create_text("x")
|
|
24
|
+
text.content = "y"
|
|
25
|
+
expect(text.content).to eq("y")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "keeps attached nodes mutable" do
|
|
29
|
+
doc = ctx.parse("<r><a>x</a></r>")
|
|
30
|
+
text = doc.root.children[0].children[0]
|
|
31
|
+
text.content = "z"
|
|
32
|
+
expect(text.content).to eq("z")
|
|
33
|
+
end
|
|
34
|
+
end
|