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/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
|
|
|
@@ -46,6 +50,29 @@ module Moxml
|
|
|
46
50
|
)
|
|
47
51
|
end
|
|
48
52
|
create_document
|
|
53
|
+
rescue ::RuntimeError => e
|
|
54
|
+
# REXML's entity-expansion cap (and any engine-level
|
|
55
|
+
# guard) surfaces as a bare RuntimeError; the facade
|
|
56
|
+
# names it (issue: taxonomy differential, huge-entity
|
|
57
|
+
# class; the cap itself is D-16).
|
|
58
|
+
raise Moxml::ParseError.new(
|
|
59
|
+
e.message,
|
|
60
|
+
source: xml.is_a?(String) ? xml[0..100] : nil,
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Rootless parses (empty or declaration-only input) are
|
|
65
|
+
# malformed: XML requires exactly one root. Nokogiri and
|
|
66
|
+
# leptris raise at the engine; rexml returns a bare
|
|
67
|
+
# document — the facade names it instead (loudness
|
|
68
|
+
# differential, TODO.sota/01). Recover mode keeps the
|
|
69
|
+
# rootless-document fallback with its diagnostics (#147).
|
|
70
|
+
if options[:strict] && native_doc.respond_to?(:root) &&
|
|
71
|
+
native_doc.root.nil?
|
|
72
|
+
raise Moxml::ParseError.new(
|
|
73
|
+
"document has no root element",
|
|
74
|
+
source: xml.is_a?(String) ? xml[0..100] : nil,
|
|
75
|
+
)
|
|
49
76
|
end
|
|
50
77
|
|
|
51
78
|
ctx = _context || Context.new(:rexml)
|
|
@@ -192,8 +219,10 @@ module Moxml
|
|
|
192
219
|
def children(node, **_entity_bearing)
|
|
193
220
|
return [] unless node.is_a?(::REXML::Parent)
|
|
194
221
|
|
|
195
|
-
# Return all children preserving whitespace text nodes
|
|
196
|
-
|
|
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)
|
|
197
226
|
|
|
198
227
|
# Include any EntityReference wrappers stored alongside native
|
|
199
228
|
# children. Native children are identity-unique; only this
|
|
@@ -435,7 +464,7 @@ module Moxml
|
|
|
435
464
|
end
|
|
436
465
|
|
|
437
466
|
def processing_instruction_content(node)
|
|
438
|
-
node.content
|
|
467
|
+
node.content.to_s
|
|
439
468
|
end
|
|
440
469
|
|
|
441
470
|
def set_processing_instruction_content(node, content)
|
|
@@ -458,17 +487,26 @@ module Moxml
|
|
|
458
487
|
return "" unless element
|
|
459
488
|
|
|
460
489
|
text = ""
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
490
|
+
begin
|
|
491
|
+
element.children.each do |child|
|
|
492
|
+
case child
|
|
493
|
+
when ::REXML::Text
|
|
494
|
+
# Preserve original spacing from text nodes exactly including newlines and all whitespace
|
|
495
|
+
text += child.value
|
|
496
|
+
when ::REXML::Element
|
|
497
|
+
# Extract text recursively from child element
|
|
498
|
+
child_text = extract_text_recursively(child)
|
|
499
|
+
# Concatenate directly like other adapters - NO SPACE INSERTION
|
|
500
|
+
text += child_text
|
|
501
|
+
end
|
|
471
502
|
end
|
|
503
|
+
rescue ::RuntimeError => e
|
|
504
|
+
# rexml expands entities lazily: the parse accepts, the
|
|
505
|
+
# read hits the expansion guard (D-16). The facade names
|
|
506
|
+
# it instead of leaking the bare RuntimeError.
|
|
507
|
+
raise Moxml::Error,
|
|
508
|
+
"entity expansion limit exceeded (rexml guard, D-16): " \
|
|
509
|
+
"#{e.message}"
|
|
472
510
|
end
|
|
473
511
|
# Don't strip - preserve original spacing including newlines
|
|
474
512
|
text
|
|
@@ -604,6 +642,15 @@ module Moxml
|
|
|
604
642
|
end
|
|
605
643
|
|
|
606
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
|
+
|
|
607
654
|
# XPath.match for every expression: get_elements only
|
|
608
655
|
# handles simple child-name paths — attribute terminals,
|
|
609
656
|
# absolute paths and predicates silently degrade there.
|
|
@@ -612,11 +659,29 @@ module Moxml
|
|
|
612
659
|
# namespaced elements never match (issue #395).
|
|
613
660
|
namespaces = namespaces.transform_keys(&:to_s) if
|
|
614
661
|
namespaces.is_a?(Hash)
|
|
615
|
-
if namespaces && !namespaces.empty?
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
662
|
+
results = if namespaces && !namespaces.empty?
|
|
663
|
+
::REXML::XPath.match(node, expression, namespaces).to_a
|
|
664
|
+
else
|
|
665
|
+
::REXML::XPath.match(node, expression).to_a
|
|
666
|
+
end
|
|
667
|
+
# REXML answers unions in expression order (//a | //b on
|
|
668
|
+
# <r><b/><a/></r> answers a,a,b); path results are
|
|
669
|
+
# document-ordered by the model (TODO.sota/02, M3) —
|
|
670
|
+
# re-order here, the oga lane's pattern.
|
|
671
|
+
return results if results.empty?
|
|
672
|
+
|
|
673
|
+
# Scalar arm: count()/string()/boolean()/arithmetic answer a
|
|
674
|
+
# one-element array holding the value; the adapter contract
|
|
675
|
+
# passes scalars through unwrapped (numbers as Float,
|
|
676
|
+
# uniform with the other adapters).
|
|
677
|
+
unless results.first.is_a?(::REXML::Node)
|
|
678
|
+
value = results.first
|
|
679
|
+
return value.is_a?(Numeric) ? value.to_f : value
|
|
619
680
|
end
|
|
681
|
+
|
|
682
|
+
ctx = Context.new(:rexml)
|
|
683
|
+
wrapped = results.map { |n| Moxml::Node.wrap(n, ctx) }
|
|
684
|
+
Moxml::XPath::Ordering.document_order(wrapped).map(&:native)
|
|
620
685
|
rescue ::REXML::ParseException => e
|
|
621
686
|
raise Moxml::XPathError.new(
|
|
622
687
|
e.message,
|
|
@@ -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
|
data/lib/moxml/context.rb
CHANGED
|
@@ -119,7 +119,11 @@ module Moxml
|
|
|
119
119
|
end
|
|
120
120
|
# Allocation-free declaration sniff: String#strip would copy the
|
|
121
121
|
# whole buffer just to test its prefix.
|
|
122
|
-
|
|
122
|
+
# Invalid byte sequences would raise from the regex before any
|
|
123
|
+
# adapter sees them; the adapters' named parse errors own
|
|
124
|
+
# malformed input (taxonomy differential, malformed class).
|
|
125
|
+
has_declaration = xml_string.valid_encoding? &&
|
|
126
|
+
xml_string.match?(/\A[ \t\r\n\f\v\0]*<\?xml/)
|
|
123
127
|
|
|
124
128
|
# Parse with adapter, passing self (context) so adapter can use our config
|
|
125
129
|
parsed_options = default_options.merge(options)
|
|
@@ -194,6 +198,19 @@ module Moxml
|
|
|
194
198
|
Moxml::XSD::Schema.new(handle, self)
|
|
195
199
|
end
|
|
196
200
|
|
|
201
|
+
# Compile a RELAX NG schema from source or from a file path (the
|
|
202
|
+
# engine resolves relative includes against the file's directory).
|
|
203
|
+
# Adapters without an RNG engine raise Moxml::NotImplementedError.
|
|
204
|
+
def relaxng(schema_source)
|
|
205
|
+
handle = config.adapter.relaxng_compile(schema_source.to_s)
|
|
206
|
+
Moxml::RelaxNG::Schema.new(handle, self)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def relaxng_file(path)
|
|
210
|
+
handle = config.adapter.relaxng_compile_file(path.to_s)
|
|
211
|
+
Moxml::RelaxNG::Schema.new(handle, self)
|
|
212
|
+
end
|
|
213
|
+
|
|
197
214
|
def xslt(stylesheet)
|
|
198
215
|
source = case stylesheet
|
|
199
216
|
when Moxml::Document then stylesheet.to_xml
|
|
@@ -278,6 +295,21 @@ module Moxml
|
|
|
278
295
|
raise ArgumentError, "Handler must inherit from Moxml::SAX::Handler"
|
|
279
296
|
end
|
|
280
297
|
|
|
298
|
+
# Loud degradation: an engine that cannot report an event family
|
|
299
|
+
# raises the named error when the handler subscribes, rather than
|
|
300
|
+
# answering a silently incomplete stream (model, SAX).
|
|
301
|
+
SAX::Handler::EVENT_METHODS.each_key do |family|
|
|
302
|
+
next unless handler.handles?(family)
|
|
303
|
+
next if config.adapter.sax_event_family_supported?(family)
|
|
304
|
+
|
|
305
|
+
raise Moxml::NotImplementedError.new(
|
|
306
|
+
"SAX #{family} events are not supported by the " \
|
|
307
|
+
"#{config.adapter.name.split('::').last} adapter",
|
|
308
|
+
feature: "sax_#{family}",
|
|
309
|
+
adapter: config.adapter.name,
|
|
310
|
+
)
|
|
311
|
+
end
|
|
312
|
+
|
|
281
313
|
# Delegate to adapter
|
|
282
314
|
config.adapter.sax_parse(xml, handler)
|
|
283
315
|
end
|
data/lib/moxml/document.rb
CHANGED
|
@@ -272,7 +272,7 @@ module Moxml
|
|
|
272
272
|
# - NodeSet: already wrapped, return directly
|
|
273
273
|
# - Array / LazyNodeSet: wrap in NodeSet
|
|
274
274
|
case result
|
|
275
|
-
when NodeSet,
|
|
275
|
+
when NodeSet, Numeric, String, TrueClass, FalseClass, NilClass
|
|
276
276
|
result
|
|
277
277
|
when Array, LazyNodeSet
|
|
278
278
|
NodeSet.new(result, context)
|
|
@@ -282,12 +282,26 @@ module Moxml
|
|
|
282
282
|
end
|
|
283
283
|
end
|
|
284
284
|
|
|
285
|
+
# The first node of a node-set; scalar results (count(), string(),
|
|
286
|
+
# boolean()) pass through unwrapped and uniform across adapters.
|
|
287
|
+
# Adapters whose at_xpath lane falls back to the reference engine
|
|
288
|
+
# when the native answer is empty (leptris, whose node-set index
|
|
289
|
+
# is stale after sibling insertion) use it.
|
|
285
290
|
def at_xpath(expression, namespaces = nil)
|
|
286
|
-
|
|
291
|
+
if adapter.native_at_xpath?
|
|
292
|
+
expression = lax_document_path(expression) if relative_name_test?(expression)
|
|
293
|
+
result = adapter.at_xpath(@native, expression, namespaces)
|
|
294
|
+
return result if scalar_result?(result)
|
|
287
295
|
|
|
288
|
-
|
|
289
|
-
Moxml::Node.wrap(native_node, context)
|
|
296
|
+
return Moxml::Node.wrap(result, context)
|
|
290
297
|
end
|
|
298
|
+
result = xpath(expression, namespaces)
|
|
299
|
+
result.is_a?(NodeSet) ? result.first : result
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def scalar_result?(result)
|
|
303
|
+
result.is_a?(Numeric) || result.is_a?(String) ||
|
|
304
|
+
result == true || result == false || result.nil?
|
|
291
305
|
end
|
|
292
306
|
|
|
293
307
|
# Quick element creation and addition
|
data/lib/moxml/node.rb
CHANGED
|
@@ -36,6 +36,11 @@ module Moxml
|
|
|
36
36
|
self
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
+
# Document-derived wrapper state (the entity-marker verdict) flows
|
|
40
|
+
# parent -> child during prewrapped traversal; adapters that keep
|
|
41
|
+
# no per-wrapper document state carry nothing.
|
|
42
|
+
def inherit_document_state(_parent); end
|
|
43
|
+
|
|
39
44
|
def refresh_native!(new_native)
|
|
40
45
|
unless new_native.equal?(@native)
|
|
41
46
|
# Cache-stable binding nodes carry the wrapper in an ivar
|
|
@@ -226,7 +231,8 @@ module Moxml
|
|
|
226
231
|
def attach_previous_sibling(node)
|
|
227
232
|
context.bump_children_generation
|
|
228
233
|
node = prepare_node(node)
|
|
229
|
-
adapter.add_previous_sibling(@native, node.native)
|
|
234
|
+
inserted = adapter.add_previous_sibling(@native, node.native)
|
|
235
|
+
refresh_inserted_native(node, inserted)
|
|
230
236
|
# Invalidate the parent's memoized children list. The wrapper-side
|
|
231
237
|
# @parent_node link is only set when this node was yielded through a
|
|
232
238
|
# parent-aware NodeSet; nodes obtained via at()/xpath() carry no such
|
|
@@ -239,12 +245,20 @@ module Moxml
|
|
|
239
245
|
def attach_next_sibling_to(anchor, node)
|
|
240
246
|
context.bump_children_generation
|
|
241
247
|
node = prepare_node(node)
|
|
242
|
-
adapter.add_next_sibling(anchor.native, node.native)
|
|
248
|
+
inserted = adapter.add_next_sibling(anchor.native, node.native)
|
|
249
|
+
refresh_inserted_native(node, inserted)
|
|
243
250
|
anchor.parent&.invalidate_children_cache!
|
|
244
251
|
anchor.invalidate_parent_children_cache!
|
|
245
252
|
self
|
|
246
253
|
end
|
|
247
254
|
|
|
255
|
+
def refresh_inserted_native(node, inserted)
|
|
256
|
+
return unless adapter.add_sibling_returns_inserted?
|
|
257
|
+
return if inserted.nil? || inserted.equal?(node.native)
|
|
258
|
+
|
|
259
|
+
node.refresh_native!(inserted)
|
|
260
|
+
end
|
|
261
|
+
|
|
248
262
|
def remove
|
|
249
263
|
# Nokogiri parity: unlinking an already-detached node is a
|
|
250
264
|
# no-op returning self. Re-attach idioms like
|
|
@@ -296,6 +310,21 @@ module Moxml
|
|
|
296
310
|
end
|
|
297
311
|
|
|
298
312
|
def to_xml(options = {})
|
|
313
|
+
# Element-level indentation is a declared capability: engines
|
|
314
|
+
# that ignore it must not answer unindented output silently
|
|
315
|
+
# (D-09). indent: 0 and the no-option path stay common.
|
|
316
|
+
indent = options[:indent]
|
|
317
|
+
if !is_a?(Document) && indent.is_a?(Integer) && indent.positive? &&
|
|
318
|
+
!adapter.element_indent_supported?
|
|
319
|
+
raise Moxml::AdapterError.new(
|
|
320
|
+
"element-level indentation is not supported by the " \
|
|
321
|
+
"#{adapter.name.split('::').last} adapter; serialize the " \
|
|
322
|
+
"document or use indent: 0",
|
|
323
|
+
adapter: adapter.name,
|
|
324
|
+
operation: "serialize",
|
|
325
|
+
)
|
|
326
|
+
end
|
|
327
|
+
|
|
299
328
|
# Determine if we should include XML declaration
|
|
300
329
|
# For Document nodes: check native then wrapper, unless explicitly overridden
|
|
301
330
|
# For other nodes: default to no declaration unless explicitly set
|
|
@@ -367,9 +396,24 @@ module Moxml
|
|
|
367
396
|
Materializer.materialize_fields(self, &block)
|
|
368
397
|
end
|
|
369
398
|
|
|
399
|
+
# The first node of a node-set; scalar results (count(), string(),
|
|
400
|
+
# boolean()) pass through unwrapped and uniform across adapters.
|
|
401
|
+
# Adapters with a native first-only face use it (see
|
|
402
|
+
# Document#at_xpath).
|
|
370
403
|
def at_xpath(expression, namespaces = {})
|
|
371
|
-
|
|
372
|
-
|
|
404
|
+
if adapter.native_at_xpath?
|
|
405
|
+
result = adapter.at_xpath(@native, expression, namespaces)
|
|
406
|
+
return result if scalar_result?(result)
|
|
407
|
+
|
|
408
|
+
return Moxml::Node.wrap(result, context)
|
|
409
|
+
end
|
|
410
|
+
result = xpath(expression, namespaces)
|
|
411
|
+
result.is_a?(NodeSet) ? result.first : result
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def scalar_result?(result)
|
|
415
|
+
result.is_a?(Numeric) || result.is_a?(String) ||
|
|
416
|
+
result == true || result == false || result.nil?
|
|
373
417
|
end
|
|
374
418
|
|
|
375
419
|
# Nokogiri-compatible sugar: at/search/at_css/css. Nokogiri's
|
|
@@ -410,10 +454,16 @@ module Moxml
|
|
|
410
454
|
end
|
|
411
455
|
|
|
412
456
|
# Nokogiri-compatible depth-first traversal: yields self, then
|
|
413
|
-
# children recursively (document-order visitor).
|
|
457
|
+
# children recursively (document-order visitor). Document-level
|
|
458
|
+
# parts (Declaration, Doctype) are state, not nodes: they are
|
|
459
|
+
# never yielded, and their (adapter-specific, sometimes missing)
|
|
460
|
+
# child faces are never consulted (issue-free by the model's
|
|
461
|
+
# object-model rule; found by the taxonomy differential).
|
|
414
462
|
def traverse(&block)
|
|
415
463
|
return to_enum(:traverse) unless block
|
|
416
464
|
|
|
465
|
+
return self if is_a?(Moxml::Declaration) || is_a?(Moxml::Doctype)
|
|
466
|
+
|
|
417
467
|
yield self
|
|
418
468
|
children.each { |child| child.traverse(&block) }
|
|
419
469
|
self
|
|
@@ -682,9 +732,16 @@ module Moxml
|
|
|
682
732
|
end
|
|
683
733
|
end
|
|
684
734
|
|
|
685
|
-
# The
|
|
686
|
-
#
|
|
687
|
-
#
|
|
735
|
+
# The node's kind (the model's Kind face): :document, :element,
|
|
736
|
+
# :text, :comment, :cdata, :processing_instruction, :doctype,
|
|
737
|
+
# :declaration, :entity_reference, :namespace, :attribute. The
|
|
738
|
+
# adapter's type probe is an FFI call per invocation; the type is
|
|
739
|
+
# fixed for a node's lifetime, so the first answer is memoized on
|
|
740
|
+
# the wrapper.
|
|
741
|
+
def node_type
|
|
742
|
+
node_type_cached
|
|
743
|
+
end
|
|
744
|
+
|
|
688
745
|
def node_type_cached
|
|
689
746
|
@node_type_cached ||= adapter.node_type(@native)
|
|
690
747
|
end
|
|
@@ -890,6 +947,16 @@ module Moxml
|
|
|
890
947
|
# input native keeps its registration so later wraps of the
|
|
891
948
|
# same base object hit the cache instead of re-minting.
|
|
892
949
|
if (extended = adapter.wrap_native(node, type, context))
|
|
950
|
+
# A native that already carries the contract IS the wrapper
|
|
951
|
+
# (leptris C mint): prime it in place and skip the WeakMap
|
|
952
|
+
# round trip — the primed @native-identity answers every
|
|
953
|
+
# later wrap through the fast path above.
|
|
954
|
+
if extended.equal?(node) && extended.is_a?(Node)
|
|
955
|
+
extended.prime_contract(extended, context, adapter, type) unless
|
|
956
|
+
extended.instance_variable_defined?(:@context)
|
|
957
|
+
return extended
|
|
958
|
+
end
|
|
959
|
+
|
|
893
960
|
extended.prime_contract(extended, context, adapter, type)
|
|
894
961
|
context.register_wrapper(node, extended)
|
|
895
962
|
context.register_wrapper(extended, extended) unless extended.equal?(node)
|
data/lib/moxml/node_set.rb
CHANGED
|
@@ -39,12 +39,18 @@ module Moxml
|
|
|
39
39
|
# nodes: Array of natives, or an adapter's LazyNodeSet — the
|
|
40
40
|
# native set is held unmaterialized until an operation needs an
|
|
41
41
|
# Array (#+, #<<, #delete, Range slices).
|
|
42
|
-
|
|
42
|
+
#
|
|
43
|
+
# prewrapped: the adapter vouches the entries are already
|
|
44
|
+
# contract-carrying wrappers (the leptris C-mint children face),
|
|
45
|
+
# so the walk path skips the wrapped-buffer allocation and the
|
|
46
|
+
# per-child wrap dispatch.
|
|
47
|
+
def initialize(nodes, context, parent_node = nil, prewrapped: false)
|
|
43
48
|
@nodes = nodes.is_a?(Array) ? nodes : nil
|
|
44
49
|
@lazy = @nodes ? nil : nodes
|
|
45
50
|
@context = context
|
|
46
51
|
@wrapped = nil
|
|
47
52
|
@parent_node = parent_node
|
|
53
|
+
@prewrapped = prewrapped
|
|
48
54
|
end
|
|
49
55
|
|
|
50
56
|
# The raw native nodes — adapter-internal work only (issue #26).
|
|
@@ -59,6 +65,13 @@ module Moxml
|
|
|
59
65
|
def each
|
|
60
66
|
return to_enum(:each) unless block_given?
|
|
61
67
|
|
|
68
|
+
if @prewrapped
|
|
69
|
+
(@nodes || @lazy).each do |node|
|
|
70
|
+
yield adopt_prewrapped(node)
|
|
71
|
+
end
|
|
72
|
+
return self
|
|
73
|
+
end
|
|
74
|
+
|
|
62
75
|
wrapped = wrapped_buffer
|
|
63
76
|
index = 0
|
|
64
77
|
(@nodes || @lazy).each do |node|
|
|
@@ -119,6 +132,17 @@ module Moxml
|
|
|
119
132
|
|
|
120
133
|
# Nokogiri-compatible: Array#flatten and multiple-assignment
|
|
121
134
|
# treat a NodeSet as its wrapped-element array via to_ary.
|
|
135
|
+
# The same node-set in document order (XPath path results are
|
|
136
|
+
# conventionally document-ordered; the compiler batches multi-step
|
|
137
|
+
# evaluation and must re-order). Wrappers are ordered, then the
|
|
138
|
+
# internal natives are re-paired — no re-wrapping.
|
|
139
|
+
def document_order
|
|
140
|
+
return self if native_nodes.size < 2
|
|
141
|
+
|
|
142
|
+
ordered = Moxml::XPath::Ordering.document_order(to_a)
|
|
143
|
+
NodeSet.new(ordered.map(&:native), @context, @parent_node)
|
|
144
|
+
end
|
|
145
|
+
|
|
122
146
|
def to_ary
|
|
123
147
|
to_a
|
|
124
148
|
end
|
|
@@ -216,5 +240,22 @@ module Moxml
|
|
|
216
240
|
end
|
|
217
241
|
wrapped
|
|
218
242
|
end
|
|
243
|
+
|
|
244
|
+
# The prewrapped entry path: prime a not-yet-primed C-minted
|
|
245
|
+
# wrapper in place (one ivar check), attach the traversal parent,
|
|
246
|
+
# and hand the document's marker verdict down the tree.
|
|
247
|
+
def adopt_prewrapped(node)
|
|
248
|
+
unless node.is_a?(Moxml::Node)
|
|
249
|
+
node = Moxml::Node.wrap(node, @context)
|
|
250
|
+
end
|
|
251
|
+
unless node.instance_variable_defined?(:@context)
|
|
252
|
+
node.prime_contract(node, @context, Moxml::Node.adapter(@context))
|
|
253
|
+
end
|
|
254
|
+
if @parent_node
|
|
255
|
+
node.parent_node = @parent_node
|
|
256
|
+
node.inherit_document_state(@parent_node)
|
|
257
|
+
end
|
|
258
|
+
node
|
|
259
|
+
end
|
|
219
260
|
end
|
|
220
261
|
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module Reference
|
|
5
|
+
# Pure-Ruby normalization faces: XML 1.0 §2.11 line endings and
|
|
6
|
+
# entity expansion. These are the model's normative answers with
|
|
7
|
+
# no engine involved; adapters are differentially tested against
|
|
8
|
+
# them.
|
|
9
|
+
module Normalizer
|
|
10
|
+
BUILTIN_ENTITIES = {
|
|
11
|
+
"amp" => "&", "lt" => "<", "gt" => ">",
|
|
12
|
+
"quot" => '"', "apos" => "'"
|
|
13
|
+
}.freeze
|
|
14
|
+
ENTITY_REFERENCE = /&([^;&<>]*);/
|
|
15
|
+
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
# CRLF → LF; bare CR → LF. Character references are untouched
|
|
19
|
+
# (
 keeps a literal CR).
