moxml 0.5.165 → 0.5.167
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/Gemfile +1 -1
- data/benchmark/README.md +9 -9
- data/benchmark/scoreboard.json +32 -32
- data/benchmark/scoreboard.rb +11 -0
- data/docs/_pages/divergences.adoc +10 -0
- data/docs/_pages/model-coverage.adoc +2 -0
- data/docs/_pages/performance.adoc +10 -10
- data/docs/_references/processing-model.adoc +16 -4
- data/lib/moxml/adapter/leptris.rb +110 -4
- data/lib/moxml/config.rb +6 -1
- data/lib/moxml/document.rb +17 -0
- data/lib/moxml/node_set.rb +6 -1
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xsd/schema_rules.rb +178 -44
- data/lib/moxml/xsd.rb +39 -5
- data/spec/consistency/xpath_idioms_spec.rb +13 -0
- data/spec/moxml/adapter/leptris_crossdoc_replace_spec.rb +37 -0
- data/spec/moxml/adapter/leptris_native_parent_axis_spec.rb +46 -0
- data/spec/moxml/xsd_engine_meta_spec.rb +66 -0
- data/spec/moxml/xsd_substitution_spec.rb +166 -0
- data/spec/support/model_coverage.rb +8 -0
- metadata +6 -2
|
@@ -105,7 +105,9 @@ module Moxml
|
|
|
105
105
|
when "##defined" then :defined
|
|
106
106
|
when "##definedSibling" then :sibling
|
|
107
107
|
when /\A([^:]+):(.+)\z/
|
|
108
|
-
uri = prefix_uri(
|
|
108
|
+
uri = ::Moxml::XSD::SchemaRules.prefix_uri(
|
|
109
|
+
Regexp.last_match(1), element
|
|
110
|
+
)
|
|
109
111
|
"{#{uri}}#{Regexp.last_match(2)}" unless uri.empty?
|
|
110
112
|
else
|
|
111
113
|
"{}#{token}"
|
|
@@ -113,23 +115,6 @@ module Moxml
|
|
|
113
115
|
end
|
|
114
116
|
end
|
|
115
117
|
|
|
116
|
-
# node.namespaces is adapter-divergent (some engines return
|
|
117
|
-
# in-scope bindings, others own-node declarations only), so
|
|
118
|
-
# the in-scope map is walked out by hand — per-node
|
|
119
|
-
# definitions merged along the ancestor axis, deeper
|
|
120
|
-
# overriding shallower, identically on every adapter.
|
|
121
|
-
def prefix_uri(prefix, element)
|
|
122
|
-
bindings = {}
|
|
123
|
-
node = element
|
|
124
|
-
while node
|
|
125
|
-
node.namespaces.each do |namespace|
|
|
126
|
-
bindings[namespace.prefix] = namespace.uri
|
|
127
|
-
end
|
|
128
|
-
node = node.parent&.element? ? node.parent : nil
|
|
129
|
-
end
|
|
130
|
-
bindings[prefix].to_s
|
|
131
|
-
end
|
|
132
|
-
|
|
133
118
|
def entry_covers?(entry, namespace)
|
|
134
119
|
namespace == case entry
|
|
135
120
|
when "##targetNamespace" then :tns
|
|
@@ -140,11 +125,34 @@ module Moxml
|
|
|
140
125
|
end
|
|
141
126
|
|
|
142
127
|
ContentModel = Struct.new(:declared_children, :any,
|
|
143
|
-
:declared_attributes, :any_attribute
|
|
128
|
+
:declared_attributes, :any_attribute,
|
|
129
|
+
:particles, :pure_sequence)
|
|
144
130
|
SchemaIndex = Struct.new(:target_namespace, :element_default,
|
|
145
131
|
:attribute_default, :elements,
|
|
146
132
|
:attributes, :contents, :version,
|
|
147
|
-
:element_patterns, :attribute_patterns
|
|
133
|
+
:element_patterns, :attribute_patterns,
|
|
134
|
+
:substitution_members)
|
|
135
|
+
|
|
136
|
+
# Resolves a QName token against an element's in-scope
|
|
137
|
+
# bindings. node.namespaces is adapter-divergent (some
|
|
138
|
+
# engines return in-scope bindings, others own-node
|
|
139
|
+
# declarations only), so the in-scope map is walked out by
|
|
140
|
+
# hand — per-node definitions merged along the ancestor
|
|
141
|
+
# axis, deeper overriding shallower, identically on every
|
|
142
|
+
# adapter. Unresolvable prefixes yield "".
|
|
143
|
+
module_function
|
|
144
|
+
|
|
145
|
+
def prefix_uri(prefix, element)
|
|
146
|
+
bindings = {}
|
|
147
|
+
node = element
|
|
148
|
+
while node
|
|
149
|
+
node.namespaces.each do |namespace|
|
|
150
|
+
bindings[namespace.prefix] = namespace.uri
|
|
151
|
+
end
|
|
152
|
+
node = node.parent&.element? ? node.parent : nil
|
|
153
|
+
end
|
|
154
|
+
bindings[prefix].to_s
|
|
155
|
+
end
|
|
148
156
|
|
|
149
157
|
# The XSD 1.1 version-tagging namespace (Structures §C.3):
|
|
150
158
|
# vc:minVersion / vc:maxVersion gate 1.1 constructs.
|
|
@@ -243,7 +251,7 @@ module Moxml
|
|
|
243
251
|
root["targetNamespace"].to_s,
|
|
244
252
|
root["elementFormDefault"] == "qualified",
|
|
245
253
|
root["attributeFormDefault"] == "qualified",
|
|
246
|
-
{}, {}, {}, schema_version(root), {}, {}
|
|
254
|
+
{}, {}, {}, schema_version(root), {}, {}, {}
|
|
247
255
|
)
|
|
248
256
|
child_elements(root).each do |decl|
|
|
249
257
|
case decl.name
|
|
@@ -339,27 +347,36 @@ module Moxml
|
|
|
339
347
|
sink[name] = sink.key?(name) ? nil : patterns
|
|
340
348
|
end
|
|
341
349
|
|
|
342
|
-
# Dismisses engine
|
|
343
|
-
#
|
|
344
|
-
#
|
|
345
|
-
#
|
|
346
|
-
#
|
|
347
|
-
#
|
|
348
|
-
#
|
|
349
|
-
#
|
|
350
|
+
# Dismisses engine findings the schema source refutes:
|
|
351
|
+
#
|
|
352
|
+
# * lexical findings pattern facets accept (#464) — dropped
|
|
353
|
+
# only when every same-named instance matches the facet;
|
|
354
|
+
# * content-model findings that reject a substitution-group
|
|
355
|
+
# member (#466 class 1) — dropped only when the parent's
|
|
356
|
+
# content model is a pure sequence, the child sequence
|
|
357
|
+
# matches it position-for-position with every particle
|
|
358
|
+
# exactly-once, and a substitution member resolves a
|
|
359
|
+
# position. One non-vouching instance — or any real
|
|
360
|
+
# mismatch — keeps the engine's answer, so a dismissal
|
|
361
|
+
# can only remove false rejections, never create false
|
|
362
|
+
# acceptances.
|
|
350
363
|
def dismiss(engine_errors, doc, index)
|
|
351
364
|
return engine_errors unless index
|
|
352
365
|
|
|
353
366
|
vouched = vouch_table(doc, index)
|
|
354
367
|
engine_errors.reject do |error|
|
|
355
|
-
subject = lexical_subject(error.message)
|
|
356
|
-
|
|
368
|
+
if (subject = lexical_subject(error.message))
|
|
369
|
+
vouched[subject]
|
|
370
|
+
elsif (sequence = content_model_subject(error.message))
|
|
371
|
+
sequence_vouched?(doc, index, *sequence)
|
|
372
|
+
end
|
|
357
373
|
end
|
|
358
374
|
end
|
|
359
375
|
|
|
360
376
|
LEXICAL_MESSAGE = /\A(?:element|attribute) '([^']+)' @\d+:\d+: (element|attribute) '([^']+)': lexical value does not match its type\z/
|
|
377
|
+
CONTENT_MODEL_MESSAGE = /\Aelement '([^']+)' @\d+:\d+: element '([^']+)': children do not match the content model \(at '([^']+)'\)\z/
|
|
361
378
|
|
|
362
|
-
private_constant :LEXICAL_MESSAGE
|
|
379
|
+
private_constant :LEXICAL_MESSAGE, :CONTENT_MODEL_MESSAGE
|
|
363
380
|
|
|
364
381
|
def lexical_subject(message)
|
|
365
382
|
match = LEXICAL_MESSAGE.match(message.to_s)
|
|
@@ -368,6 +385,81 @@ module Moxml
|
|
|
368
385
|
[match[2], match[3]]
|
|
369
386
|
end
|
|
370
387
|
|
|
388
|
+
def content_model_subject(message)
|
|
389
|
+
match = CONTENT_MODEL_MESSAGE.match(message.to_s)
|
|
390
|
+
return unless match
|
|
391
|
+
|
|
392
|
+
[match[2], match[3]]
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
# Every same-named element in the document must vouch: a
|
|
396
|
+
# sequence the finding names could be valid while another
|
|
397
|
+
# same-named one is not. The root is included — the
|
|
398
|
+
# engine's finding can name it.
|
|
399
|
+
def sequence_vouched?(doc, index, name, offender)
|
|
400
|
+
root = doc.root
|
|
401
|
+
return false unless root
|
|
402
|
+
|
|
403
|
+
witnesses = []
|
|
404
|
+
yield_self = root.name == name
|
|
405
|
+
witnesses << sequence_valid?(root, index, offender) if yield_self
|
|
406
|
+
each_named_element(root, name) do |element|
|
|
407
|
+
witnesses << sequence_valid?(element, index, offender)
|
|
408
|
+
end
|
|
409
|
+
!witnesses.empty? && witnesses.all?
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def each_named_element(node, name, &block)
|
|
413
|
+
child_elements(node).each do |child|
|
|
414
|
+
yield child if child.name == name
|
|
415
|
+
each_named_element(child, name, &block)
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
# The airtight witness: child count equals particle count,
|
|
420
|
+
# every position resolves to its particle's name or a
|
|
421
|
+
# registered member of it, every particle occurs exactly
|
|
422
|
+
# once, and the reported offender is a substitution member
|
|
423
|
+
# realized at some position. Anything else leaves the
|
|
424
|
+
# engine's answer standing.
|
|
425
|
+
def sequence_valid?(element, index, offender)
|
|
426
|
+
model = index.contents[qname(element)]
|
|
427
|
+
return false unless model&.pure_sequence
|
|
428
|
+
|
|
429
|
+
particles = model.particles
|
|
430
|
+
children = child_elements(element)
|
|
431
|
+
return false if children.empty? || children.size != particles.size
|
|
432
|
+
|
|
433
|
+
resolved = false
|
|
434
|
+
children.each_with_index do |child, position|
|
|
435
|
+
particle_name, min, max = particles[position]
|
|
436
|
+
child_qname = qname(child)
|
|
437
|
+
return false unless exactly_once?(min, max)
|
|
438
|
+
|
|
439
|
+
if child_qname == particle_name
|
|
440
|
+
next
|
|
441
|
+
elsif index.substitution_members.fetch(particle_name, [])
|
|
442
|
+
.include?(child_qname)
|
|
443
|
+
resolved = true
|
|
444
|
+
else
|
|
445
|
+
return false
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
resolved && offender_realized?(children, offender, index)
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def exactly_once?(min, max)
|
|
452
|
+
min <= 1 && (max == :unbounded || max >= 1)
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def offender_realized?(children, offender, index)
|
|
456
|
+
members = index.substitution_members.values.flatten
|
|
457
|
+
children.any? do |child|
|
|
458
|
+
child.name == offender &&
|
|
459
|
+
members.include?(qname(child))
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
|
|
371
463
|
def vouch_table(doc, index)
|
|
372
464
|
root = doc.root
|
|
373
465
|
return {} unless root
|
|
@@ -434,6 +526,9 @@ module Moxml
|
|
|
434
526
|
name = decl["name"] or return
|
|
435
527
|
qname = "{#{index.target_namespace}}#{name}"
|
|
436
528
|
index.elements[qname] = { nillable: decl["nillable"] == "true" }
|
|
529
|
+
if (head = decl["substitutionGroup"])
|
|
530
|
+
register_substitution(head, decl, qname, index)
|
|
531
|
+
end
|
|
437
532
|
type_attr = decl.attribute("type")
|
|
438
533
|
model = if type_attr.nil? || type_attr.value.empty?
|
|
439
534
|
inline_model(decl, index)
|
|
@@ -443,6 +538,23 @@ module Moxml
|
|
|
443
538
|
index.contents[qname] = model if model
|
|
444
539
|
end
|
|
445
540
|
|
|
541
|
+
# substitutionGroup is a QName resolved against the global
|
|
542
|
+
# declaration's in-scope bindings (#466 class 1); the
|
|
543
|
+
# member's own qname is target-namespace-qualified — global
|
|
544
|
+
# declarations always are.
|
|
545
|
+
def register_substitution(head, decl, member_qname, index)
|
|
546
|
+
if head.include?(":")
|
|
547
|
+
prefix, _, local = head.partition(":")
|
|
548
|
+
uri = prefix_uri(prefix, decl)
|
|
549
|
+
return if uri.empty?
|
|
550
|
+
|
|
551
|
+
head_qname = "{#{uri}}#{local}"
|
|
552
|
+
else
|
|
553
|
+
head_qname = "{}#{head}"
|
|
554
|
+
end
|
|
555
|
+
(index.substitution_members[head_qname] ||= []) << member_qname
|
|
556
|
+
end
|
|
557
|
+
|
|
446
558
|
def index_global_attribute(decl, index)
|
|
447
559
|
name = decl["name"] or return
|
|
448
560
|
index.attributes["{#{index.target_namespace}}#{name}"] = true
|
|
@@ -468,7 +580,7 @@ module Moxml
|
|
|
468
580
|
end
|
|
469
581
|
|
|
470
582
|
def content_model(type_node, index)
|
|
471
|
-
model = ContentModel.new([], nil, [], nil)
|
|
583
|
+
model = ContentModel.new([], nil, [], nil, [], true)
|
|
472
584
|
collect_model(type_node, index, model)
|
|
473
585
|
model
|
|
474
586
|
end
|
|
@@ -477,36 +589,58 @@ module Moxml
|
|
|
477
589
|
# choice / all / complexContent wrappers flattened): we only
|
|
478
590
|
# ADD findings for strict-wildcard matches, so flattening
|
|
479
591
|
# can only under-report, never over-report. Cardinality and
|
|
480
|
-
# order stay the engine's job
|
|
592
|
+
# order stay the engine's job — EXCEPT for the substitution
|
|
593
|
+
# dismissal (#466 class 1), which vouches only for pure
|
|
594
|
+
# sequences: any choice/all marks the model impure and the
|
|
595
|
+
# flattened order is then not a validity witness.
|
|
481
596
|
def collect_model(node, index, model)
|
|
482
597
|
child_elements(node).each do |child|
|
|
483
598
|
case child.name
|
|
484
599
|
when "element"
|
|
485
|
-
|
|
486
|
-
|
|
600
|
+
if (qname = particle_qname(child, index))
|
|
601
|
+
model.declared_children << qname
|
|
602
|
+
model.particles << [
|
|
603
|
+
qname,
|
|
604
|
+
occur_value(child, "minOccurs", 1),
|
|
605
|
+
occur_value(child, "maxOccurs", 1),
|
|
606
|
+
]
|
|
607
|
+
end
|
|
487
608
|
when "attribute"
|
|
488
|
-
|
|
489
|
-
|
|
609
|
+
if (qname = particle_qname(child, index))
|
|
610
|
+
model.declared_attributes << qname
|
|
611
|
+
end
|
|
490
612
|
when "any"
|
|
491
613
|
model.any ||= wildcard_from(child)
|
|
492
614
|
when "anyAttribute"
|
|
493
615
|
model.any_attribute ||= wildcard_from(child)
|
|
616
|
+
when "choice", "all"
|
|
617
|
+
model.pure_sequence = false
|
|
618
|
+
collect_model(child, index, model)
|
|
494
619
|
else
|
|
495
620
|
collect_model(child, index, model)
|
|
496
621
|
end
|
|
497
622
|
end
|
|
498
623
|
end
|
|
499
624
|
|
|
500
|
-
def
|
|
625
|
+
def particle_qname(particle, index)
|
|
501
626
|
ref = particle["ref"]
|
|
502
627
|
name = ref&.split(":")&.last || particle["name"]
|
|
503
628
|
return unless name
|
|
504
629
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
630
|
+
if ref
|
|
631
|
+
"{#{index.target_namespace}}#{name}"
|
|
632
|
+
else
|
|
633
|
+
"{#{form_namespace(particle, index)}}#{name}"
|
|
634
|
+
end
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
def occur_value(particle, attribute, default)
|
|
638
|
+
value = particle[attribute]
|
|
639
|
+
return :unbounded if value == "unbounded"
|
|
640
|
+
|
|
641
|
+
Integer(value || default)
|
|
642
|
+
rescue ArgumentError
|
|
643
|
+
default
|
|
510
644
|
end
|
|
511
645
|
|
|
512
646
|
# Particles declare with form="qualified"/"unqualified",
|
data/lib/moxml/xsd.rb
CHANGED
|
@@ -177,15 +177,49 @@ module Moxml
|
|
|
177
177
|
@context.config.adapter.xsd_declarations(@handle)
|
|
178
178
|
end
|
|
179
179
|
|
|
180
|
-
# The effective schema version
|
|
181
|
-
#
|
|
182
|
-
#
|
|
183
|
-
#
|
|
184
|
-
#
|
|
180
|
+
# The effective schema version (#460 item 2): "1.1" or
|
|
181
|
+
# "1.0". The engine's answer (root @version) can only
|
|
182
|
+
# UPGRADE to 1.1 — it never downgrades the source-derived
|
|
183
|
+
# answer, because vc:minVersion="1.1" selects 1.1 semantics
|
|
184
|
+
# even when @version is absent, and @version="1.1" alongside
|
|
185
|
+
# vc:minVersion="1.0" is an authoring contradiction the
|
|
186
|
+
# source annotation wins.
|
|
185
187
|
def version
|
|
188
|
+
adapter = @context.config.adapter
|
|
189
|
+
engine = if adapter.respond_to?(:xsd_version)
|
|
190
|
+
adapter.xsd_version(@handle)
|
|
191
|
+
end
|
|
192
|
+
return "1.1" if engine == "1.1"
|
|
193
|
+
|
|
186
194
|
@rules_index ? @rules_index.version : "1.0"
|
|
187
195
|
end
|
|
188
196
|
|
|
197
|
+
# One row per engine finding with the structured fields the
|
|
198
|
+
# #1843 report carries (moxml#460): kind, component, line,
|
|
199
|
+
# column, message. Nil when the binding predates the face —
|
|
200
|
+
# callers keep using #validate/#errors for the flat shape.
|
|
201
|
+
EngineFinding = Struct.new(:kind, :component, :line, :column,
|
|
202
|
+
:message)
|
|
203
|
+
|
|
204
|
+
def structured_errors(node_or_xml)
|
|
205
|
+
check_released!
|
|
206
|
+
doc = case node_or_xml
|
|
207
|
+
when Moxml::Document then node_or_xml
|
|
208
|
+
when Moxml::Node then node_or_xml.document
|
|
209
|
+
else @context.parse(node_or_xml.to_s)
|
|
210
|
+
end
|
|
211
|
+
adapter = @context.config.adapter
|
|
212
|
+
rows = if adapter.respond_to?(:xsd_structured_errors)
|
|
213
|
+
adapter.xsd_structured_errors(@handle, doc.native)
|
|
214
|
+
end
|
|
215
|
+
return nil unless rows
|
|
216
|
+
|
|
217
|
+
rows.map do |r|
|
|
218
|
+
EngineFinding.new(r.kind, r.component, r.line, r.column,
|
|
219
|
+
r.message)
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
189
223
|
# Deterministic drop (#362.3): true when the adapter freed
|
|
190
224
|
# the engine-side schema NOW; false when the adapter can only
|
|
191
225
|
# wait for GC (documented per adapter). After a true release
|
|
@@ -122,6 +122,19 @@ RSpec.describe "XPath idioms across adapters" do
|
|
|
122
122
|
expect(doc.at_xpath("count(//a)")).to eq(1.0)
|
|
123
123
|
end
|
|
124
124
|
|
|
125
|
+
# The parent of an attribute node is its OWNER element
|
|
126
|
+
# (XPath 1.0 section 2.2) — the load-bearing arm of standoc's
|
|
127
|
+
# svg id disambiguation (#474).
|
|
128
|
+
it "answers the parent-of-attribute step with the owner element" do
|
|
129
|
+
doc = ctx.parse("<r><svg id='s'><use id='u'/></svg></r>")
|
|
130
|
+
svg = doc.xpath("//*[local-name()='svg']").first
|
|
131
|
+
expect(svg.xpath("./@id/..").map(&:name)).to eq(["svg"])
|
|
132
|
+
expect(svg.xpath("./@id/parent::*").map(&:name)).to eq(["svg"])
|
|
133
|
+
expect(
|
|
134
|
+
svg.xpath(".//*[@id] | ./@id/..").map(&:name).sort,
|
|
135
|
+
).to eq(%w[svg use])
|
|
136
|
+
end
|
|
137
|
+
|
|
125
138
|
# The live-tree contract: a sibling inserted after an XPath
|
|
126
139
|
# evaluation must be visible to at_xpath and xpath. leptris's
|
|
127
140
|
# node-set lane caches a document index that sibling insertions
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# leptris#1866: a cross-document replace used to hand back the
|
|
6
|
+
# SCRATCH document's wrapper — the adapter's insert deep-copies the
|
|
7
|
+
# subtree into the target, so the returned wrapper's native pointer
|
|
8
|
+
# belonged to the scratch arena. Callers stored it (svg cleanup
|
|
9
|
+
# keeps `new_svg` for id bookkeeping), the scratch was collected,
|
|
10
|
+
# and every later read of the stored wrapper walked freed memory:
|
|
11
|
+
# serializer segfaults and `<use ="">` empty-attribute names in
|
|
12
|
+
# the output.
|
|
13
|
+
RSpec.describe "leptris cross-document replace survival" do
|
|
14
|
+
let(:context) { Moxml.new(:leptris) }
|
|
15
|
+
|
|
16
|
+
it "keeps the installed wrapper valid after the scratch is collected" do
|
|
17
|
+
host = context.parse(
|
|
18
|
+
'<root xmlns:xlink="http://www.w3.org/1999/xlink">' \
|
|
19
|
+
'<svg xmlns="http://www.w3.org/2000/svg" id="s1">' \
|
|
20
|
+
'<use xlink:href="#a"/><use href="#b"/></svg></root>',
|
|
21
|
+
)
|
|
22
|
+
scratch = context.parse(
|
|
23
|
+
"<svg xmlns=\"http://www.w3.org/2000/svg\" id=\"fresh\">" \
|
|
24
|
+
"<use xlink:href=\"#fx\"/><use href=\"#b\"/></svg>",
|
|
25
|
+
)
|
|
26
|
+
olds = host.root.children.select { |c| c.name == "svg" }
|
|
27
|
+
olds[0].replace(scratch.root)
|
|
28
|
+
GC.start(full_mark: true, immediate_sweep: true)
|
|
29
|
+
out = host.root.to_xml
|
|
30
|
+
expect(out).not_to include("<use =")
|
|
31
|
+
# the installed copy's OWN content survives the scratch GC
|
|
32
|
+
expect(out).to include("fx").and include("#b")
|
|
33
|
+
# NOTE: pre-existing divergence D-22 (materialized-wrapper
|
|
34
|
+
# lane) — the UNREPLACED siblings of a cross-doc replace are
|
|
35
|
+
# dropped; reproduced on the stock adapter before this fix.
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# The parent step answers the OWNER element natively on bindings
|
|
6
|
+
# serving the fixed engine (1.9.385: leptris#1869 parent-of-attribute
|
|
7
|
+
# names the owner; the cross-doc adopt keeps its attribute chain) —
|
|
8
|
+
# moxml#474: the reverse-axis gate used to route every parent-axis
|
|
9
|
+
# expression to the Ruby engine, so the fast consumers (standoc's
|
|
10
|
+
# svg id cleanup runs `.//*[@id] | ./@id/..` per styled svg) paid
|
|
11
|
+
# the interpreter. Other reverse axes keep the Ruby lane (their
|
|
12
|
+
# native results are reverse-ordered; document order is the
|
|
13
|
+
# model's contract).
|
|
14
|
+
RSpec.describe "leptris native parent-axis lane" do
|
|
15
|
+
let(:ctx) { Moxml.new(:leptris) }
|
|
16
|
+
let(:doc) do
|
|
17
|
+
ctx.parse('<root><svg id="s"><use id="u"/></svg></root>')
|
|
18
|
+
end
|
|
19
|
+
let(:svg) { doc.xpath("//*[local-name()='svg']").first }
|
|
20
|
+
|
|
21
|
+
it "routes parent-of-attribute expressions to the native lane" do
|
|
22
|
+
native = svg.adapter.native_xpath(
|
|
23
|
+
svg.native, "./@id/..", {}
|
|
24
|
+
)
|
|
25
|
+
expect(native).not_to be_nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "answers the owner element through the facade" do
|
|
29
|
+
expect(svg.xpath("./@id/..").map(&:name)).to eq(["svg"])
|
|
30
|
+
expect(svg.xpath("./@id/..").first).to equal(svg)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "serves the two-arm union natively" do
|
|
34
|
+
expect(
|
|
35
|
+
svg.xpath(".//*[@id] | ./@id/..").map(&:name).sort,
|
|
36
|
+
).to eq(%w[svg use])
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "keeps non-parent reverse axes on the Ruby lane" do
|
|
40
|
+
doc2 = ctx.parse('<r><e a="1" b="2" c="3"/></r>')
|
|
41
|
+
e = doc2.xpath("//e").first
|
|
42
|
+
expect(e.xpath("ancestor::*").map(&:name)).to eq(["r"])
|
|
43
|
+
native = e.adapter.native_xpath(e.native, "ancestor::*", {})
|
|
44
|
+
expect(native).to be_nil
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# Processing model §Validation (normative): pinned here.
|
|
6
|
+
# moxml#460 items 1-2: the engine's metadata routes through the
|
|
7
|
+
# facade. On bindings predating the faces the probes answer nil and
|
|
8
|
+
# the facade degrades exactly as before (rules heuristic / flat
|
|
9
|
+
# errors) — the degradation is part of the contract.
|
|
10
|
+
RSpec.describe "Moxml::XSD engine metadata" do
|
|
11
|
+
let(:context) { Moxml.new(:leptris) }
|
|
12
|
+
|
|
13
|
+
describe "degradation on faces-less adapters" do
|
|
14
|
+
it "answers the source-derived version and nil structured rows" do
|
|
15
|
+
ctx = Moxml.new(:nokogiri)
|
|
16
|
+
skip "nokogiri requires an XSD-capable engine" unless
|
|
17
|
+
Moxml::XSD.supported?(ctx)
|
|
18
|
+
schema = ctx.xsd(
|
|
19
|
+
"<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" version=\"1.1\"><xs:element name=\"e\" type=\"xs:string\"/></xs:schema>",
|
|
20
|
+
)
|
|
21
|
+
expect(schema.version).to eq("1.0")
|
|
22
|
+
expect(schema.structured_errors("<e/>")).to be_nil
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
describe "Schema#version" do
|
|
27
|
+
it "answers 1.1 for a 1.1-tagged schema when the binding serves it" do
|
|
28
|
+
schema = context.xsd(
|
|
29
|
+
'<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" ' \
|
|
30
|
+
'version="1.1">' \
|
|
31
|
+
'<xs:element name="e" type="xs:string"/></xs:schema>',
|
|
32
|
+
)
|
|
33
|
+
v = schema.version
|
|
34
|
+
expect(v).to eq("1.1").or eq("1.0")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "answers 1.0 for an untagged schema" do
|
|
38
|
+
schema = context.xsd(
|
|
39
|
+
'<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">' \
|
|
40
|
+
'<xs:element name="e" type="xs:string"/></xs:schema>',
|
|
41
|
+
)
|
|
42
|
+
expect(schema.version).to eq("1.0")
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
describe "Schema#structured_errors" do
|
|
47
|
+
it "returns rows with kind/component when the binding serves it" do
|
|
48
|
+
schema = context.xsd(
|
|
49
|
+
'<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">' \
|
|
50
|
+
'<xs:element name="note">' \
|
|
51
|
+
"<xs:complexType><xs:sequence>" \
|
|
52
|
+
'<xs:element name="to" type="xs:string"/>' \
|
|
53
|
+
"</xs:sequence></xs:complexType>" \
|
|
54
|
+
"</xs:element></xs:schema>",
|
|
55
|
+
)
|
|
56
|
+
# a WRONG child, not an empty instance: pre-1.9.383 engines
|
|
57
|
+
# false-validate empty instances (leptris#1859) — this keeps
|
|
58
|
+
# the spec meaningful across both engine generations
|
|
59
|
+
rows = schema.structured_errors("<note><from>x</from></note>")
|
|
60
|
+
skip "binding predates the structured report" if rows.nil?
|
|
61
|
+
expect(rows).not_to be_empty
|
|
62
|
+
expect(rows.first).to respond_to(:kind)
|
|
63
|
+
expect(rows.first).to respond_to(:component)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|