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
|
@@ -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,
|
|
@@ -579,6 +630,21 @@ module Moxml
|
|
|
579
630
|
bearing
|
|
580
631
|
end
|
|
581
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
|
+
|
|
582
648
|
def [](key)
|
|
583
649
|
if key.is_a?(String) && !key.include?(":")
|
|
584
650
|
value = attr_read(key)
|
|
@@ -650,12 +716,17 @@ module Moxml
|
|
|
650
716
|
if NN.method_defined?(:children_read)
|
|
651
717
|
module ChildReads
|
|
652
718
|
def children
|
|
653
|
-
|
|
654
|
-
|
|
719
|
+
ctx = @context || context
|
|
720
|
+
generation = ctx.children_generation
|
|
721
|
+
if @children_gen != generation
|
|
722
|
+
@children = nil
|
|
723
|
+
@children_gen = generation
|
|
724
|
+
end
|
|
655
725
|
@children ||= if doc_entity_bearing?
|
|
656
726
|
super
|
|
657
727
|
else
|
|
658
|
-
NodeSet.new(children_read,
|
|
728
|
+
NodeSet.new(children_read, ctx, self,
|
|
729
|
+
prewrapped: true)
|
|
659
730
|
end
|
|
660
731
|
end
|
|
661
732
|
end
|
|
@@ -1115,6 +1186,72 @@ module Moxml
|
|
|
1115
1186
|
@attachments ||= Moxml::NativeAttachment.new
|
|
1116
1187
|
end
|
|
1117
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
|
+
#
|
|
1197
|
+
# FIXED upstream in libleptris 1.9.378 (leptris/leptris#1802):
|
|
1198
|
+
# every splice path invalidates the index, so the guard is
|
|
1199
|
+
# dead weight on modern engines — and it sat on the builder
|
|
1200
|
+
# hot path (one Monitor-synchronized attachment set plus a
|
|
1201
|
+
# document hop per add_child). Retirement is probed, not
|
|
1202
|
+
# version-gated: any build that passes the #519 repro opts
|
|
1203
|
+
# itself out, no moxml release needed.
|
|
1204
|
+
XPATH_INDEX_STALE = :xpath_index_stale
|
|
1205
|
+
|
|
1206
|
+
def mark_xpath_index_stale(node)
|
|
1207
|
+
return if native_xpath_index_fixed?
|
|
1208
|
+
|
|
1209
|
+
doc = xpath_index_document(node)
|
|
1210
|
+
attachments.set(doc, XPATH_INDEX_STALE, true) if doc
|
|
1211
|
+
end
|
|
1212
|
+
|
|
1213
|
+
def clear_xpath_index_stale(node)
|
|
1214
|
+
return if native_xpath_index_fixed?
|
|
1215
|
+
|
|
1216
|
+
doc = xpath_index_document(node)
|
|
1217
|
+
attachments.set(doc, XPATH_INDEX_STALE, false) if doc
|
|
1218
|
+
end
|
|
1219
|
+
|
|
1220
|
+
def xpath_index_stale?(doc)
|
|
1221
|
+
# leptris-ruby#519 is fixed upstream: on builds whose
|
|
1222
|
+
# node-set index survives sibling insertion the stale mark
|
|
1223
|
+
# is moot, and post-insert XPath stays on the native lane.
|
|
1224
|
+
return false if native_xpath_index_fixed?
|
|
1225
|
+
|
|
1226
|
+
doc && attachments.get(doc, XPATH_INDEX_STALE)
|
|
1227
|
+
end
|
|
1228
|
+
|
|
1229
|
+
# Runs the #519 repro once at the binding level: populate the
|
|
1230
|
+
# node-set index, insert a sibling, and require the re-query
|
|
1231
|
+
# to see the inserted element. True on fixed builds, so the
|
|
1232
|
+
# guard auto-retires without a moxml release.
|
|
1233
|
+
def native_xpath_index_fixed?
|
|
1234
|
+
return @native_xpath_index_fixed unless @native_xpath_index_fixed.nil?
|
|
1235
|
+
|
|
1236
|
+
@native_xpath_index_fixed = begin
|
|
1237
|
+
native_doc = ::Leptris::XML::Document.parse("<r><a/><b/></r>")
|
|
1238
|
+
root = native_doc.root
|
|
1239
|
+
root.xpath("child::*")
|
|
1240
|
+
b = root.children.to_a[1]
|
|
1241
|
+
b.add_previous_sibling(native_doc.create_element("c"))
|
|
1242
|
+
names = root.xpath("child::*").to_a.map { |n| n.name.to_s }
|
|
1243
|
+
names == %w[a c b]
|
|
1244
|
+
rescue StandardError
|
|
1245
|
+
false
|
|
1246
|
+
end
|
|
1247
|
+
end
|
|
1248
|
+
|
|
1249
|
+
def xpath_index_document(node)
|
|
1250
|
+
return node if node.is_a?(::Leptris::XML::Document)
|
|
1251
|
+
|
|
1252
|
+
node.document
|
|
1253
|
+
end
|
|
1254
|
+
|
|
1118
1255
|
def set_root(doc, element)
|
|
1119
1256
|
to_binding(doc).root = to_binding(element)
|
|
1120
1257
|
end
|
|
@@ -1135,13 +1272,23 @@ module Moxml
|
|
|
1135
1272
|
true
|
|
1136
1273
|
end
|
|
1137
1274
|
|
|
1275
|
+
# Canonical XML excludes the initial whitespace of PI data;
|
|
1276
|
+
# the C canonicalizer keeps the raw separator when the source
|
|
1277
|
+
# used more than one whitespace character, so collapse it to
|
|
1278
|
+
# the single canonical space (libxml2 oracle). In canonical
|
|
1279
|
+
# output a literal "<?" can only start a PI: "<" is escaped in
|
|
1280
|
+
# text and attribute values, and comments/DTD are not emitted
|
|
1281
|
+
# on this lane.
|
|
1282
|
+
NATIVE_C14N_PI_SEPARATOR = /<\?([^\s?]+)[ \t\r\n]+/
|
|
1283
|
+
|
|
1138
1284
|
def native_inclusive10(native)
|
|
1139
1285
|
return nil unless native_c14n_byte_safe?
|
|
1140
1286
|
|
|
1141
|
-
to_binding(native).canonicalize(
|
|
1287
|
+
canonical = to_binding(native).canonicalize(
|
|
1142
1288
|
::Leptris::XML::FFI::C14N_1_0, nil,
|
|
1143
1289
|
mode: ::Leptris::XML::FFI::C14N_MODE_CANONICAL
|
|
1144
1290
|
)
|
|
1291
|
+
canonical.gsub(NATIVE_C14N_PI_SEPARATOR, '<?\1 ')
|
|
1145
1292
|
end
|
|
1146
1293
|
|
|
1147
1294
|
def expanded_attr_value(element, uri, local)
|
|
@@ -1184,6 +1331,14 @@ module Moxml
|
|
|
1184
1331
|
true
|
|
1185
1332
|
end
|
|
1186
1333
|
|
|
1334
|
+
# Our at_xpath falls back to the model's Ruby engine when the
|
|
1335
|
+
# native lane answers nothing — the engine's node-set XPath
|
|
1336
|
+
# index is stale after sibling insertion, so `at` must not be
|
|
1337
|
+
# routed through the node-set lane.
|
|
1338
|
+
def native_at_xpath?
|
|
1339
|
+
true
|
|
1340
|
+
end
|
|
1341
|
+
|
|
1187
1342
|
# UTF-8 is the default effective encoding; a prolog declaring
|
|
1188
1343
|
# another encoding hands byte validity to that encoding's lane.
|
|
1189
1344
|
DECLARED_ENCODING_RE =
|
|
@@ -1619,6 +1774,23 @@ module Moxml
|
|
|
1619
1774
|
(owner_doc || create_document).create_element(name.to_s)
|
|
1620
1775
|
end
|
|
1621
1776
|
|
|
1777
|
+
# MINT_PRIMED: the compiled ext's create_element takes
|
|
1778
|
+
# optional (context, adapter) and pre-writes the contract
|
|
1779
|
+
# ivars in C — arity -1 vs the old 2. Self-gating: no
|
|
1780
|
+
# version floor, the probe reads the loaded ext.
|
|
1781
|
+
MINT_PRIMED = defined?(::Leptris::XML::NativeNode) &&
|
|
1782
|
+
::Leptris::XML::NativeNode
|
|
1783
|
+
.method(:create_element).arity == -1
|
|
1784
|
+
|
|
1785
|
+
def mints_primed?
|
|
1786
|
+
MINT_PRIMED
|
|
1787
|
+
end
|
|
1788
|
+
|
|
1789
|
+
def create_primed_element(name, owner_doc:, context:, adapter:)
|
|
1790
|
+
validate_element_name(name)
|
|
1791
|
+
owner_doc.native_create_element(name.to_s, context, adapter)
|
|
1792
|
+
end
|
|
1793
|
+
|
|
1622
1794
|
def create_native_text(content, owner_doc = nil)
|
|
1623
1795
|
if NATIVE_MUTATIONS_COHERENT && owner_doc
|
|
1624
1796
|
return owner_doc.native_create_text(content)
|
|
@@ -2265,6 +2437,7 @@ module Moxml
|
|
|
2265
2437
|
# callers fall back to create_element + set_attribute +
|
|
2266
2438
|
# add_child when absent.
|
|
2267
2439
|
def create_element_with_attrs(parent, name, attrs)
|
|
2440
|
+
validate_element_name(name)
|
|
2268
2441
|
binding_parent = to_binding(parent)
|
|
2269
2442
|
doc = binding_parent.document or return nil
|
|
2270
2443
|
return nil unless ::Leptris::XML::Native.respond_to?(:create_element_with_attrs)
|
|
@@ -2600,6 +2773,15 @@ module Moxml
|
|
|
2600
2773
|
end
|
|
2601
2774
|
|
|
2602
2775
|
def add_child(parent, child)
|
|
2776
|
+
result = add_child_impl(parent, child)
|
|
2777
|
+
# A successful child insertion rebuilds the engine's
|
|
2778
|
+
# document index; the upstream sibling-insert bug leaves it
|
|
2779
|
+
# stale (see native_xpath), so a rebuild clears the guard.
|
|
2780
|
+
clear_xpath_index_stale(parent) if result
|
|
2781
|
+
result
|
|
2782
|
+
end
|
|
2783
|
+
|
|
2784
|
+
def add_child_impl(parent, child)
|
|
2603
2785
|
# Same-document Identity append — the builder hot path. One
|
|
2604
2786
|
# document pair decides the verdict: the pin, the absorb
|
|
2605
2787
|
# checks and the binding bridges only exist for
|
|
@@ -2712,6 +2894,12 @@ module Moxml
|
|
|
2712
2894
|
end
|
|
2713
2895
|
|
|
2714
2896
|
def add_previous_sibling(node, new_node)
|
|
2897
|
+
result = add_previous_sibling_impl(node, new_node)
|
|
2898
|
+
mark_xpath_index_stale(node)
|
|
2899
|
+
result
|
|
2900
|
+
end
|
|
2901
|
+
|
|
2902
|
+
def add_previous_sibling_impl(node, new_node)
|
|
2715
2903
|
if NATIVE_READ_LAYER
|
|
2716
2904
|
node = to_binding(node)
|
|
2717
2905
|
new_node = to_binding(new_node)
|
|
@@ -2752,6 +2940,12 @@ module Moxml
|
|
|
2752
2940
|
end
|
|
2753
2941
|
|
|
2754
2942
|
def add_next_sibling(node, new_node)
|
|
2943
|
+
result = add_next_sibling_impl(node, new_node)
|
|
2944
|
+
mark_xpath_index_stale(node)
|
|
2945
|
+
result
|
|
2946
|
+
end
|
|
2947
|
+
|
|
2948
|
+
def add_next_sibling_impl(node, new_node)
|
|
2755
2949
|
if NATIVE_READ_LAYER
|
|
2756
2950
|
node = to_binding(node)
|
|
2757
2951
|
new_node = to_binding(new_node)
|
|
@@ -2965,7 +3159,44 @@ module Moxml
|
|
|
2965
3159
|
end.join
|
|
2966
3160
|
end
|
|
2967
3161
|
|
|
3162
|
+
# SET_TEXT_NATIVE: bindings with the unshadowed C alias
|
|
3163
|
+
# (the add_child_native pattern — set_text_native aliases
|
|
3164
|
+
# content= on the TypedData self) take the one-frame write:
|
|
3165
|
+
# no doc_for climb, no address read, no module dispatch.
|
|
3166
|
+
# Self-gating by respond_to? — older exts fall through to
|
|
3167
|
+
# the set_binding_text face.
|
|
3168
|
+
SET_TEXT_NATIVE = defined?(::Leptris::XML::NativeNode) &&
|
|
3169
|
+
::Leptris::XML::NativeNode.method_defined?(:set_text_native)
|
|
3170
|
+
|
|
2968
3171
|
def set_text_content(node, content)
|
|
3172
|
+
if SET_TEXT_NATIVE && node.is_a?(::Leptris::XML::NativeNode)
|
|
3173
|
+
begin
|
|
3174
|
+
node.set_text_native(content.to_s)
|
|
3175
|
+
return
|
|
3176
|
+
rescue ::Leptris::XML::Error => e
|
|
3177
|
+
# Same #401 contract as the fallback below: a detached
|
|
3178
|
+
# (parentless) node re-raises as the named Moxml::Error.
|
|
3179
|
+
raise if node.parent
|
|
3180
|
+
|
|
3181
|
+
raise Moxml::Error,
|
|
3182
|
+
"cannot set text content on a detached " \
|
|
3183
|
+
"(parentless) node — attach it to a document " \
|
|
3184
|
+
"first (issue #401): #{e.message}"
|
|
3185
|
+
end
|
|
3186
|
+
end
|
|
3187
|
+
|
|
3188
|
+
# Native-layer fast path (leptris-ruby#460): the C setter
|
|
3189
|
+
# takes document+address directly — no binding twin mint
|
|
3190
|
+
# per write. The Identity wrapper's own content= would
|
|
3191
|
+
# re-enter the facade, so call the module face.
|
|
3192
|
+
if NATIVE_SET_BINDING_TEXT && node.is_a?(::Leptris::XML::NativeNode) &&
|
|
3193
|
+
(doc = doc_for(node))
|
|
3194
|
+
::Leptris::XML::Native.set_binding_text(
|
|
3195
|
+
doc, node.address, content.to_s
|
|
3196
|
+
)
|
|
3197
|
+
return
|
|
3198
|
+
end
|
|
3199
|
+
|
|
2969
3200
|
node = to_binding(node) if NATIVE_READ_LAYER
|
|
2970
3201
|
case node
|
|
2971
3202
|
when ::Leptris::XML::Document
|
|
@@ -3185,6 +3416,9 @@ module Moxml
|
|
|
3185
3416
|
::Leptris::XML::XPath.compile(expression)
|
|
3186
3417
|
end
|
|
3187
3418
|
document = node.is_a?(::Leptris::XML::Document) ? node : node.document
|
|
3419
|
+
# A sibling insertion left the engine index stale; the
|
|
3420
|
+
# reference engine walks the live tree (see the guard above).
|
|
3421
|
+
return nil if xpath_index_stale?(document)
|
|
3188
3422
|
|
|
3189
3423
|
if namespaces && !namespaces.empty?
|
|
3190
3424
|
# Namespace-bound evaluation has no raw-pointer entry —
|
data/lib/moxml/adapter/libxml.rb
CHANGED
|
@@ -87,6 +87,10 @@ module Moxml
|
|
|
87
87
|
false
|
|
88
88
|
end
|
|
89
89
|
|
|
90
|
+
def add_sibling_returns_inserted?
|
|
91
|
+
true
|
|
92
|
+
end
|
|
93
|
+
|
|
90
94
|
# Linear DOCTYPE prolog scanner. Returns
|
|
91
95
|
# {name:, external_id:, system_id:, internal_subset:} or nil.
|
|
92
96
|
# Single pass, quote- and bracket-aware: no backtracking, so
|
|
@@ -780,7 +784,8 @@ module Moxml
|
|
|
780
784
|
return unless node && sibling
|
|
781
785
|
|
|
782
786
|
native_node = unpatch_node(node)
|
|
783
|
-
native_sibling =
|
|
787
|
+
native_sibling = import_for_attachment(native_node,
|
|
788
|
+
unpatch_node(sibling))
|
|
784
789
|
|
|
785
790
|
# Special handling for document-level processing instructions
|
|
786
791
|
# When adding a PI as sibling to root element, store it on document
|
|
@@ -794,14 +799,35 @@ module Moxml
|
|
|
794
799
|
end
|
|
795
800
|
|
|
796
801
|
native_node.prev = native_sibling
|
|
802
|
+
native_sibling
|
|
797
803
|
end
|
|
798
804
|
|
|
799
805
|
def add_next_sibling(node, sibling)
|
|
800
806
|
return unless node && sibling
|
|
801
807
|
|
|
802
808
|
native_node = unpatch_node(node)
|
|
803
|
-
native_sibling =
|
|
809
|
+
native_sibling = import_for_attachment(native_node,
|
|
810
|
+
unpatch_node(sibling))
|
|
804
811
|
native_node.next = native_sibling
|
|
812
|
+
native_sibling
|
|
813
|
+
end
|
|
814
|
+
|
|
815
|
+
# A markup fragment parsed by the facade lives in its own
|
|
816
|
+
# document; libxml refuses cross-document attachment. Import
|
|
817
|
+
# (deep-copy) into the target document and hand back the copy —
|
|
818
|
+
# the caller refreshes its wrapper to this native.
|
|
819
|
+
def import_for_attachment(native_node, native_sibling)
|
|
820
|
+
return native_sibling unless
|
|
821
|
+
native_node.is_a?(::LibXML::XML::Node) &&
|
|
822
|
+
native_sibling.is_a?(::LibXML::XML::Node)
|
|
823
|
+
|
|
824
|
+
target = native_node.doc
|
|
825
|
+
return native_sibling if target.nil?
|
|
826
|
+
|
|
827
|
+
source = native_sibling.doc
|
|
828
|
+
return native_sibling if source.equal?(target)
|
|
829
|
+
|
|
830
|
+
target.import(native_sibling)
|
|
805
831
|
end
|
|
806
832
|
|
|
807
833
|
def remove(node)
|
|
@@ -1066,6 +1092,12 @@ module Moxml
|
|
|
1066
1092
|
native_node = unpatch_node(node)
|
|
1067
1093
|
return [] unless native_node
|
|
1068
1094
|
|
|
1095
|
+
if empty_default_namespace_binding?(namespaces)
|
|
1096
|
+
return Moxml::XPath.evaluate_natives(native_node, expression,
|
|
1097
|
+
namespaces,
|
|
1098
|
+
adapter: :libxml)
|
|
1099
|
+
end
|
|
1100
|
+
|
|
1069
1101
|
# libxml-ruby's Document#find returns EMPTY for relative
|
|
1070
1102
|
# paths (nokogiri/libxml2 evaluate them fine): a relative
|
|
1071
1103
|
# expression from the document node is equivalent to the
|
|
@@ -58,6 +58,10 @@ module Moxml
|
|
|
58
58
|
true
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
+
def element_indent_supported?
|
|
62
|
+
true
|
|
63
|
+
end
|
|
64
|
+
|
|
61
65
|
# --- XSLT (Moxml::XSLT contract, libxslt-backed) ---
|
|
62
66
|
|
|
63
67
|
def xslt_compile(sheet_source)
|
|
@@ -646,6 +650,12 @@ module Moxml
|
|
|
646
650
|
end
|
|
647
651
|
|
|
648
652
|
def xpath(node, expression, namespaces = nil)
|
|
653
|
+
if empty_default_namespace_binding?(namespaces)
|
|
654
|
+
return Moxml::XPath.evaluate_natives(node, expression,
|
|
655
|
+
namespaces,
|
|
656
|
+
adapter: :nokogiri)
|
|
657
|
+
end
|
|
658
|
+
|
|
649
659
|
# An explicit bindings hash — even EMPTY — makes nokogiri
|
|
650
660
|
# register a prefix context, which disables libxml2's
|
|
651
661
|
# literal xmlns: default-namespace handling. Only pass the
|
data/lib/moxml/adapter/oga.rb
CHANGED
|
@@ -28,8 +28,23 @@ module Moxml
|
|
|
28
28
|
doc.children << element
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
# The UTF-8 BOM is an encoding signature, not content (model,
|
|
32
|
+
# Parsing); Oga otherwise leaves it in the tree as a
|
|
33
|
+
# document-level text node.
|
|
34
|
+
def strip_bom(xml)
|
|
35
|
+
return xml unless xml.is_a?(String)
|
|
36
|
+
|
|
37
|
+
if defined?(Encoding) && xml.encoding == Encoding::BINARY
|
|
38
|
+
xml.byteslice(0, 3) == "\xEF\xBB\xBF".b ? xml.byteslice(3..) : xml
|
|
39
|
+
elsif xml.start_with?("")
|
|
40
|
+
xml[1..]
|
|
41
|
+
else
|
|
42
|
+
xml
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
31
46
|
def parse(xml, options = {}, _context = nil)
|
|
32
|
-
processed_xml = preprocess_entities(xml)
|
|
47
|
+
processed_xml = strip_bom(preprocess_entities(xml))
|
|
33
48
|
|
|
34
49
|
native_doc = begin
|
|
35
50
|
::Oga.parse_xml(processed_xml, strict: options[:strict])
|
|
@@ -203,7 +218,15 @@ module Moxml
|
|
|
203
218
|
|
|
204
219
|
def namespace(element)
|
|
205
220
|
case element
|
|
206
|
-
when ::Oga::XML::
|
|
221
|
+
when ::Oga::XML::Attribute
|
|
222
|
+
# Unprefixed attributes are in no namespace, always
|
|
223
|
+
# (Namespaces 1.0 §5.2). Oga resolves them against the
|
|
224
|
+
# element's default namespace, which is wrong and makes
|
|
225
|
+
# bare-name reads miss under a default namespace.
|
|
226
|
+
return nil if element.namespace_name.nil?
|
|
227
|
+
|
|
228
|
+
element.namespace
|
|
229
|
+
when ::Oga::XML::Element
|
|
207
230
|
element.namespace
|
|
208
231
|
end
|
|
209
232
|
rescue NoMethodError
|
|
@@ -247,10 +270,10 @@ module Moxml
|
|
|
247
270
|
def children(node, **_entity_bearing)
|
|
248
271
|
all_children = []
|
|
249
272
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
273
|
+
# The XML declaration is document state, not a child node
|
|
274
|
+
# (model, Object model): it never appears in children listings.
|
|
275
|
+
# The doctype follows the Nokogiri contract and is listed.
|
|
276
|
+
all_children << node.doctype if node.is_a?(::Oga::XML::Document) && node.doctype
|
|
254
277
|
|
|
255
278
|
return all_children unless node.is_a?(::Oga::XML::Node) || node.is_a?(::Oga::XML::Document)
|
|
256
279
|
|
|
@@ -513,7 +536,7 @@ module Moxml
|
|
|
513
536
|
def processing_instruction_content(node)
|
|
514
537
|
# Oga keeps the target/content separator space inside the
|
|
515
538
|
# PI's text; the model's content is the data AFTER the
|
|
516
|
-
# separator
|
|
539
|
+
# separator. Separator whitespace is parser syntax, and
|
|
517
540
|
# leaving it in makes canonicalization non-idempotent (the
|
|
518
541
|
# serializer re-adds the separator each round — TODO.sota/02,
|
|
519
542
|
# M2).
|
|
@@ -565,6 +588,15 @@ module Moxml
|
|
|
565
588
|
end
|
|
566
589
|
|
|
567
590
|
def xpath(node, expression, namespaces = nil)
|
|
591
|
+
# oga's engine has no xmlns: default-namespace idiom
|
|
592
|
+
# (D-01); the reference engine implements it (model,
|
|
593
|
+
# Names and namespaces).
|
|
594
|
+
if expression.include?("xmlns:")
|
|
595
|
+
return Moxml::XPath.evaluate_natives(
|
|
596
|
+
node, expression, namespaces || {}, adapter: :oga
|
|
597
|
+
)
|
|
598
|
+
end
|
|
599
|
+
|
|
568
600
|
result = node.xpath(expression, {},
|
|
569
601
|
namespaces: namespaces&.transform_keys(&:to_s))
|
|
570
602
|
# Adapter contract: Array<native> | scalar. Oga's engine
|
data/lib/moxml/adapter/ox.rb
CHANGED
|
@@ -108,6 +108,14 @@ module Moxml
|
|
|
108
108
|
end
|
|
109
109
|
end
|
|
110
110
|
|
|
111
|
+
# Ox's SAX parser reports text only: CDATA, comments and PIs
|
|
112
|
+
# are folded into text or dropped (D-10). Subscribing to those
|
|
113
|
+
# families raises the named error instead of silently answering
|
|
114
|
+
# an incomplete stream.
|
|
115
|
+
def sax_event_family_supported?(family)
|
|
116
|
+
!%i[cdata comment processing_instruction].include?(family)
|
|
117
|
+
end
|
|
118
|
+
|
|
111
119
|
def create_document(native_doc = nil)
|
|
112
120
|
attrs = native_doc&.attributes || {}
|
|
113
121
|
::Ox::Document.new(**attrs)
|
data/lib/moxml/adapter/rexml.rb
CHANGED
|
@@ -22,6 +22,10 @@ module Moxml
|
|
|
22
22
|
true
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
def element_indent_supported?
|
|
26
|
+
true
|
|
27
|
+
end
|
|
28
|
+
|
|
25
29
|
def parse(xml, options = {}, _context = nil)
|
|
26
30
|
xml = "" if xml.nil?
|
|
27
31
|
|
|
@@ -215,8 +219,10 @@ module Moxml
|
|
|
215
219
|
def children(node, **_entity_bearing)
|
|
216
220
|
return [] unless node.is_a?(::REXML::Parent)
|
|
217
221
|
|
|
218
|
-
# Return all children preserving whitespace text nodes
|
|
219
|
-
|
|
222
|
+
# Return all children preserving whitespace text nodes. The XML
|
|
223
|
+
# declaration is document state, not a child node (model,
|
|
224
|
+
# Object model), so it is never listed.
|
|
225
|
+
result = node.children.grep_v(::REXML::XMLDecl)
|
|
220
226
|
|
|
221
227
|
# Include any EntityReference wrappers stored alongside native
|
|
222
228
|
# children. Native children are identity-unique; only this
|
|
@@ -636,6 +642,15 @@ module Moxml
|
|
|
636
642
|
end
|
|
637
643
|
|
|
638
644
|
def xpath(node, expression, namespaces = {})
|
|
645
|
+
# REXML's engine answers no text nodes for text() node
|
|
646
|
+
# tests (D-07, both axes); the reference engine does
|
|
647
|
+
# (model, XPath evaluation).
|
|
648
|
+
if expression.match?(/\btext\s*\(/)
|
|
649
|
+
return Moxml::XPath.evaluate_natives(
|
|
650
|
+
node, expression, namespaces || {}, adapter: :rexml
|
|
651
|
+
)
|
|
652
|
+
end
|
|
653
|
+
|
|
639
654
|
# XPath.match for every expression: get_elements only
|
|
640
655
|
# handles simple child-name paths — attribute terminals,
|
|
641
656
|
# absolute paths and predicates silently degrade there.
|
|
@@ -109,6 +109,10 @@ module Moxml
|
|
|
109
109
|
# over-invalidate (global bump instead of local) — a no-op
|
|
110
110
|
# for correctness.
|
|
111
111
|
if name.start_with?("xmlns")
|
|
112
|
+
# Introducing (or rewriting) a namespace declaration: mark the
|
|
113
|
+
# context BEFORE the invalidation, so this write's generation
|
|
114
|
+
# bump fans out to every wrapper's memoized scope reads.
|
|
115
|
+
element.context.mark_namespaces_active!
|
|
112
116
|
adapter.set_attribute(element.native, name, value)
|
|
113
117
|
element.invalidate_attribute_cache!
|
|
114
118
|
return value
|
|
@@ -115,6 +115,14 @@ module Moxml
|
|
|
115
115
|
seen_root = false
|
|
116
116
|
idx = 0
|
|
117
117
|
document.children.each do |child|
|
|
118
|
+
# Whitespace between document-level nodes is not part of
|
|
119
|
+
# the canonical form (C14N §2.1 renders the document
|
|
120
|
+
# element and the PIs/comments outside it only). rexml
|
|
121
|
+
# retains such text nodes at parse (D-15); every engine's
|
|
122
|
+
# form must still exclude them.
|
|
123
|
+
next if child.is_a?(::Moxml::Text) ||
|
|
124
|
+
child.is_a?(::Moxml::Cdata)
|
|
125
|
+
|
|
118
126
|
if child.is_a?(::Moxml::Element)
|
|
119
127
|
next if seen_root
|
|
120
128
|
|
data/lib/moxml/c14n/processor.rb
CHANGED
|
@@ -21,6 +21,11 @@ module Moxml
|
|
|
21
21
|
def process(root_node)
|
|
22
22
|
output = (+"")
|
|
23
23
|
process_node(root_node, output)
|
|
24
|
+
# Document-level PI/comment rendering uses a newline as a
|
|
25
|
+
# SEPARATOR between nodes (and around the root), not as a
|
|
26
|
+
# leading/trailing pad: the canonical form starts and ends
|
|
27
|
+
# with the first/last node itself (libxml2 oracle).
|
|
28
|
+
output.chomp! if root_node.node_type == :root
|
|
24
29
|
output
|
|
25
30
|
end
|
|
26
31
|
|
|
@@ -93,26 +98,30 @@ module Moxml
|
|
|
93
98
|
return unless with_comments
|
|
94
99
|
return unless node.in_node_set?
|
|
95
100
|
|
|
96
|
-
|
|
97
|
-
# to keep canonical output readable.
|
|
98
|
-
if parent_element.nil? && output.length.positive?
|
|
99
|
-
output << "\n"
|
|
100
|
-
end
|
|
101
|
+
ensure_separator(output) if parent_element.nil?
|
|
101
102
|
output << "<!--" << node.value << "-->"
|
|
102
103
|
output << "\n" if parent_element.nil?
|
|
103
104
|
end
|
|
104
105
|
|
|
105
106
|
def process_pi_node(node, output, parent_element)
|
|
106
107
|
return unless node.in_node_set?
|
|
108
|
+
# C14N never emits the XML declaration; the "xml" target is
|
|
109
|
+
# reserved and cannot appear as a PI in a well-formed document.
|
|
110
|
+
return if node.target.to_s.casecmp("xml").zero?
|
|
107
111
|
|
|
108
|
-
if parent_element.nil?
|
|
109
|
-
output << "\n"
|
|
110
|
-
end
|
|
112
|
+
ensure_separator(output) if parent_element.nil?
|
|
111
113
|
output << "<?" << node.target
|
|
112
114
|
output << " " << node.data unless node.data.to_s.empty?
|
|
113
115
|
output << "?>"
|
|
114
116
|
output << "\n" if parent_element.nil?
|
|
115
117
|
end
|
|
118
|
+
|
|
119
|
+
# One newline between document-level nodes, never a doubled
|
|
120
|
+
# separator (a PI's trailing newline already starts the next
|
|
121
|
+
# node's line).
|
|
122
|
+
def ensure_separator(output)
|
|
123
|
+
output << "\n" if output.length.positive? && !output.end_with?("\n")
|
|
124
|
+
end
|
|
116
125
|
end
|
|
117
126
|
end
|
|
118
127
|
end
|