|
|
20
|
+
def normalize_eol(text)
|
|
21
|
+
text.to_s.gsub("\r\n", "\n").gsub("\r", "\n")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Attribute-value normalization: literal tab/newline/CR become a
|
|
25
|
+
# space (character references keep their value).
|
|
26
|
+
def normalize_attribute_literal(text)
|
|
27
|
+
text.to_s.gsub(/[\t\n\r]/, " ")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Expand built-in and numeric references; custom entities come
|
|
31
|
+
# from the provided table. An undefined reference raises a named
|
|
32
|
+
# ParseError (the model has no silently unresolved entities).
|
|
33
|
+
def expand_entities(text, entities = {})
|
|
34
|
+
text.to_s.gsub(ENTITY_REFERENCE) do
|
|
35
|
+
reference = Regexp.last_match(1)
|
|
36
|
+
resolve(reference, BUILTIN_ENTITIES.merge(entities))
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def resolve(reference, entities)
|
|
41
|
+
if reference.start_with?("#x")
|
|
42
|
+
numeric(reference[2..], 16)
|
|
43
|
+
elsif reference.start_with?("#")
|
|
44
|
+
numeric(reference[1..], 10)
|
|
45
|
+
elsif BUILTIN_ENTITIES.key?(reference)
|
|
46
|
+
BUILTIN_ENTITIES[reference]
|
|
47
|
+
else
|
|
48
|
+
value = entities[reference]
|
|
49
|
+
raise ParseError, "undefined entity &#{reference};" unless value
|
|
50
|
+
|
|
51
|
+
expand_entities(value, entities)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def numeric(digits, base)
|
|
56
|
+
pattern = base == 16 ? /\A[0-9A-Fa-f]+\z/ : /\A[0-9]+\z/
|
|
57
|
+
raise ParseError, "malformed character reference" unless digits.match?(pattern)
|
|
58
|
+
|
|
59
|
+
[digits.to_i(base)].pack("U")
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|