moxml 0.5.148 → 0.5.150
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/nightly-fuzz.yml +80 -0
- 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 +88 -39
- 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 +69 -3
- data/lib/compat/opal/moxml_boot.rb +4 -0
- data/lib/moxml/adapter/base.rb +73 -0
- data/lib/moxml/adapter/leptris.rb +293 -23
- data/lib/moxml/adapter/libxml.rb +179 -25
- data/lib/moxml/adapter/nokogiri.rb +10 -0
- data/lib/moxml/adapter/oga.rb +71 -9
- data/lib/moxml/adapter/ox.rb +34 -2
- data/lib/moxml/adapter/rexml.rb +82 -17
- data/lib/moxml/c14n/data_model.rb +8 -0
- data/lib/moxml/c14n/processor.rb +17 -8
- data/lib/moxml/context.rb +33 -1
- data/lib/moxml/document.rb +18 -4
- data/lib/moxml/node.rb +75 -8
- data/lib/moxml/node_set.rb +42 -1
- data/lib/moxml/reference/normalizer.rb +63 -0
- data/lib/moxml/reference/parser.rb +417 -0
- data/lib/moxml/reference/writer.rb +121 -0
- data/lib/moxml/reference.rb +205 -0
- 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/compiler.rb +17 -2
- data/lib/moxml/xpath/ordering.rb +97 -0
- data/lib/moxml/xpath.rb +29 -0
- data/lib/moxml.rb +2 -0
- data/spec/consistency/adapter_parity_spec.rb +24 -0
- data/spec/consistency/citation_hygiene_spec.rb +30 -0
- data/spec/consistency/differential_taxonomy_spec.rb +117 -0
- data/spec/consistency/divergence_register_spec.rb +9 -1
- data/spec/consistency/eol_normalization_spec.rb +2 -0
- data/spec/consistency/loud_degradation_spec.rb +2 -0
- data/spec/consistency/metamorphic_properties_spec.rb +195 -0
- data/spec/consistency/metamorphic_spec.rb +138 -0
- data/spec/consistency/model_coverage_audit_spec.rb +144 -61
- data/spec/consistency/published_docs_spec.rb +49 -0
- data/spec/consistency/reference_differential_spec.rb +186 -0
- data/spec/consistency/xpath_idioms_spec.rb +34 -5
- data/spec/consistency/xslt_spec.rb +2 -0
- data/spec/integration/shared_examples/edge_cases.rb +0 -13
- data/spec/moxml/adapter/leptris_attribute_snapshot_spec.rb +10 -7
- data/spec/moxml/adapter/leptris_spec.rb +23 -3
- data/spec/moxml/adapter/ox_spec.rb +1 -1
- data/spec/moxml/adapter/shared_examples/adapter_contract.rb +0 -8
- data/spec/moxml/attribute_resolver_spec.rb +35 -0
- data/spec/moxml/c14n/adapter_delegation_spec.rb +35 -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/doctype_spec.rb +7 -0
- data/spec/moxml/document_parse_diagnostics_spec.rb +2 -0
- data/spec/moxml/element_indent_spec.rb +34 -0
- data/spec/moxml/entity_spec.rb +2 -0
- data/spec/moxml/node_spec.rb +0 -3
- data/spec/moxml/plan_conformance_spec.rb +2 -0
- data/spec/moxml/processing_instruction_spec.rb +9 -0
- data/spec/moxml/reference_opal_spec.rb +29 -0
- data/spec/moxml/reference_spec.rb +162 -0
- data/spec/moxml/relax_ng_spec.rb +67 -0
- data/spec/moxml/sax_spec.rb +47 -74
- data/spec/moxml/text_spec.rb +2 -0
- data/spec/moxml/to_html_spec.rb +2 -0
- data/spec/moxml/xhtml_output_spec.rb +2 -0
- data/spec/moxml/xpath/axes_spec.rb +2 -2
- data/spec/moxml/xpath_capabilities_spec.rb +11 -38
- data/spec/moxml/xsd_spec.rb +38 -0
- data/spec/support/input_generator.rb +154 -0
- data/spec/support/input_taxonomy.rb +219 -0
- data/spec/support/model_coverage.rb +159 -0
- data/spec/support/xml_generator.rb +54 -0
- metadata +32 -2
data/lib/moxml/adapter/base.rb
CHANGED
|
@@ -178,6 +178,18 @@ module Moxml
|
|
|
178
178
|
method(:sax_parse).owner != Moxml::Adapter::Base.singleton_class
|
|
179
179
|
end
|
|
180
180
|
|
|
181
|
+
# Whether the SAX engine reports an event family as distinct
|
|
182
|
+
# events. Engines that cannot must raise the named error when
|
|
183
|
+
# the handler subscribes (model, SAX: the callback stream is
|
|
184
|
+
# authoritative; absence is loud, never a silent fold).
|
|
185
|
+
#
|
|
186
|
+
# @param family [Symbol] :cdata, :comment, or
|
|
187
|
+
# :processing_instruction
|
|
188
|
+
# @return [Boolean]
|
|
189
|
+
def sax_event_family_supported?(_family)
|
|
190
|
+
true
|
|
191
|
+
end
|
|
192
|
+
|
|
181
193
|
def create_document(_native_doc = nil)
|
|
182
194
|
raise Moxml::NotImplementedError.new(
|
|
183
195
|
"create_document not implemented",
|
|
@@ -312,6 +324,30 @@ module Moxml
|
|
|
312
324
|
# validator raise; leptris gates on >= 1.9.321.0.
|
|
313
325
|
XSD_SUPPORTED = false
|
|
314
326
|
|
|
327
|
+
# RELAX NG validation (moxml RelaxNG contract): the same
|
|
328
|
+
# loud-capability rule.
|
|
329
|
+
RELAXNG_SUPPORTED = false
|
|
330
|
+
|
|
331
|
+
def relaxng_compile(_schema_source)
|
|
332
|
+
raise Moxml::NotImplementedError,
|
|
333
|
+
"RELAX NG validation requires an adapter with an RNG engine"
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def relaxng_compile_file(_path)
|
|
337
|
+
raise Moxml::NotImplementedError,
|
|
338
|
+
"RELAX NG validation requires an adapter with an RNG engine"
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def relaxng_validate_errors(_handle, _native_document)
|
|
342
|
+
raise Moxml::NotImplementedError,
|
|
343
|
+
"RELAX NG validation requires an adapter with an RNG engine"
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def relaxng_valid?(_handle, _native_document)
|
|
347
|
+
raise Moxml::NotImplementedError,
|
|
348
|
+
"RELAX NG validation requires an adapter with an RNG engine"
|
|
349
|
+
end
|
|
350
|
+
|
|
315
351
|
# Issue #384: schema handles are adapter-native objects — a
|
|
316
352
|
# handle compiled through one adapter must not dispatch
|
|
317
353
|
# through another. Adapters with an XSD engine answer their
|
|
@@ -499,6 +535,43 @@ namespace_validation_mode: :strict)
|
|
|
499
535
|
node
|
|
500
536
|
end
|
|
501
537
|
|
|
538
|
+
# Whether add_next_sibling/add_previous_sibling answer the
|
|
539
|
+
# native actually installed (libxml imports cross-document
|
|
540
|
+
# fragments and answers the copy) — the facade then refreshes
|
|
541
|
+
# the caller's wrapper to it.
|
|
542
|
+
def add_sibling_returns_inserted?
|
|
543
|
+
false
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
# Whether element-level to_xml(indent:) is honored. The
|
|
547
|
+
# facade raises a named AdapterError on incapable engines
|
|
548
|
+
# instead of returning unindented output silently (D-09).
|
|
549
|
+
def element_indent_supported?
|
|
550
|
+
false
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
# Whether adapter.at_xpath is safe as the primary first-node
|
|
554
|
+
# lane. leptris: its at_xpath falls back to the model's Ruby
|
|
555
|
+
# engine when the native lane answers nothing (an engine index
|
|
556
|
+
# left stale by sibling insertion), so it reflects the live
|
|
557
|
+
# tree where the node-set lane does not.
|
|
558
|
+
def native_at_xpath?
|
|
559
|
+
false
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
# libxml2 (nokogiri and libxml-ruby alike) rejects an empty
|
|
563
|
+
# namespace URI in an XPath binding context, but the model's
|
|
564
|
+
# `xmlns:` idiom must be able to address the no-namespace
|
|
565
|
+
# default. Adapters route such expressions through the
|
|
566
|
+
# reference engine.
|
|
567
|
+
def empty_default_namespace_binding?(namespaces)
|
|
568
|
+
return false unless namespaces
|
|
569
|
+
|
|
570
|
+
namespaces.any? do |prefix, uri|
|
|
571
|
+
prefix.to_s == "xmlns" && uri.to_s.empty?
|
|
572
|
+
end
|
|
573
|
+
end
|
|
574
|
+
|
|
502
575
|
# Whether children() results need per-child patch_node
|
|
503
576
|
# rewriting. Adapters whose natives arrive pre-wrapped (all but
|
|
504
577
|
# ox and libxml) answer false so the wrapper layer can skip
|
|
@@ -71,6 +71,13 @@ module Moxml
|
|
|
71
71
|
defined?(::Leptris::XML::NativeNode) &&
|
|
72
72
|
Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.162.6")
|
|
73
73
|
|
|
74
|
+
# leptris-ruby#460: set-text by document+address, kind-dispatched
|
|
75
|
+
# in C — no binding twin mint per write. Face-probed, not
|
|
76
|
+
# version-gated (the floor admits bindings without it).
|
|
77
|
+
NATIVE_SET_BINDING_TEXT =
|
|
78
|
+
defined?(::Leptris::XML::Native) &&
|
|
79
|
+
::Leptris::XML::Native.respond_to?(:set_binding_text)
|
|
80
|
+
|
|
74
81
|
# 1.9.163.2's native layer returns UTF-8, unfrozen strings
|
|
75
82
|
# (1.9.162.x answered ASCII-8BIT — the reads retagged with a
|
|
76
83
|
# dup+force_encoding per call).
|
|
@@ -234,6 +241,16 @@ module Moxml
|
|
|
234
241
|
|
|
235
242
|
XSD_SUPPORTED = NATIVE_XSD
|
|
236
243
|
|
|
244
|
+
# RELAX NG (moxml RelaxNG contract): the binding ships the RNG
|
|
245
|
+
# facade over the engine's Jing-compatible validator.
|
|
246
|
+
RELAXNG_SUPPORTED = begin
|
|
247
|
+
require "leptris/xml/relaxng"
|
|
248
|
+
defined?(::Leptris::XML::RelaxNG) &&
|
|
249
|
+
::Leptris::XML::RelaxNG.respond_to?(:parse_file)
|
|
250
|
+
rescue LoadError
|
|
251
|
+
false
|
|
252
|
+
end
|
|
253
|
+
|
|
237
254
|
# Schema#release (moxml#362.3, binding 1.9.342+): the
|
|
238
255
|
# deterministic drop face — disarms the AutoPointer's GC
|
|
239
256
|
# finalizer and frees now. Face-probed with the version
|
|
@@ -319,6 +336,40 @@ module Moxml
|
|
|
319
336
|
::Leptris::XML::XSD.compile_file(path)
|
|
320
337
|
end
|
|
321
338
|
|
|
339
|
+
def relaxng_compile(schema_source)
|
|
340
|
+
unless RELAXNG_SUPPORTED
|
|
341
|
+
raise Moxml::NotImplementedError,
|
|
342
|
+
"RELAX NG validation requires a leptris binding with the RNG face"
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
::Leptris::XML::RelaxNG.parse(schema_source)
|
|
346
|
+
rescue ::Leptris::XML::Error => e
|
|
347
|
+
raise Moxml::ValidationError.new(
|
|
348
|
+
"RELAX NG schema parse failed: #{e.message}",
|
|
349
|
+
)
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def relaxng_compile_file(path)
|
|
353
|
+
unless RELAXNG_SUPPORTED
|
|
354
|
+
raise Moxml::NotImplementedError,
|
|
355
|
+
"RELAX NG validation requires a leptris binding with the RNG face"
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
::Leptris::XML::RelaxNG.parse_file(path)
|
|
359
|
+
rescue ::Leptris::XML::Error => e
|
|
360
|
+
raise Moxml::ValidationError.new(
|
|
361
|
+
"RELAX NG schema parse failed: #{e.message}",
|
|
362
|
+
)
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def relaxng_validate_errors(handle, document)
|
|
366
|
+
handle.validate_errors(to_binding(document))
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def relaxng_valid?(handle, document)
|
|
370
|
+
handle.valid?(to_binding(document))
|
|
371
|
+
end
|
|
372
|
+
|
|
322
373
|
def xsd_builtin_valid?(builtin, lexical)
|
|
323
374
|
unless XSD_SUPPORTED
|
|
324
375
|
raise Moxml::NotImplementedError,
|
|
@@ -348,9 +399,13 @@ module Moxml
|
|
|
348
399
|
|
|
349
400
|
def xsd_content_valid?(xsd_handle, element_name, children)
|
|
350
401
|
# The binding reads each child's name + effective
|
|
351
|
-
# namespace through its own node API —
|
|
402
|
+
# namespace through its own node API (c_ptr) — bridge to
|
|
403
|
+
# the binding's nodes. 1.9.374+ mints registered klasses
|
|
404
|
+
# at create, so children arrive as Identity wrappers whose
|
|
405
|
+
# .native is self (no c_ptr); to_binding hands back the
|
|
406
|
+
# binding node whatever the wrapper flavor.
|
|
352
407
|
xsd_handle.content_valid?(
|
|
353
|
-
element_name, children.map(
|
|
408
|
+
element_name, children.map { |c| to_binding(c.native) }
|
|
354
409
|
)
|
|
355
410
|
end
|
|
356
411
|
|
|
@@ -525,12 +580,18 @@ module Moxml
|
|
|
525
580
|
include ::Moxml::Node
|
|
526
581
|
include ::Moxml::ProcessingInstruction
|
|
527
582
|
|
|
528
|
-
# Same nil bridge as Comment (leptris-ruby#344)
|
|
583
|
+
# Same nil bridge as Comment (leptris-ruby#344); the
|
|
584
|
+
# separating whitespace after the target is not content
|
|
585
|
+
# (uniform PI semantics, TODO.sota/02).
|
|
529
586
|
def content
|
|
530
587
|
content = native_content
|
|
531
|
-
|
|
588
|
+
if content.nil?
|
|
589
|
+
return ::Moxml::Adapter::Leptris.to_binding(self)
|
|
590
|
+
.content.to_s
|
|
591
|
+
.sub(/\A\s+/, "")
|
|
592
|
+
end
|
|
532
593
|
|
|
533
|
-
content
|
|
594
|
+
content.sub(/\A\s+/, "")
|
|
534
595
|
end
|
|
535
596
|
end
|
|
536
597
|
|
|
@@ -569,6 +630,21 @@ module Moxml
|
|
|
569
630
|
bearing
|
|
570
631
|
end
|
|
571
632
|
|
|
633
|
+
# The resolved verdict plus its generation stamp; the
|
|
634
|
+
# prewrapped traversal hands it down the tree so a fresh
|
|
635
|
+
# child skips its own document climb.
|
|
636
|
+
def doc_entity_stamp
|
|
637
|
+
doc_entity_bearing?
|
|
638
|
+
@doc_entity_stamp
|
|
639
|
+
end
|
|
640
|
+
|
|
641
|
+
def inherit_document_state(parent)
|
|
642
|
+
# The parent's own children call resolved the
|
|
643
|
+
# document-level verdict for this generation, and
|
|
644
|
+
# parent and child share the document.
|
|
645
|
+
@doc_entity_stamp = parent.doc_entity_stamp
|
|
646
|
+
end
|
|
647
|
+
|
|
572
648
|
def [](key)
|
|
573
649
|
if key.is_a?(String) && !key.include?(":")
|
|
574
650
|
value = attr_read(key)
|
|
@@ -640,12 +716,17 @@ module Moxml
|
|
|
640
716
|
if NN.method_defined?(:children_read)
|
|
641
717
|
module ChildReads
|
|
642
718
|
def children
|
|
643
|
-
|
|
644
|
-
|
|
719
|
+
ctx = @context || context
|
|
720
|
+
generation = ctx.children_generation
|
|
721
|
+
if @children_gen != generation
|
|
722
|
+
@children = nil
|
|
723
|
+
@children_gen = generation
|
|
724
|
+
end
|
|
645
725
|
@children ||= if doc_entity_bearing?
|
|
646
726
|
super
|
|
647
727
|
else
|
|
648
|
-
NodeSet.new(children_read,
|
|
728
|
+
NodeSet.new(children_read, ctx, self,
|
|
729
|
+
prewrapped: true)
|
|
649
730
|
end
|
|
650
731
|
end
|
|
651
732
|
end
|
|
@@ -1105,6 +1186,60 @@ module Moxml
|
|
|
1105
1186
|
@attachments ||= Moxml::NativeAttachment.new
|
|
1106
1187
|
end
|
|
1107
1188
|
|
|
1189
|
+
# --- engine XPath index guard ---------------------------------
|
|
1190
|
+
# Sibling insertions do not invalidate the engine's document
|
|
1191
|
+
# index that the node-set XPath lane uses, so after one the
|
|
1192
|
+
# lane answers stale results (the tree and the first-only
|
|
1193
|
+
# entry are correct; upstream bug filed). Track the state per
|
|
1194
|
+
# binding document and route XPath to the reference Ruby
|
|
1195
|
+
# engine until a child insertion rebuilds the engine index.
|
|
1196
|
+
XPATH_INDEX_STALE = :xpath_index_stale
|
|
1197
|
+
|
|
1198
|
+
def mark_xpath_index_stale(node)
|
|
1199
|
+
doc = xpath_index_document(node)
|
|
1200
|
+
attachments.set(doc, XPATH_INDEX_STALE, true) if doc
|
|
1201
|
+
end
|
|
1202
|
+
|
|
1203
|
+
def clear_xpath_index_stale(node)
|
|
1204
|
+
doc = xpath_index_document(node)
|
|
1205
|
+
attachments.set(doc, XPATH_INDEX_STALE, false) if doc
|
|
1206
|
+
end
|
|
1207
|
+
|
|
1208
|
+
def xpath_index_stale?(doc)
|
|
1209
|
+
# leptris-ruby#519 is fixed upstream: on builds whose
|
|
1210
|
+
# node-set index survives sibling insertion the stale mark
|
|
1211
|
+
# is moot, and post-insert XPath stays on the native lane.
|
|
1212
|
+
return false if native_xpath_index_fixed?
|
|
1213
|
+
|
|
1214
|
+
doc && attachments.get(doc, XPATH_INDEX_STALE)
|
|
1215
|
+
end
|
|
1216
|
+
|
|
1217
|
+
# Runs the #519 repro once at the binding level: populate the
|
|
1218
|
+
# node-set index, insert a sibling, and require the re-query
|
|
1219
|
+
# to see the inserted element. True on fixed builds, so the
|
|
1220
|
+
# guard auto-retires without a moxml release.
|
|
1221
|
+
def native_xpath_index_fixed?
|
|
1222
|
+
return @native_xpath_index_fixed unless @native_xpath_index_fixed.nil?
|
|
1223
|
+
|
|
1224
|
+
@native_xpath_index_fixed = begin
|
|
1225
|
+
native_doc = ::Leptris::XML::Document.parse("<r><a/><b/></r>")
|
|
1226
|
+
root = native_doc.root
|
|
1227
|
+
root.xpath("child::*")
|
|
1228
|
+
b = root.children.to_a[1]
|
|
1229
|
+
b.add_previous_sibling(native_doc.create_element("c"))
|
|
1230
|
+
names = root.xpath("child::*").to_a.map { |n| n.name.to_s }
|
|
1231
|
+
names == %w[a c b]
|
|
1232
|
+
rescue StandardError
|
|
1233
|
+
false
|
|
1234
|
+
end
|
|
1235
|
+
end
|
|
1236
|
+
|
|
1237
|
+
def xpath_index_document(node)
|
|
1238
|
+
return node if node.is_a?(::Leptris::XML::Document)
|
|
1239
|
+
|
|
1240
|
+
node.document
|
|
1241
|
+
end
|
|
1242
|
+
|
|
1108
1243
|
def set_root(doc, element)
|
|
1109
1244
|
to_binding(doc).root = to_binding(element)
|
|
1110
1245
|
end
|
|
@@ -1125,13 +1260,23 @@ module Moxml
|
|
|
1125
1260
|
true
|
|
1126
1261
|
end
|
|
1127
1262
|
|
|
1263
|
+
# Canonical XML excludes the initial whitespace of PI data;
|
|
1264
|
+
# the C canonicalizer keeps the raw separator when the source
|
|
1265
|
+
# used more than one whitespace character, so collapse it to
|
|
1266
|
+
# the single canonical space (libxml2 oracle). In canonical
|
|
1267
|
+
# output a literal "<?" can only start a PI: "<" is escaped in
|
|
1268
|
+
# text and attribute values, and comments/DTD are not emitted
|
|
1269
|
+
# on this lane.
|
|
1270
|
+
NATIVE_C14N_PI_SEPARATOR = /<\?([^\s?]+)[ \t\r\n]+/
|
|
1271
|
+
|
|
1128
1272
|
def native_inclusive10(native)
|
|
1129
1273
|
return nil unless native_c14n_byte_safe?
|
|
1130
1274
|
|
|
1131
|
-
to_binding(native).canonicalize(
|
|
1275
|
+
canonical = to_binding(native).canonicalize(
|
|
1132
1276
|
::Leptris::XML::FFI::C14N_1_0, nil,
|
|
1133
1277
|
mode: ::Leptris::XML::FFI::C14N_MODE_CANONICAL
|
|
1134
1278
|
)
|
|
1279
|
+
canonical.gsub(NATIVE_C14N_PI_SEPARATOR, '<?\1 ')
|
|
1135
1280
|
end
|
|
1136
1281
|
|
|
1137
1282
|
def expanded_attr_value(element, uri, local)
|
|
@@ -1174,8 +1319,44 @@ module Moxml
|
|
|
1174
1319
|
true
|
|
1175
1320
|
end
|
|
1176
1321
|
|
|
1322
|
+
# Our at_xpath falls back to the model's Ruby engine when the
|
|
1323
|
+
# native lane answers nothing — the engine's node-set XPath
|
|
1324
|
+
# index is stale after sibling insertion, so `at` must not be
|
|
1325
|
+
# routed through the node-set lane.
|
|
1326
|
+
def native_at_xpath?
|
|
1327
|
+
true
|
|
1328
|
+
end
|
|
1329
|
+
|
|
1330
|
+
# UTF-8 is the default effective encoding; a prolog declaring
|
|
1331
|
+
# another encoding hands byte validity to that encoding's lane.
|
|
1332
|
+
DECLARED_ENCODING_RE =
|
|
1333
|
+
/<\?xml\s[^>]*?encoding\s*=\s*["']([^"']+)["']/i
|
|
1334
|
+
private_constant :DECLARED_ENCODING_RE
|
|
1335
|
+
|
|
1336
|
+
def encoding_lane_owns_validity?(xml_string)
|
|
1337
|
+
# Binary-tagged head: the prolog is ASCII, and matching
|
|
1338
|
+
# never raises on binary-tagged bytes (invalid-UTF-8 input
|
|
1339
|
+
# is exactly the case this check exists for).
|
|
1340
|
+
declared = xml_string.b[0, 256].match(DECLARED_ENCODING_RE)
|
|
1341
|
+
declared &&
|
|
1342
|
+
!declared[1].match?(/\A(?:utf-?8|us-ascii|ascii)\z/i)
|
|
1343
|
+
end
|
|
1344
|
+
|
|
1177
1345
|
def parse(xml, options = {}, _context = nil)
|
|
1178
1346
|
xml_string = xml.is_a?(IO) || xml.is_a?(StringIO) ? xml.read : xml.to_s
|
|
1347
|
+
# Strict parse raises on bytes invalid in the effective
|
|
1348
|
+
# encoding (default UTF-8; declared non-UTF-8 encodings own
|
|
1349
|
+
# their validity) — the validating engines do this at load
|
|
1350
|
+
# (libxml2: "Invalid bytes in character encoding"), the
|
|
1351
|
+
# leptris engine lane does not yet (leptris-ruby#514), so
|
|
1352
|
+
# the facade names it (taxonomy differential, D-18).
|
|
1353
|
+
if options[:strict] && !xml_string.valid_encoding? &&
|
|
1354
|
+
!encoding_lane_owns_validity?(xml_string)
|
|
1355
|
+
raise Moxml::ParseError.new(
|
|
1356
|
+
"Invalid bytes in character encoding (#{xml_string.encoding})",
|
|
1357
|
+
source: xml_string[0, 100],
|
|
1358
|
+
)
|
|
1359
|
+
end
|
|
1179
1360
|
# The marker flag rides preprocess's own `&` scan — no
|
|
1180
1361
|
# second full-buffer probe (issue #132 parse-side note).
|
|
1181
1362
|
entity_mode_keep =
|
|
@@ -1213,7 +1394,11 @@ module Moxml
|
|
|
1213
1394
|
options: parse_flags(options, recover: recover,
|
|
1214
1395
|
context: _context),
|
|
1215
1396
|
)
|
|
1216
|
-
rescue ::Leptris::XML::
|
|
1397
|
+
rescue ::Leptris::XML::Error => e
|
|
1398
|
+
# Invalid encoding bytes surface as the base engine
|
|
1399
|
+
# error class, not ParseError — parse failures are all
|
|
1400
|
+
# named by the facade (taxonomy differential, malformed
|
|
1401
|
+
# class).
|
|
1217
1402
|
raise Moxml::ParseError.new(e.message) if options[:strict]
|
|
1218
1403
|
|
|
1219
1404
|
# Engines below the 1.9.344 recover floor (or the
|
|
@@ -1383,7 +1568,11 @@ module Moxml
|
|
|
1383
1568
|
else
|
|
1384
1569
|
::Leptris::XML.parse_html(html_string)
|
|
1385
1570
|
end
|
|
1386
|
-
rescue ::Leptris::XML::
|
|
1571
|
+
rescue ::Leptris::XML::Error => e
|
|
1572
|
+
# ParseError is the common class; other engine errors
|
|
1573
|
+
# (invalid encoding bytes surface as the base class) are
|
|
1574
|
+
# parse failures too — the facade names them all
|
|
1575
|
+
# (taxonomy differential, malformed class).
|
|
1387
1576
|
raise Moxml::ParseError.new(e.message)
|
|
1388
1577
|
end
|
|
1389
1578
|
attachments.set(native_doc, :entity_markers, false)
|
|
@@ -2265,9 +2454,19 @@ module Moxml
|
|
|
2265
2454
|
def attribute_pairs(element)
|
|
2266
2455
|
binding_element = to_binding(element)
|
|
2267
2456
|
return super unless binding_element.respond_to?(:attribute_pairs)
|
|
2268
|
-
return binding_element.attributes_snapshot.to_a if ELEMENT_ATTR_SNAPSHOT
|
|
2269
2457
|
|
|
2270
|
-
|
|
2458
|
+
pairs = if ELEMENT_ATTR_SNAPSHOT
|
|
2459
|
+
binding_element.attributes_snapshot.to_a
|
|
2460
|
+
else
|
|
2461
|
+
binding_element.attribute_pairs
|
|
2462
|
+
end
|
|
2463
|
+
# The engine exposes qualified names; the facade's listing
|
|
2464
|
+
# face is bare (Attribute#name), uniform with every adapter.
|
|
2465
|
+
pairs.map { |(name, value)| [local_name(name), value] }
|
|
2466
|
+
end
|
|
2467
|
+
|
|
2468
|
+
def local_name(name)
|
|
2469
|
+
name.include?(":") ? name.split(":", 2)[1] : name
|
|
2271
2470
|
end
|
|
2272
2471
|
|
|
2273
2472
|
def attribute_element(attr)
|
|
@@ -2544,6 +2743,15 @@ module Moxml
|
|
|
2544
2743
|
end
|
|
2545
2744
|
|
|
2546
2745
|
def add_child(parent, child)
|
|
2746
|
+
result = add_child_impl(parent, child)
|
|
2747
|
+
# A successful child insertion rebuilds the engine's
|
|
2748
|
+
# document index; the upstream sibling-insert bug leaves it
|
|
2749
|
+
# stale (see native_xpath), so a rebuild clears the guard.
|
|
2750
|
+
clear_xpath_index_stale(parent) if result
|
|
2751
|
+
result
|
|
2752
|
+
end
|
|
2753
|
+
|
|
2754
|
+
def add_child_impl(parent, child)
|
|
2547
2755
|
# Same-document Identity append — the builder hot path. One
|
|
2548
2756
|
# document pair decides the verdict: the pin, the absorb
|
|
2549
2757
|
# checks and the binding bridges only exist for
|
|
@@ -2656,6 +2864,12 @@ module Moxml
|
|
|
2656
2864
|
end
|
|
2657
2865
|
|
|
2658
2866
|
def add_previous_sibling(node, new_node)
|
|
2867
|
+
result = add_previous_sibling_impl(node, new_node)
|
|
2868
|
+
mark_xpath_index_stale(node)
|
|
2869
|
+
result
|
|
2870
|
+
end
|
|
2871
|
+
|
|
2872
|
+
def add_previous_sibling_impl(node, new_node)
|
|
2659
2873
|
if NATIVE_READ_LAYER
|
|
2660
2874
|
node = to_binding(node)
|
|
2661
2875
|
new_node = to_binding(new_node)
|
|
@@ -2696,6 +2910,12 @@ module Moxml
|
|
|
2696
2910
|
end
|
|
2697
2911
|
|
|
2698
2912
|
def add_next_sibling(node, new_node)
|
|
2913
|
+
result = add_next_sibling_impl(node, new_node)
|
|
2914
|
+
mark_xpath_index_stale(node)
|
|
2915
|
+
result
|
|
2916
|
+
end
|
|
2917
|
+
|
|
2918
|
+
def add_next_sibling_impl(node, new_node)
|
|
2699
2919
|
if NATIVE_READ_LAYER
|
|
2700
2920
|
node = to_binding(node)
|
|
2701
2921
|
new_node = to_binding(new_node)
|
|
@@ -2910,6 +3130,18 @@ module Moxml
|
|
|
2910
3130
|
end
|
|
2911
3131
|
|
|
2912
3132
|
def set_text_content(node, content)
|
|
3133
|
+
# Native-layer fast path (leptris-ruby#460): the C setter
|
|
3134
|
+
# takes document+address directly — no binding twin mint
|
|
3135
|
+
# per write. The Identity wrapper's own content= would
|
|
3136
|
+
# re-enter the facade, so call the module face.
|
|
3137
|
+
if NATIVE_SET_BINDING_TEXT && node.is_a?(::Leptris::XML::NativeNode) &&
|
|
3138
|
+
(doc = doc_for(node))
|
|
3139
|
+
::Leptris::XML::Native.set_binding_text(
|
|
3140
|
+
doc, node.address, content.to_s
|
|
3141
|
+
)
|
|
3142
|
+
return
|
|
3143
|
+
end
|
|
3144
|
+
|
|
2913
3145
|
node = to_binding(node) if NATIVE_READ_LAYER
|
|
2914
3146
|
case node
|
|
2915
3147
|
when ::Leptris::XML::Document
|
|
@@ -2959,16 +3191,20 @@ module Moxml
|
|
|
2959
3191
|
end
|
|
2960
3192
|
|
|
2961
3193
|
def processing_instruction_content(node)
|
|
3194
|
+
# The model's PI content excludes the separating whitespace
|
|
3195
|
+
# after the target (Nokogiri semantics; uniform across
|
|
3196
|
+
# adapters — TODO.sota/02).
|
|
2962
3197
|
if NATIVE_READ_LAYER && node.is_a?(NN)
|
|
2963
3198
|
content = NN_CONTENT.bind_call(node)
|
|
2964
3199
|
# NativeNode#content answers nil for PI-kind nodes too
|
|
2965
3200
|
# (leptris-ruby#344 covers comments); bridge for the data.
|
|
2966
|
-
return to_binding(node).content if
|
|
3201
|
+
return to_binding(node).content.to_s.sub(/\A\s+/, "") if
|
|
3202
|
+
content.nil?
|
|
2967
3203
|
|
|
2968
|
-
return content
|
|
3204
|
+
return content.sub(/\A\s+/, "")
|
|
2969
3205
|
end
|
|
2970
3206
|
|
|
2971
|
-
node.content
|
|
3207
|
+
node.content.to_s.sub(/\A\s+/, "")
|
|
2972
3208
|
end
|
|
2973
3209
|
|
|
2974
3210
|
def set_processing_instruction_content(node, content)
|
|
@@ -3104,6 +3340,12 @@ module Moxml
|
|
|
3104
3340
|
# namespace-bound queries to the Ruby engine until the
|
|
3105
3341
|
# engine grows correct prefix resolution.
|
|
3106
3342
|
return nil if namespaces && !namespaces.empty?
|
|
3343
|
+
# Reverse axes answer reverse-ordered from the native
|
|
3344
|
+
# engine; route them to the Ruby engine (document order).
|
|
3345
|
+
return nil if REVERSE_AXIS_CACHE.get_or_set(expression) do
|
|
3346
|
+
expression_uses_reverse_axis?(expression)
|
|
3347
|
+
end
|
|
3348
|
+
|
|
3107
3349
|
# The binding reports the root element parentless while moxml
|
|
3108
3350
|
# roots it at the document, so parent-axis queries from the
|
|
3109
3351
|
# root element keep the Ruby engine.
|
|
@@ -3119,6 +3361,9 @@ module Moxml
|
|
|
3119
3361
|
::Leptris::XML::XPath.compile(expression)
|
|
3120
3362
|
end
|
|
3121
3363
|
document = node.is_a?(::Leptris::XML::Document) ? node : node.document
|
|
3364
|
+
# A sibling insertion left the engine index stale; the
|
|
3365
|
+
# reference engine walks the live tree (see the guard above).
|
|
3366
|
+
return nil if xpath_index_stale?(document)
|
|
3122
3367
|
|
|
3123
3368
|
if namespaces && !namespaces.empty?
|
|
3124
3369
|
# Namespace-bound evaluation has no raw-pointer entry —
|
|
@@ -3190,6 +3435,14 @@ module Moxml
|
|
|
3190
3435
|
node.is_a?(::Leptris::XML::Element)
|
|
3191
3436
|
end
|
|
3192
3437
|
|
|
3438
|
+
# Reverse axes answer in reverse document order from the
|
|
3439
|
+
# native engine; XPath path results are document-ordered (the
|
|
3440
|
+
# model's order contract — TODO.sota/02, M3), so these
|
|
3441
|
+
# expressions route to the Ruby engine, which orders them.
|
|
3442
|
+
REVERSE_AXES = %w[ancestor ancestor-or-self preceding
|
|
3443
|
+
preceding-sibling parent].freeze
|
|
3444
|
+
REVERSE_AXIS_CACHE = XPath::Cache.new(200)
|
|
3445
|
+
|
|
3193
3446
|
def expression_uses_parent_axis?(expression)
|
|
3194
3447
|
ast = XPath::Parser.parse_with_cache(expression)
|
|
3195
3448
|
contains_parent_axis?(ast)
|
|
@@ -3197,6 +3450,24 @@ module Moxml
|
|
|
3197
3450
|
true
|
|
3198
3451
|
end
|
|
3199
3452
|
|
|
3453
|
+
def expression_uses_reverse_axis?(expression)
|
|
3454
|
+
ast = XPath::Parser.parse_with_cache(expression)
|
|
3455
|
+
contains_reverse_axis?(ast)
|
|
3456
|
+
rescue XPath::SyntaxError
|
|
3457
|
+
false
|
|
3458
|
+
end
|
|
3459
|
+
|
|
3460
|
+
def contains_reverse_axis?(ast)
|
|
3461
|
+
return true if ast.type == :parent
|
|
3462
|
+
if ast.type == :axis && REVERSE_AXES.include?(ast.children.first)
|
|
3463
|
+
return true
|
|
3464
|
+
end
|
|
3465
|
+
|
|
3466
|
+
ast.children.any? do |child|
|
|
3467
|
+
child.is_a?(XPath::AST::Node) && contains_reverse_axis?(child)
|
|
3468
|
+
end
|
|
3469
|
+
end
|
|
3470
|
+
|
|
3200
3471
|
def contains_parent_axis?(ast)
|
|
3201
3472
|
return true if ast.type == :parent
|
|
3202
3473
|
return true if ast.type == :axis && ast.children.first == "parent"
|
|
@@ -3366,13 +3637,12 @@ module Moxml
|
|
|
3366
3637
|
|
|
3367
3638
|
def record_source_declaration(native_doc, xml_string)
|
|
3368
3639
|
head = xml_string[0, 256]
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
end
|
|
3640
|
+
# invalid bytes never reach regexes: named parse errors own
|
|
3641
|
+
# malformed input (taxonomy differential, malformed class)
|
|
3642
|
+
had = xml_string.valid_encoding? &&
|
|
3643
|
+
(head.match?(SOURCE_DECLARATION_RE) ||
|
|
3644
|
+
(head.match?(/\A\s*\z/) &&
|
|
3645
|
+
xml_string.match?(SOURCE_DECLARATION_RE)))
|
|
3376
3646
|
attachments.set(native_doc, :had_source_declaration, had)
|
|
3377
3647
|
end
|
|
3378
3648
|
end
|