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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "spec_helper"
|
|
4
|
+
require "support/model_coverage"
|
|
4
5
|
|
|
5
6
|
# Model coverage matrix (TODO.model/03): every face family of the
|
|
6
7
|
# processing model is governed for every adapter — covered by a pin
|
|
@@ -8,84 +9,13 @@ require "spec_helper"
|
|
|
8
9
|
# a registered divergence (adapter => D-ID), or loud (adapter =>
|
|
9
10
|
# the named error class). An unpinned family is an ungoverned cell:
|
|
10
11
|
# this audit fails until it has one of the three.
|
|
11
|
-
#
|
|
12
|
+
#
|
|
13
|
+
# The matrix itself lives in spec/support/model_coverage.rb: the same
|
|
14
|
+
# data drives this audit, the model document's pin lists, and the
|
|
15
|
+
# published coverage page. See docs/_references/processing-model.adoc
|
|
16
|
+
# for the model itself.
|
|
12
17
|
RSpec.describe "model coverage matrix" do
|
|
13
|
-
MODEL_MATRIX =
|
|
14
|
-
"document order and children listing" => {
|
|
15
|
-
section: "Document order and traversal",
|
|
16
|
-
pin: "spec/consistency/adapter_parity_spec.rb",
|
|
17
|
-
},
|
|
18
|
-
"name resolution" => {
|
|
19
|
-
section: "Names and namespaces",
|
|
20
|
-
pin: "spec/consistency/xpath_idioms_spec.rb",
|
|
21
|
-
},
|
|
22
|
-
"namespace resolution and bindings normalization" => {
|
|
23
|
-
section: "Names and namespaces",
|
|
24
|
-
pin: "spec/consistency/xpath_idioms_spec.rb",
|
|
25
|
-
},
|
|
26
|
-
"attribute access" => {
|
|
27
|
-
section: "Access semantics",
|
|
28
|
-
pin: "spec/moxml/attribute_resolver_spec.rb",
|
|
29
|
-
},
|
|
30
|
-
"text/content/raw_content" => {
|
|
31
|
-
section: "Access semantics",
|
|
32
|
-
pin: "spec/moxml/text_spec.rb",
|
|
33
|
-
},
|
|
34
|
-
"parsing modes and diagnostics" => {
|
|
35
|
-
section: "Parsing",
|
|
36
|
-
pin: "spec/moxml/document_parse_diagnostics_spec.rb",
|
|
37
|
-
},
|
|
38
|
-
"EOL normalization" => {
|
|
39
|
-
section: "Parsing",
|
|
40
|
-
pin: "spec/consistency/eol_normalization_spec.rb",
|
|
41
|
-
exempt: { oga: "D-02", ox: "D-03" },
|
|
42
|
-
},
|
|
43
|
-
"entity modes" => {
|
|
44
|
-
section: "Parsing",
|
|
45
|
-
pin: "spec/moxml/entity_spec.rb",
|
|
46
|
-
},
|
|
47
|
-
"XPath evaluation and declared idioms" => {
|
|
48
|
-
section: "XPath evaluation",
|
|
49
|
-
pin: "spec/consistency/xpath_idioms_spec.rb",
|
|
50
|
-
exempt: { oga: "D-01" },
|
|
51
|
-
},
|
|
52
|
-
"serialization shapes (xml/html/xhtml)" => {
|
|
53
|
-
section: "Serialization",
|
|
54
|
-
pin: "spec/moxml/xhtml_output_spec.rb",
|
|
55
|
-
},
|
|
56
|
-
"indentation knobs" => {
|
|
57
|
-
section: "Serialization",
|
|
58
|
-
pin: "spec/moxml/to_html_spec.rb",
|
|
59
|
-
exempt: { oga: "D-09", ox: "D-09", headed_ox: "D-09",
|
|
60
|
-
libxml: "D-09", leptris: "D-09" },
|
|
61
|
-
},
|
|
62
|
-
"canonicalization" => {
|
|
63
|
-
section: "Canonicalization",
|
|
64
|
-
pin: "spec/moxml/c14n/adapter_delegation_spec.rb",
|
|
65
|
-
exempt: { oga: "D-04", ox: "D-05", headed_ox: "D-05" },
|
|
66
|
-
},
|
|
67
|
-
"XSLT" => {
|
|
68
|
-
section: "XSLT",
|
|
69
|
-
pin: "spec/consistency/xslt_spec.rb",
|
|
70
|
-
loud: { oga: "Moxml::NotImplementedError",
|
|
71
|
-
ox: "Moxml::NotImplementedError",
|
|
72
|
-
headed_ox: "Moxml::NotImplementedError",
|
|
73
|
-
rexml: "Moxml::NotImplementedError",
|
|
74
|
-
libxml: "Moxml::NotImplementedError" },
|
|
75
|
-
},
|
|
76
|
-
"SAX" => {
|
|
77
|
-
section: "SAX",
|
|
78
|
-
pin: "spec/moxml/sax_spec.rb",
|
|
79
|
-
},
|
|
80
|
-
"materialization" => {
|
|
81
|
-
section: "Materialization",
|
|
82
|
-
pin: "spec/moxml/plan_conformance_spec.rb",
|
|
83
|
-
},
|
|
84
|
-
"loud capability contracts" => {
|
|
85
|
-
section: "Errors and loud degradation",
|
|
86
|
-
pin: "spec/consistency/loud_degradation_spec.rb",
|
|
87
|
-
},
|
|
88
|
-
}.freeze
|
|
18
|
+
MODEL_MATRIX = ModelCoverage::MODEL_COVERAGE_MATRIX # rubocop:disable Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration
|
|
89
19
|
|
|
90
20
|
def register_ids
|
|
91
21
|
File.readlines("docs/_pages/divergences.adoc")
|
|
@@ -134,12 +64,7 @@ RSpec.describe "model coverage matrix" do
|
|
|
134
64
|
# section vanished fails too.
|
|
135
65
|
MODEL_DOC = "docs/_references/processing-model.adoc" # rubocop:disable Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration
|
|
136
66
|
|
|
137
|
-
|
|
138
|
-
"Status and precedence",
|
|
139
|
-
"Mission and invariant",
|
|
140
|
-
"Object model",
|
|
141
|
-
"Performance: a declared, non-semantic channel",
|
|
142
|
-
].freeze
|
|
67
|
+
NON_FACE = ModelCoverage::MODEL_COVERAGE_NON_FACE # rubocop:disable Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration
|
|
143
68
|
|
|
144
69
|
def model_sections
|
|
145
70
|
File.readlines(MODEL_DOC).filter_map do |line|
|
|
@@ -147,6 +72,24 @@ RSpec.describe "model coverage matrix" do
|
|
|
147
72
|
end.uniq
|
|
148
73
|
end
|
|
149
74
|
|
|
75
|
+
# Document-side pin lists (TODO.sota/07): every section carries a
|
|
76
|
+
# `Pins::` line naming the specs that enforce it (`Pins:: none` for
|
|
77
|
+
# non-face sections). Parsed as section => [paths] or nil when the
|
|
78
|
+
# line is missing.
|
|
79
|
+
def model_pin_lists
|
|
80
|
+
lists = {}
|
|
81
|
+
current = nil
|
|
82
|
+
File.readlines(MODEL_DOC).each do |line|
|
|
83
|
+
if (heading = line[/\A=== (\S.*)$/, 1])
|
|
84
|
+
current = heading.strip
|
|
85
|
+
lists[current] = nil
|
|
86
|
+
elsif current && (entry = line[/\APins:: (.*)$/, 1])
|
|
87
|
+
lists[current] = entry.scan(/`([^`]+)`/).flatten
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
lists
|
|
91
|
+
end
|
|
92
|
+
|
|
150
93
|
it "cites an existing model section for every face family" do
|
|
151
94
|
sections = model_sections
|
|
152
95
|
unknown = MODEL_MATRIX.filter_map do |family, cell|
|
|
@@ -162,7 +105,7 @@ RSpec.describe "model coverage matrix" do
|
|
|
162
105
|
it "covers or declares every model section" do
|
|
163
106
|
cited = MODEL_MATRIX.values.filter_map { |cell| cell[:section] }
|
|
164
107
|
uncovered = model_sections.reject do |section|
|
|
165
|
-
cited.include?(section) ||
|
|
108
|
+
cited.include?(section) || NON_FACE.include?(section)
|
|
166
109
|
end
|
|
167
110
|
expect(uncovered).to be_empty,
|
|
168
111
|
"model sections with no coverage matrix family " \
|
|
@@ -170,11 +113,43 @@ RSpec.describe "model coverage matrix" do
|
|
|
170
113
|
end
|
|
171
114
|
|
|
172
115
|
it "declares non-face sections that actually exist in the model doc" do
|
|
173
|
-
stale =
|
|
116
|
+
stale = NON_FACE.reject { |s| model_sections.include?(s) }
|
|
174
117
|
expect(stale).to be_empty,
|
|
175
118
|
"non-face declarations citing missing sections: #{stale.join('; ')}"
|
|
176
119
|
end
|
|
177
120
|
|
|
121
|
+
it "gives every model section a Pins line" do
|
|
122
|
+
missing = model_pin_lists.select { |_, pins| pins.nil? }.keys
|
|
123
|
+
expect(missing).to be_empty,
|
|
124
|
+
"model sections without a Pins line: #{missing.join('; ')}"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it "matches the model doc's face-section pin lists to the matrix" do
|
|
128
|
+
lists = model_pin_lists
|
|
129
|
+
drift = ModelCoverage.sections.filter_map do |section|
|
|
130
|
+
expected = ModelCoverage.pins_for(section)
|
|
131
|
+
got = lists[section]
|
|
132
|
+
"#{section}: doc #{got.inspect} vs matrix #{expected.inspect}" if got != expected
|
|
133
|
+
end
|
|
134
|
+
expect(drift).to be_empty,
|
|
135
|
+
"model-doc pin lists drift from the matrix: #{drift.join('; ')}"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
it "leaves non-face sections without pins" do
|
|
139
|
+
lists = model_pin_lists
|
|
140
|
+
pinned = NON_FACE.select { |section| lists[section]&.any? }
|
|
141
|
+
expect(pinned).to be_empty,
|
|
142
|
+
"non-face sections must declare Pins:: none: #{pinned.join('; ')}"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
it "points every model-doc pin at an existing spec" do
|
|
146
|
+
missing = model_pin_lists.values.compact.flatten.uniq.reject do |path|
|
|
147
|
+
File.exist?(path)
|
|
148
|
+
end
|
|
149
|
+
expect(missing).to be_empty,
|
|
150
|
+
"model-doc pins missing on disk: #{missing.join(', ')}"
|
|
151
|
+
end
|
|
152
|
+
|
|
178
153
|
# Bidirectional linkage (TODO.sota/03): the model doc, the pins, and
|
|
179
154
|
# the reference form one checkable triangle. Every pin cites the
|
|
180
155
|
# section it enforces; every citation names a section that exists.
|
|
@@ -213,7 +188,7 @@ RSpec.describe "model coverage matrix" do
|
|
|
213
188
|
File.exist?(path) ? pin_citations(path) : []
|
|
214
189
|
end
|
|
215
190
|
uncovered = model_sections.reject do |section|
|
|
216
|
-
cited.include?(section) ||
|
|
191
|
+
cited.include?(section) || NON_FACE.include?(section)
|
|
217
192
|
end
|
|
218
193
|
expect(uncovered).to be_empty,
|
|
219
194
|
"model sections no pin cites: #{uncovered.join('; ')}"
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "support/model_coverage"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
# Published documentation is part of the specification apparatus
|
|
8
|
+
# (TODO.sota/07): generated blocks must be views of committed data. A
|
|
9
|
+
# data change without regeneration fails here, so the published
|
|
10
|
+
# coverage proof and scoreboard can never claim more than the tests do.
|
|
11
|
+
RSpec.describe "published documentation" do
|
|
12
|
+
it "keeps the model-coverage page in sync with the matrix" do
|
|
13
|
+
page = File.read("docs/_pages/model-coverage.adoc")
|
|
14
|
+
block = page[/MODEL-COVERAGE:BEGIN\n(.*?)\n?\/\/ MODEL-COVERAGE:END/m, 1]
|
|
15
|
+
expect(block).not_to be_nil, "coverage page is missing its markers"
|
|
16
|
+
|
|
17
|
+
expect(block).to eq(ModelCoverage.render_page_block),
|
|
18
|
+
"model-coverage page is stale: run rake docs:coverage"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it "keeps every published scoreboard row in sync with the JSON data" do
|
|
22
|
+
data = JSON.parse(File.read("benchmark/scoreboard.json"))
|
|
23
|
+
page = File.read("docs/_pages/performance.adoc")
|
|
24
|
+
block = page[/SCOREBOARD:BEGIN(.*?)SCOREBOARD:END/m, 1]
|
|
25
|
+
expect(block).not_to be_nil, "performance page is missing its markers"
|
|
26
|
+
|
|
27
|
+
data["rows"].each do |row|
|
|
28
|
+
expect(block).to include(
|
|
29
|
+
"| `#{row['row']}` | #{format('%.2f', row['ratio'])}x | " \
|
|
30
|
+
"#{row['kind']} |",
|
|
31
|
+
), "scoreboard row #{row['row']} is stale against scoreboard.json"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
expect(block.scan(/\| `[^`]+` \| \d+\.\d\dx \|/).size)
|
|
35
|
+
.to eq(data["rows"].size), "scoreboard row count differs"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it "names the measured engine version in the scoreboard metadata" do
|
|
39
|
+
data = JSON.parse(File.read("benchmark/scoreboard.json"))
|
|
40
|
+
protocol = data["protocol"]
|
|
41
|
+
page = File.read("docs/_pages/performance.adoc")
|
|
42
|
+
block = page[/SCOREBOARD:BEGIN(.*?)SCOREBOARD:END/m, 1]
|
|
43
|
+
|
|
44
|
+
expect(block).to include("#{protocol['adapter']} " \
|
|
45
|
+
"#{protocol['adapter_version']}"),
|
|
46
|
+
"scoreboard metadata does not name the measured engine"
|
|
47
|
+
expect(block).to include("nokogiri #{protocol['nokogiri']}")
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -42,11 +42,6 @@ RSpec.describe "XPath idioms across adapters" do
|
|
|
42
42
|
Gem::Version.new(Leptris::VERSION) < Gem::Version.new("1.9.359.0")
|
|
43
43
|
skip "binding #{Leptris::VERSION} predates the xmlns: fix (1.9.359)"
|
|
44
44
|
end
|
|
45
|
-
if adapter_name == :oga
|
|
46
|
-
# oga's own Ruby XPath engine has no xmlns: default-ns
|
|
47
|
-
# support (third-party; explicit bindings work).
|
|
48
|
-
skip "D-01: oga's XPath engine does not implement the xmlns: idiom"
|
|
49
|
-
end
|
|
50
45
|
end
|
|
51
46
|
|
|
52
47
|
it "matches default-namespaced elements via the xmlns: prefix" do
|
|
@@ -62,6 +57,14 @@ RSpec.describe "XPath idioms across adapters" do
|
|
|
62
57
|
expect(targets).to be_empty
|
|
63
58
|
end
|
|
64
59
|
|
|
60
|
+
it "addresses the no-namespace default with an empty xmlns binding" do
|
|
61
|
+
doc = ctx.parse(
|
|
62
|
+
'<root xmlns="urn:a"><child xmlns="urn:b"><leaf xmlns=""/></child></root>',
|
|
63
|
+
)
|
|
64
|
+
leaf = doc.at_xpath("//xmlns:leaf", "xmlns" => "")
|
|
65
|
+
expect(leaf&.name).to eq("leaf")
|
|
66
|
+
end
|
|
67
|
+
|
|
65
68
|
it "resolves attribute-terminal steps in every path position" do
|
|
66
69
|
doc = ctx.parse(plain_xml)
|
|
67
70
|
expect(doc.xpath("//a/@id").first&.value).to eq("x")
|
|
@@ -118,6 +121,21 @@ RSpec.describe "XPath idioms across adapters" do
|
|
|
118
121
|
expect(doc.xpath("boolean(//b)")).to be(true)
|
|
119
122
|
expect(doc.at_xpath("count(//a)")).to eq(1.0)
|
|
120
123
|
end
|
|
124
|
+
|
|
125
|
+
# The live-tree contract: a sibling inserted after an XPath
|
|
126
|
+
# evaluation must be visible to at_xpath and xpath. leptris's
|
|
127
|
+
# node-set lane caches a document index that sibling insertions
|
|
128
|
+
# do not rebuild (engine bug, tracked upstream); the adapter
|
|
129
|
+
# routes such documents to the reference engine until a child
|
|
130
|
+
# insertion rebuilds the index.
|
|
131
|
+
it "answers at_xpath and xpath from the live tree after a sibling insertion" do
|
|
132
|
+
doc = ctx.parse("<r><a><b/></a></r>")
|
|
133
|
+
doc.xpath("//a")
|
|
134
|
+
doc.at("//a").next = "<c/>"
|
|
135
|
+
expect(doc.at("//c")&.name).to eq("c")
|
|
136
|
+
expect(doc.xpath("//c").map(&:name)).to eq(["c"])
|
|
137
|
+
expect(doc.at_xpath("count(//c)")).to eq(1.0)
|
|
138
|
+
end
|
|
121
139
|
end
|
|
122
140
|
end
|
|
123
141
|
end
|
|
@@ -80,15 +80,6 @@ RSpec.shared_examples "Moxml Edge Cases" do
|
|
|
80
80
|
|
|
81
81
|
describe "namespace edge cases" do
|
|
82
82
|
it "handles default namespace changes" do
|
|
83
|
-
if context.config.adapter_name == :headed_ox
|
|
84
|
-
skip "HeadedOx limitation: Namespace methods not implemented in adapter. Requires Ox namespace API enhancement. See docs/_pages/headed-ox-limitations.adoc"
|
|
85
|
-
end
|
|
86
|
-
if context.config.adapter_name == :libxml
|
|
87
|
-
skip "LibXML cannot query empty default namespace with XPath (documented limitation)"
|
|
88
|
-
end
|
|
89
|
-
if context.config.adapter_name == :nokogiri
|
|
90
|
-
skip "Nokogiri XPath does not support querying empty namespace with xmlns prefix mapping"
|
|
91
|
-
end
|
|
92
83
|
xml = <<~XML
|
|
93
84
|
<root xmlns="http://default1.org">
|
|
94
85
|
<child xmlns="http://default2.org">
|
|
@@ -103,10 +94,6 @@ RSpec.shared_examples "Moxml Edge Cases" do
|
|
|
103
94
|
end
|
|
104
95
|
|
|
105
96
|
it "handles recursive namespace definitions" do
|
|
106
|
-
if context.config.adapter_name == :headed_ox
|
|
107
|
-
skip "HeadedOx limitation: Namespace methods not implemented in adapter. Requires Ox namespace API enhancement. See docs/_pages/headed-ox-limitations.adoc"
|
|
108
|
-
end
|
|
109
|
-
|
|
110
97
|
xml = <<~XML
|
|
111
98
|
<root xmlns:a="http://a.org">
|
|
112
99
|
<a:child xmlns:a="http://b.org">
|
|
@@ -184,12 +184,14 @@ RSpec.describe Moxml::Adapter::Leptris do
|
|
|
184
184
|
end
|
|
185
185
|
end
|
|
186
186
|
|
|
187
|
-
it "keeps comments in
|
|
187
|
+
it "keeps comments in indented serialization with tab units (leptris-ruby#115)" do
|
|
188
188
|
skip "requires the element unit fix (leptris 1.9.50+)" unless described_class::ELEMENT_UNIT_COMMENTS
|
|
189
189
|
|
|
190
|
+
# Element-level indentation is loud on this engine (D-09); the
|
|
191
|
+
# document lane exercises the same generator's unit handling.
|
|
190
192
|
doc = ctx.parse(%(<r><a/><!-- c --></r>))
|
|
191
|
-
expect(doc.
|
|
192
|
-
.to eq("<r>\n\t<a></a>\n\t<!-- c -->\n</r
|
|
193
|
+
expect(doc.to_xml(indent: 1, indent_text: "\t"))
|
|
194
|
+
.to eq("<r>\n\t<a></a>\n\t<!-- c -->\n</r>\n")
|
|
193
195
|
end
|
|
194
196
|
|
|
195
197
|
it "round-trips document-level PI mutations through serialization" do
|
|
@@ -846,4 +848,112 @@ RSpec.describe Moxml::Adapter::Leptris do
|
|
|
846
848
|
expect(first.next_sibling.parent).to eq(doc.root)
|
|
847
849
|
end
|
|
848
850
|
end
|
|
851
|
+
|
|
852
|
+
describe "stale-index guard retirement (leptris-ruby#519)" do
|
|
853
|
+
let(:ctx) { Moxml.new(:leptris) }
|
|
854
|
+
|
|
855
|
+
it "auto-retires the guard when the engine invalidates its index" do
|
|
856
|
+
skip "requires an engine whose node-set index survives sibling insertion (leptris-ruby#519)" unless described_class.native_xpath_index_fixed?
|
|
857
|
+
|
|
858
|
+
doc = ctx.parse("<r><a/><b/></r>")
|
|
859
|
+
root = doc.root
|
|
860
|
+
_ = root.xpath("child::*") # populate the engine node-set index
|
|
861
|
+
b = root.children.to_a[1]
|
|
862
|
+
b.add_previous_sibling(doc.create_element("c"))
|
|
863
|
+
|
|
864
|
+
native = described_class.native_xpath(root.native, "child::*", nil)
|
|
865
|
+
expect(native).not_to be_nil
|
|
866
|
+
expect(native.to_a.map { |n| n.name.to_s }).to eq(%w[a c b])
|
|
867
|
+
end
|
|
868
|
+
|
|
869
|
+
it "never arms the guard on engines that invalidate on splice" do
|
|
870
|
+
skip "requires an engine whose node-set index survives sibling insertion (leptris-ruby#519)" unless described_class.native_xpath_index_fixed?
|
|
871
|
+
|
|
872
|
+
doc = ctx.parse("<root><anchor/></root>")
|
|
873
|
+
doc.xpath("//ext") # prime the engine index
|
|
874
|
+
doc.root.add_child(doc.create_element("ext"))
|
|
875
|
+
expect(doc.xpath("//ext").size).to eq(1)
|
|
876
|
+
|
|
877
|
+
expect(
|
|
878
|
+
described_class.attachments.get(
|
|
879
|
+
described_class.xpath_index_document(doc.root.native),
|
|
880
|
+
described_class.singleton_class.const_get(:XPATH_INDEX_STALE),
|
|
881
|
+
),
|
|
882
|
+
).to be_nil
|
|
883
|
+
end
|
|
884
|
+
end
|
|
885
|
+
|
|
886
|
+
describe "namespace scope-generation gate" do
|
|
887
|
+
let(:ctx) { Moxml.new(:leptris) }
|
|
888
|
+
|
|
889
|
+
# The per-attach scope-generation bump only matters once
|
|
890
|
+
# namespaces exist; the builder hot path must not pay it.
|
|
891
|
+
it "does not bump the scope generation while namespaces are absent" do
|
|
892
|
+
doc = ctx.create_document
|
|
893
|
+
root = doc.create_element("r")
|
|
894
|
+
doc.root = root
|
|
895
|
+
gen = ctx.namespace_scope_generation
|
|
896
|
+
root.add_child(doc.create_element("a"))
|
|
897
|
+
root.add_child(doc.create_element("b"))
|
|
898
|
+
expect(ctx.namespace_scope_generation).to eq(gen)
|
|
899
|
+
expect(ctx.namespace_scopes_active?).to be(false)
|
|
900
|
+
end
|
|
901
|
+
|
|
902
|
+
it "arms the gate when a write introduces a namespace" do
|
|
903
|
+
doc = ctx.create_document
|
|
904
|
+
root = doc.create_element("r")
|
|
905
|
+
doc.root = root
|
|
906
|
+
root["xmlns:foo"] = "urn:x"
|
|
907
|
+
expect(ctx.namespace_scopes_active?).to be(true)
|
|
908
|
+
expect(root.namespaces.map { |n| [n.prefix, n.uri] })
|
|
909
|
+
.to include(%w[foo urn:x])
|
|
910
|
+
end
|
|
911
|
+
|
|
912
|
+
it "is armed by parsed documents" do
|
|
913
|
+
doc = ctx.parse("<r xmlns:foo='urn:x'><foo:a/></r>")
|
|
914
|
+
expect(ctx.namespace_scopes_active?).to be(true)
|
|
915
|
+
expect(doc.xpath("//foo:a", "foo" => "urn:x").size).to eq(1)
|
|
916
|
+
end
|
|
917
|
+
|
|
918
|
+
it "imports the flag from a foreign context on adoption" do
|
|
919
|
+
donor_ctx = Moxml.new(:leptris)
|
|
920
|
+
donor = donor_ctx.parse("<x xmlns:foo='urn:x'/>")
|
|
921
|
+
doc = ctx.create_document
|
|
922
|
+
root = doc.create_element("r")
|
|
923
|
+
doc.root = root
|
|
924
|
+
expect(ctx.namespace_scopes_active?).to be(false)
|
|
925
|
+
root.add_child(donor.root)
|
|
926
|
+
expect(ctx.namespace_scopes_active?).to be(true)
|
|
927
|
+
expect(root.children.first.namespaces.map(&:uri))
|
|
928
|
+
.to include("urn:x")
|
|
929
|
+
end
|
|
930
|
+
end
|
|
931
|
+
|
|
932
|
+
# The README error-handling contract: an invalid element name must
|
|
933
|
+
# raise Moxml::ValidationError at CREATE time. The adapter's native
|
|
934
|
+
# fast paths (create_primed_element, create_element_with_attrs) mint
|
|
935
|
+
# from a name without passing through Base#create_element, where
|
|
936
|
+
# validate_element_name lives — the failure then surfaced late, at
|
|
937
|
+
# add_child, as Moxml::Error "Document already has a root element".
|
|
938
|
+
describe "element name validation on the create seam" do
|
|
939
|
+
let(:ctx) { Moxml.new(:leptris) }
|
|
940
|
+
|
|
941
|
+
it "raises ValidationError when create_element mints an invalid name" do
|
|
942
|
+
doc = ctx.parse("<root/>")
|
|
943
|
+
expect { doc.create_element("wrong,name") }
|
|
944
|
+
.to raise_error(Moxml::ValidationError, /Invalid XML element name/)
|
|
945
|
+
end
|
|
946
|
+
|
|
947
|
+
it "raises ValidationError when add_element mints an invalid name" do
|
|
948
|
+
doc = ctx.create_document
|
|
949
|
+
expect { doc.add_element("bad,name", id: "1") }
|
|
950
|
+
.to raise_error(Moxml::ValidationError, /Invalid XML element name/)
|
|
951
|
+
end
|
|
952
|
+
|
|
953
|
+
it "still mints valid names with punctuation allowed in XML Names" do
|
|
954
|
+
doc = ctx.parse("<root/>")
|
|
955
|
+
element = doc.create_element("ns:my-element.v2")
|
|
956
|
+
expect(element).to be_a(Moxml::Element)
|
|
957
|
+
end
|
|
958
|
+
end
|
|
849
959
|
end
|
|
@@ -457,10 +457,6 @@ RSpec.shared_examples "xml adapter" do
|
|
|
457
457
|
end
|
|
458
458
|
|
|
459
459
|
it "preserves and correctly handles multiple namespaces" do
|
|
460
|
-
if described_class.name.include?("Ox")
|
|
461
|
-
skip("Ox does not support namespace-aware XPath queries (documented limitation)")
|
|
462
|
-
end
|
|
463
|
-
|
|
464
460
|
# Parse original XML
|
|
465
461
|
doc = described_class.parse(xml).native
|
|
466
462
|
|
|
@@ -513,10 +509,6 @@ RSpec.shared_examples "xml adapter" do
|
|
|
513
509
|
end
|
|
514
510
|
|
|
515
511
|
it "preserves and correctly handles multiple namespaces" do
|
|
516
|
-
if described_class.name.include?("Ox")
|
|
517
|
-
skip("Ox does not support namespace-aware XPath queries (documented limitation)")
|
|
518
|
-
end
|
|
519
|
-
|
|
520
512
|
# Parse original XML
|
|
521
513
|
doc = described_class.parse(xml).native
|
|
522
514
|
|
|
@@ -31,6 +31,39 @@ RSpec.describe Moxml::AttributeResolver do
|
|
|
31
31
|
expect(described_class.resolve(element, "z:type")).to be_nil
|
|
32
32
|
expect(described_class.resolve(element, "xmlns:q")).to be_nil
|
|
33
33
|
end
|
|
34
|
+
|
|
35
|
+
# Namespaces 1.0 §5.2: the default namespace never applies to
|
|
36
|
+
# attributes. Engines that resolve unprefixed attributes against
|
|
37
|
+
# the default namespace must answer nil for it, or bare-name reads
|
|
38
|
+
# miss on every element under a default namespace.
|
|
39
|
+
it "never treats bare attributes as default-namespace members" do
|
|
40
|
+
Moxml::Adapter::AVAILABLE_ADAPTERS.each do |adapter_name|
|
|
41
|
+
d = Moxml.new(adapter_name).parse(%(<r xmlns="urn:a"><e id="1"/></r>))
|
|
42
|
+
e = d.root.children.find(&:element?)
|
|
43
|
+
expect(e["id"]).to eq("1"), "#{adapter_name}: bare read"
|
|
44
|
+
expect(e.attribute("id")&.value).to eq("1"),
|
|
45
|
+
"#{adapter_name}: resolver"
|
|
46
|
+
expect(e.attribute("id")&.namespace).to be_nil,
|
|
47
|
+
"#{adapter_name}: attribute namespace"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it "keeps bare reads on programmatically built default-namespaced trees" do
|
|
52
|
+
Moxml::Adapter::AVAILABLE_ADAPTERS.each do |adapter_name|
|
|
53
|
+
ctx = Moxml.new(adapter_name)
|
|
54
|
+
d = ctx.create_document
|
|
55
|
+
root = d.create_element("root")
|
|
56
|
+
root.add_namespace(nil, "urn:a")
|
|
57
|
+
d.add_child(root)
|
|
58
|
+
e = d.create_element("item")
|
|
59
|
+
e["id"] = "123"
|
|
60
|
+
root.add_child(e)
|
|
61
|
+
|
|
62
|
+
expect(e["id"]).to eq("123"), "#{adapter_name}: bare read"
|
|
63
|
+
expect(d.xpath('//xmlns:item[@id="123"]').to_a.size).to eq(1),
|
|
64
|
+
"#{adapter_name}: xmlns predicate"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
34
67
|
end
|
|
35
68
|
|
|
36
69
|
describe ".assign" do
|
|
@@ -59,20 +59,47 @@ RSpec.describe "Moxml::C14n cross-adapter canonicalization" do
|
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
describe "document-level nodes keep document order" do
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
let(:
|
|
62
|
+
# ox/headed_ox drop PI content at parse (D-05), so content-bearing
|
|
63
|
+
# document-level PIs are pinned on the remaining adapters; the
|
|
64
|
+
# separator/newline rule itself is pinned on every adapter below
|
|
65
|
+
# (comments and empty-data PIs are common across all of them).
|
|
66
|
+
let(:content_adapters) do
|
|
67
|
+
Moxml::Adapter::AVAILABLE_ADAPTERS - %i[ox headed_ox]
|
|
68
|
+
end
|
|
67
69
|
|
|
68
70
|
it "canonicalizes a leading PI before the root element" do
|
|
69
|
-
|
|
71
|
+
content_adapters.each do |adapter_name|
|
|
70
72
|
doc = Moxml.new(adapter_name)
|
|
71
73
|
.parse(%(<?xml version="1.0"?><?render mode="x"?><doc><e/></doc>))
|
|
72
74
|
expect(Moxml::C14n.canonicalize(doc))
|
|
73
75
|
.to eq(%(<?render mode="x"?>\n<doc><e></e></doc>))
|
|
74
76
|
end
|
|
75
77
|
end
|
|
78
|
+
|
|
79
|
+
it "uses exactly one newline between document-level nodes" do
|
|
80
|
+
Moxml::Adapter::AVAILABLE_ADAPTERS.each do |adapter_name|
|
|
81
|
+
doc = Moxml.new(adapter_name)
|
|
82
|
+
.parse(%(<?pi?><?pi2?><!--a--><doc/><!--b--><?pi3?>))
|
|
83
|
+
expect(Moxml::C14n.canonicalize(doc, with_comments: true))
|
|
84
|
+
.to eq(%(<?pi?>\n<?pi2?>\n<!--a-->\n<doc></doc>\n<!--b-->\n<?pi3?>))
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it "never emits the XML declaration" do
|
|
89
|
+
Moxml::Adapter::AVAILABLE_ADAPTERS.each do |adapter_name|
|
|
90
|
+
doc = Moxml.new(adapter_name)
|
|
91
|
+
.parse(%(<?xml version="1.0" encoding="UTF-8"?><doc/>))
|
|
92
|
+
expect(Moxml::C14n.canonicalize(doc)).to eq("<doc></doc>")
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it "collapses PI separator whitespace to one space" do
|
|
97
|
+
Moxml::Adapter::AVAILABLE_ADAPTERS.each do |adapter_name|
|
|
98
|
+
doc = Moxml.new(adapter_name).parse(%(<?pi a b?><doc/>))
|
|
99
|
+
expect(Moxml::C14n.canonicalize(doc))
|
|
100
|
+
.to eq(%(<?pi a b?>\n<doc></doc>))
|
|
101
|
+
end
|
|
102
|
+
end
|
|
76
103
|
end
|
|
77
104
|
|
|
78
105
|
describe "native delegation" do
|
|
@@ -12,25 +12,19 @@ LATIN1_XML = (+"<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?><r>caf\xE9</r>").
|
|
|
12
12
|
BOM_XML = (+"\xEF\xBB\xBF<r/>").force_encoding("UTF-8")
|
|
13
13
|
NFC_XML = (+"<r>caf\xC3\xA9</r>").force_encoding("UTF-8")
|
|
14
14
|
|
|
15
|
-
# oga's parse layer leaks the declaration as a PI node and the BOM as
|
|
16
|
-
# text (pre-existing, documented in the W3C examples spec).
|
|
17
|
-
OGA_LEAK = %i[oga].freeze
|
|
18
|
-
|
|
19
15
|
RSpec.describe "Moxml character encoding at the parse boundary" do
|
|
20
16
|
Moxml::Adapter::AVAILABLE_ADAPTERS.each do |adapter_name|
|
|
21
17
|
context "with the #{adapter_name} adapter" do
|
|
22
18
|
let(:ctx) { Moxml.new(adapter_name) }
|
|
23
19
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
end
|
|
20
|
+
it "transcodes declared Latin-1 to UTF-8 bytes" do
|
|
21
|
+
doc = ctx.parse(LATIN1_XML.dup)
|
|
22
|
+
expect(Moxml::C14n.canonicalize(doc).bytes).to eq(CAF_E_UTF8)
|
|
23
|
+
end
|
|
29
24
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
end
|
|
25
|
+
it "strips a leading UTF-8 BOM" do
|
|
26
|
+
doc = ctx.parse(BOM_XML.dup)
|
|
27
|
+
expect(Moxml::C14n.canonicalize(doc)).to eq("<r></r>")
|
|
34
28
|
end
|
|
35
29
|
|
|
36
30
|
it "passes NFC content through without normalization" do
|
|
@@ -48,18 +48,45 @@ RSpec.describe "C14N comments and PIs" do
|
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
describe "PIs at document level" do
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
it "renders PIs outside the document element in document order" do
|
|
52
|
+
doc = ctx.parse(%(<?pi before?><root><?pi inner?></root><?pi after?>))
|
|
53
|
+
expect(Moxml::C14n.canonicalize(doc))
|
|
54
|
+
.to eq(%(<?pi before?>\n<root><?pi inner?></root>\n<?pi after?>))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it "uses exactly one newline between document-level nodes" do
|
|
58
|
+
doc = ctx.parse(%(<?pi a?><?pi b?><root/><?pi c?>))
|
|
59
|
+
expect(Moxml::C14n.canonicalize(doc))
|
|
60
|
+
.to eq(%(<?pi a?>\n<?pi b?>\n<root></root>\n<?pi c?>))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it "ignores whitespace between document-level nodes" do
|
|
64
|
+
# rexml retains the text node (D-15); the canonical form never
|
|
65
|
+
# includes whitespace outside the document element.
|
|
66
|
+
Moxml::Adapter::AVAILABLE_ADAPTERS.each do |adapter_name|
|
|
67
|
+
doc = Moxml.new(adapter_name).parse(%(<?pi?>\n<root/>\n<!--c-->))
|
|
68
|
+
expect(Moxml::C14n.canonicalize(doc, with_comments: true))
|
|
69
|
+
.to eq(%(<?pi?>\n<root></root>\n<!--c-->)), adapter_name.to_s
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it "omits the XML declaration from the canonical form" do
|
|
74
|
+
doc = ctx.parse(%(<?xml version="1.0" encoding="UTF-8"?><root/>))
|
|
75
|
+
expect(Moxml::C14n.canonicalize(doc)).to eq("<root></root>")
|
|
57
76
|
end
|
|
58
77
|
end
|
|
59
78
|
|
|
60
79
|
describe "comments at document level" do
|
|
61
|
-
it "renders document-level comments when with_comments: true
|
|
62
|
-
|
|
80
|
+
it "renders document-level comments when with_comments: true" do
|
|
81
|
+
doc = ctx.parse(%(<!--lead--><root><!--inner--></root><!--trail-->))
|
|
82
|
+
expect(Moxml::C14n.canonicalize(doc, with_comments: true))
|
|
83
|
+
.to eq(%(<!--lead-->\n<root><!--inner--></root>\n<!--trail-->))
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it "keeps exactly one newline between a document-level PI and comment" do
|
|
87
|
+
doc = ctx.parse(%(<?pi x?><!--lead--><root/>))
|
|
88
|
+
expect(Moxml::C14n.canonicalize(doc, with_comments: true))
|
|
89
|
+
.to eq(%(<?pi x?>\n<!--lead-->\n<root></root>))
|
|
63
90
|
end
|
|
64
91
|
end
|
|
65
92
|
end
|