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
data/lib/moxml/context.rb
CHANGED
|
@@ -90,7 +90,10 @@ module Moxml
|
|
|
90
90
|
end
|
|
91
91
|
|
|
92
92
|
def bump_children_generation
|
|
93
|
-
|
|
93
|
+
# One dispatch, not two: children_generation re-dispatched just
|
|
94
|
+
# to default nil. Shared counter — node wrappers' children memos
|
|
95
|
+
# key on it.
|
|
96
|
+
@children_generation = @children_generation.to_i + 1
|
|
94
97
|
end
|
|
95
98
|
|
|
96
99
|
def namespace_scope_generation
|
|
@@ -101,6 +104,22 @@ module Moxml
|
|
|
101
104
|
@namespace_scope_generation += 1
|
|
102
105
|
end
|
|
103
106
|
|
|
107
|
+
# Namespace scopes only need the per-mutation generation bump
|
|
108
|
+
# while namespaces can exist. A builder-only document (no parse,
|
|
109
|
+
# no xmlns write) never has one, and the bump sat on the attach
|
|
110
|
+
# hot path (deletion experiment: build row 2.128x -> 1.923x).
|
|
111
|
+
# Every namespace introduction route marks the context active
|
|
112
|
+
# first — parse faces and the xmlns attribute-write face — so
|
|
113
|
+
# the sticky flag never misses an introduction.
|
|
114
|
+
def namespace_scopes_active?
|
|
115
|
+
@namespace_scopes_active = false if @namespace_scopes_active.nil?
|
|
116
|
+
@namespace_scopes_active
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def mark_namespaces_active!
|
|
120
|
+
@namespace_scopes_active = true
|
|
121
|
+
end
|
|
122
|
+
|
|
104
123
|
def entity_registry
|
|
105
124
|
@entity_registry ||= build_entity_registry
|
|
106
125
|
end
|
|
@@ -128,6 +147,7 @@ module Moxml
|
|
|
128
147
|
# Parse with adapter, passing self (context) so adapter can use our config
|
|
129
148
|
parsed_options = default_options.merge(options)
|
|
130
149
|
doc = config.adapter.parse(xml_string, parsed_options, self)
|
|
150
|
+
mark_namespaces_active!
|
|
131
151
|
|
|
132
152
|
# Set declaration flag on Document wrapper (proper OOP)
|
|
133
153
|
doc.has_xml_declaration = has_declaration if doc.is_a?(Document)
|
|
@@ -198,6 +218,19 @@ module Moxml
|
|
|
198
218
|
Moxml::XSD::Schema.new(handle, self)
|
|
199
219
|
end
|
|
200
220
|
|
|
221
|
+
# Compile a RELAX NG schema from source or from a file path (the
|
|
222
|
+
# engine resolves relative includes against the file's directory).
|
|
223
|
+
# Adapters without an RNG engine raise Moxml::NotImplementedError.
|
|
224
|
+
def relaxng(schema_source)
|
|
225
|
+
handle = config.adapter.relaxng_compile(schema_source.to_s)
|
|
226
|
+
Moxml::RelaxNG::Schema.new(handle, self)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def relaxng_file(path)
|
|
230
|
+
handle = config.adapter.relaxng_compile_file(path.to_s)
|
|
231
|
+
Moxml::RelaxNG::Schema.new(handle, self)
|
|
232
|
+
end
|
|
233
|
+
|
|
201
234
|
def xslt(stylesheet)
|
|
202
235
|
source = case stylesheet
|
|
203
236
|
when Moxml::Document then stylesheet.to_xml
|
|
@@ -220,7 +253,9 @@ module Moxml
|
|
|
220
253
|
# are the result either way.
|
|
221
254
|
def parse_fragment(xml)
|
|
222
255
|
adapter = config.adapter
|
|
223
|
-
adapter.parse_fragment(xml, self)
|
|
256
|
+
nodes = adapter.parse_fragment(xml, self)
|
|
257
|
+
mark_namespaces_active!
|
|
258
|
+
nodes.map do |node|
|
|
224
259
|
# Adapters that tie a parent_node lifetime chain (#245)
|
|
225
260
|
# return wrapped nodes — pass them through untouched;
|
|
226
261
|
# re-wrapping would race the weak wrapper map.
|
|
@@ -229,6 +264,7 @@ module Moxml
|
|
|
229
264
|
end
|
|
230
265
|
|
|
231
266
|
def iterparse(xml, mode: :top_level, &block)
|
|
267
|
+
mark_namespaces_active!
|
|
232
268
|
config.adapter.iterparse(xml, mode, self, &block)
|
|
233
269
|
end
|
|
234
270
|
|
|
@@ -282,6 +318,21 @@ module Moxml
|
|
|
282
318
|
raise ArgumentError, "Handler must inherit from Moxml::SAX::Handler"
|
|
283
319
|
end
|
|
284
320
|
|
|
321
|
+
# Loud degradation: an engine that cannot report an event family
|
|
322
|
+
# raises the named error when the handler subscribes, rather than
|
|
323
|
+
# answering a silently incomplete stream (model, SAX).
|
|
324
|
+
SAX::Handler::EVENT_METHODS.each_key do |family|
|
|
325
|
+
next unless handler.handles?(family)
|
|
326
|
+
next if config.adapter.sax_event_family_supported?(family)
|
|
327
|
+
|
|
328
|
+
raise Moxml::NotImplementedError.new(
|
|
329
|
+
"SAX #{family} events are not supported by the " \
|
|
330
|
+
"#{config.adapter.name.split('::').last} adapter",
|
|
331
|
+
feature: "sax_#{family}",
|
|
332
|
+
adapter: config.adapter.name,
|
|
333
|
+
)
|
|
334
|
+
end
|
|
335
|
+
|
|
285
336
|
# Delegate to adapter
|
|
286
337
|
config.adapter.sax_parse(xml, handler)
|
|
287
338
|
end
|
data/lib/moxml/document.rb
CHANGED
|
@@ -163,7 +163,17 @@ module Moxml
|
|
|
163
163
|
end
|
|
164
164
|
|
|
165
165
|
def create_element(name)
|
|
166
|
-
|
|
166
|
+
# The preprimed mint (leptris binding with the C-primed
|
|
167
|
+
# create_element): the mint already wrote @context/@adapter in
|
|
168
|
+
# C, so the returned node IS the finished wrapper and the Ruby
|
|
169
|
+
# prime pass disappears from the build path.
|
|
170
|
+
native = if adapter.mints_primed?
|
|
171
|
+
adapter.create_primed_element(name, owner_doc: @native,
|
|
172
|
+
context: context,
|
|
173
|
+
adapter: adapter)
|
|
174
|
+
else
|
|
175
|
+
adapter.create_element(name, owner_doc: @native)
|
|
176
|
+
end
|
|
167
177
|
# leptris-ruby#508 mints registered klasses at create: the node
|
|
168
178
|
# IS a contract-carrying identity (@native is self) — prime it
|
|
169
179
|
# in place instead of shelling a second wrapper (a raw mint
|
|
@@ -282,14 +292,28 @@ module Moxml
|
|
|
282
292
|
end
|
|
283
293
|
end
|
|
284
294
|
|
|
285
|
-
#
|
|
286
|
-
#
|
|
287
|
-
#
|
|
295
|
+
# The first node of a node-set; scalar results (count(), string(),
|
|
296
|
+
# boolean()) pass through unwrapped and uniform across adapters.
|
|
297
|
+
# Adapters whose at_xpath lane falls back to the reference engine
|
|
298
|
+
# when the native answer is empty (leptris, whose node-set index
|
|
299
|
+
# is stale after sibling insertion) use it.
|
|
288
300
|
def at_xpath(expression, namespaces = nil)
|
|
301
|
+
if adapter.native_at_xpath?
|
|
302
|
+
expression = lax_document_path(expression) if relative_name_test?(expression)
|
|
303
|
+
result = adapter.at_xpath(@native, expression, namespaces)
|
|
304
|
+
return result if scalar_result?(result)
|
|
305
|
+
|
|
306
|
+
return Moxml::Node.wrap(result, context)
|
|
307
|
+
end
|
|
289
308
|
result = xpath(expression, namespaces)
|
|
290
309
|
result.is_a?(NodeSet) ? result.first : result
|
|
291
310
|
end
|
|
292
311
|
|
|
312
|
+
def scalar_result?(result)
|
|
313
|
+
result.is_a?(Numeric) || result.is_a?(String) ||
|
|
314
|
+
result == true || result == false || result.nil?
|
|
315
|
+
end
|
|
316
|
+
|
|
293
317
|
# Quick element creation and addition
|
|
294
318
|
def add_element(name, attributes = {}, &block)
|
|
295
319
|
elem = create_element(name)
|
data/lib/moxml/element.rb
CHANGED
|
@@ -276,6 +276,7 @@ module Moxml
|
|
|
276
276
|
def add_namespace(prefix, uri)
|
|
277
277
|
adapter.create_namespace(@native, prefix, uri,
|
|
278
278
|
namespace_validation_mode: context.config.namespace_validation_mode)
|
|
279
|
+
context.mark_namespaces_active!
|
|
279
280
|
invalidate_namespace_cache!
|
|
280
281
|
self
|
|
281
282
|
rescue ValidationError => e
|
|
@@ -530,8 +531,19 @@ module Moxml
|
|
|
530
531
|
|
|
531
532
|
# Clear the namespace caches and bump the context's scope
|
|
532
533
|
# generation so every wrapper — including ones not reachable from
|
|
533
|
-
# any children cache — recomputes on next read.
|
|
534
|
+
# any children cache — recomputes on next read. While namespaces
|
|
535
|
+
# cannot exist (context.namespace_scopes_active? is false) the
|
|
536
|
+
# memos can only hold namespace-free reads, so skipping the whole
|
|
537
|
+
# body is exact — and the nil-assignment form would MATERIALIZE
|
|
538
|
+
# three unset ivars per attached node, which is the cost this
|
|
539
|
+
# gate exists to remove. Every introduction route — parse faces
|
|
540
|
+
# and the xmlns write face — marks the context BEFORE its own
|
|
541
|
+
# invalidation, so the introducing mutation still clears and
|
|
542
|
+
# bumps; in-scope reads are generation-keyed, so other wrappers
|
|
543
|
+
# recompute on the bump.
|
|
534
544
|
def invalidate_namespace_cache!
|
|
545
|
+
return unless context.namespace_scopes_active?
|
|
546
|
+
|
|
535
547
|
@namespaces = nil
|
|
536
548
|
@namespace_definitions = nil
|
|
537
549
|
@in_scope_namespaces = nil
|
data/lib/moxml/node.rb
CHANGED
|
@@ -30,12 +30,19 @@ module Moxml
|
|
|
30
30
|
def prime_contract(native, context, adapter = nil, node_type = nil)
|
|
31
31
|
@context = context
|
|
32
32
|
@native = native
|
|
33
|
-
@parent_node
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
# @parent_node and @node_type_cached stay UNSET when empty —
|
|
34
|
+
# every reader is nil-tolerant, and a nil write materializes the
|
|
35
|
+
# ivar (object-shape transition) on every created node.
|
|
36
|
+
@adapter = adapter unless adapter.nil?
|
|
37
|
+
@node_type_cached = node_type unless node_type.nil?
|
|
36
38
|
self
|
|
37
39
|
end
|
|
38
40
|
|
|
41
|
+
# Document-derived wrapper state (the entity-marker verdict) flows
|
|
42
|
+
# parent -> child during prewrapped traversal; adapters that keep
|
|
43
|
+
# no per-wrapper document state carry nothing.
|
|
44
|
+
def inherit_document_state(_parent); end
|
|
45
|
+
|
|
39
46
|
def refresh_native!(new_native)
|
|
40
47
|
unless new_native.equal?(@native)
|
|
41
48
|
# Cache-stable binding nodes carry the wrapper in an ivar
|
|
@@ -209,10 +216,16 @@ module Moxml
|
|
|
209
216
|
def attach_child(node)
|
|
210
217
|
context.bump_children_generation
|
|
211
218
|
node = prepare_node(node)
|
|
212
|
-
|
|
219
|
+
oc = node.context
|
|
220
|
+
if !oc.nil? && !oc.equal?(context) &&
|
|
221
|
+
oc.namespace_scopes_active?
|
|
222
|
+
context.mark_namespaces_active!
|
|
223
|
+
end
|
|
224
|
+
a = adapter
|
|
225
|
+
a.add_child(@native, node.native)
|
|
213
226
|
# Refresh native in case adapter changed identity (e.g., LibXML
|
|
214
227
|
# doc.root=); stable-identity adapters skip the round trip.
|
|
215
|
-
unless
|
|
228
|
+
unless a.native_identity_stable?
|
|
216
229
|
refreshed = adapter.actual_native(node.native, @native)
|
|
217
230
|
node.refresh_native!(refreshed) if refreshed && refreshed != node.native
|
|
218
231
|
end
|
|
@@ -226,7 +239,9 @@ module Moxml
|
|
|
226
239
|
def attach_previous_sibling(node)
|
|
227
240
|
context.bump_children_generation
|
|
228
241
|
node = prepare_node(node)
|
|
229
|
-
|
|
242
|
+
absorb_namespace_activity(node)
|
|
243
|
+
inserted = adapter.add_previous_sibling(@native, node.native)
|
|
244
|
+
refresh_inserted_native(node, inserted)
|
|
230
245
|
# Invalidate the parent's memoized children list. The wrapper-side
|
|
231
246
|
# @parent_node link is only set when this node was yielded through a
|
|
232
247
|
# parent-aware NodeSet; nodes obtained via at()/xpath() carry no such
|
|
@@ -239,12 +254,31 @@ module Moxml
|
|
|
239
254
|
def attach_next_sibling_to(anchor, node)
|
|
240
255
|
context.bump_children_generation
|
|
241
256
|
node = prepare_node(node)
|
|
242
|
-
|
|
257
|
+
absorb_namespace_activity(node)
|
|
258
|
+
inserted = adapter.add_next_sibling(anchor.native, node.native)
|
|
259
|
+
refresh_inserted_native(node, inserted)
|
|
243
260
|
anchor.parent&.invalidate_children_cache!
|
|
244
261
|
anchor.invalidate_parent_children_cache!
|
|
245
262
|
self
|
|
246
263
|
end
|
|
247
264
|
|
|
265
|
+
# A node adopted from ANOTHER context can carry namespaces this
|
|
266
|
+
# context's own faces (parse, xmlns writes) never saw; import the
|
|
267
|
+
# source context's activity flag so the gated scope-generation
|
|
268
|
+
# bump stays armed here too.
|
|
269
|
+
def absorb_namespace_activity(node)
|
|
270
|
+
oc = node.context
|
|
271
|
+
return if oc.nil? || oc.equal?(context)
|
|
272
|
+
context.mark_namespaces_active! if oc.namespace_scopes_active?
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def refresh_inserted_native(node, inserted)
|
|
276
|
+
return unless adapter.add_sibling_returns_inserted?
|
|
277
|
+
return if inserted.nil? || inserted.equal?(node.native)
|
|
278
|
+
|
|
279
|
+
node.refresh_native!(inserted)
|
|
280
|
+
end
|
|
281
|
+
|
|
248
282
|
def remove
|
|
249
283
|
# Nokogiri parity: unlinking an already-detached node is a
|
|
250
284
|
# no-op returning self. Re-attach idioms like
|
|
@@ -296,6 +330,21 @@ module Moxml
|
|
|
296
330
|
end
|
|
297
331
|
|
|
298
332
|
def to_xml(options = {})
|
|
333
|
+
# Element-level indentation is a declared capability: engines
|
|
334
|
+
# that ignore it must not answer unindented output silently
|
|
335
|
+
# (D-09). indent: 0 and the no-option path stay common.
|
|
336
|
+
indent = options[:indent]
|
|
337
|
+
if !is_a?(Document) && indent.is_a?(Integer) && indent.positive? &&
|
|
338
|
+
!adapter.element_indent_supported?
|
|
339
|
+
raise Moxml::AdapterError.new(
|
|
340
|
+
"element-level indentation is not supported by the " \
|
|
341
|
+
"#{adapter.name.split('::').last} adapter; serialize the " \
|
|
342
|
+
"document or use indent: 0",
|
|
343
|
+
adapter: adapter.name,
|
|
344
|
+
operation: "serialize",
|
|
345
|
+
)
|
|
346
|
+
end
|
|
347
|
+
|
|
299
348
|
# Determine if we should include XML declaration
|
|
300
349
|
# For Document nodes: check native then wrapper, unless explicitly overridden
|
|
301
350
|
# For other nodes: default to no declaration unless explicitly set
|
|
@@ -367,14 +416,26 @@ module Moxml
|
|
|
367
416
|
Materializer.materialize_fields(self, &block)
|
|
368
417
|
end
|
|
369
418
|
|
|
370
|
-
#
|
|
371
|
-
#
|
|
372
|
-
#
|
|
419
|
+
# The first node of a node-set; scalar results (count(), string(),
|
|
420
|
+
# boolean()) pass through unwrapped and uniform across adapters.
|
|
421
|
+
# Adapters with a native first-only face use it (see
|
|
422
|
+
# Document#at_xpath).
|
|
373
423
|
def at_xpath(expression, namespaces = {})
|
|
424
|
+
if adapter.native_at_xpath?
|
|
425
|
+
result = adapter.at_xpath(@native, expression, namespaces)
|
|
426
|
+
return result if scalar_result?(result)
|
|
427
|
+
|
|
428
|
+
return Moxml::Node.wrap(result, context)
|
|
429
|
+
end
|
|
374
430
|
result = xpath(expression, namespaces)
|
|
375
431
|
result.is_a?(NodeSet) ? result.first : result
|
|
376
432
|
end
|
|
377
433
|
|
|
434
|
+
def scalar_result?(result)
|
|
435
|
+
result.is_a?(Numeric) || result.is_a?(String) ||
|
|
436
|
+
result == true || result == false || result.nil?
|
|
437
|
+
end
|
|
438
|
+
|
|
378
439
|
# Nokogiri-compatible sugar: at/search/at_css/css. Nokogiri's
|
|
379
440
|
# at/search auto-detect XPath vs CSS; the XPath-first fallback
|
|
380
441
|
# covers the dominant caller shape (XPath expression strings)
|
|
@@ -732,7 +793,10 @@ module Moxml
|
|
|
732
793
|
protected
|
|
733
794
|
|
|
734
795
|
def invalidate_children_cache!
|
|
735
|
-
|
|
796
|
+
# Only nil an EXISTING memo: nil-assignment materializes an
|
|
797
|
+
# unset ivar (object-shape transition), and the builder hot
|
|
798
|
+
# path invalidates parents whose children memo was never read.
|
|
799
|
+
@children = nil unless @children.nil?
|
|
736
800
|
end
|
|
737
801
|
|
|
738
802
|
# Invalidate parent's cached children when this node
|
|
@@ -906,6 +970,16 @@ module Moxml
|
|
|
906
970
|
# input native keeps its registration so later wraps of the
|
|
907
971
|
# same base object hit the cache instead of re-minting.
|
|
908
972
|
if (extended = adapter.wrap_native(node, type, context))
|
|
973
|
+
# A native that already carries the contract IS the wrapper
|
|
974
|
+
# (leptris C mint): prime it in place and skip the WeakMap
|
|
975
|
+
# round trip — the primed @native-identity answers every
|
|
976
|
+
# later wrap through the fast path above.
|
|
977
|
+
if extended.equal?(node) && extended.is_a?(Node)
|
|
978
|
+
extended.prime_contract(extended, context, adapter, type) unless
|
|
979
|
+
extended.instance_variable_defined?(:@context)
|
|
980
|
+
return extended
|
|
981
|
+
end
|
|
982
|
+
|
|
909
983
|
extended.prime_contract(extended, context, adapter, type)
|
|
910
984
|
context.register_wrapper(node, extended)
|
|
911
985
|
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|
|
|
@@ -227,5 +240,22 @@ module Moxml
|
|
|
227
240
|
end
|
|
228
241
|
wrapped
|
|
229
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
|
|
230
260
|
end
|
|
231
261
|
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
# RELAX NG schema validation (TODO.sota/05): a facade for engines
|
|
5
|
+
# that ship an RNG validator. Engines without one raise the named
|
|
6
|
+
# Moxml::NotImplementedError — the face is never silently absent.
|
|
7
|
+
#
|
|
8
|
+
# ctx = Moxml.new(:leptris)
|
|
9
|
+
# schema = ctx.relaxng_file("schema.rng")
|
|
10
|
+
# schema.valid?(doc) # => true/false
|
|
11
|
+
# schema.validate_errors(doc) # => [] or [{line:, column:, message:}]
|
|
12
|
+
# schema.validate(doc) # => [] or ["line:col: error: message"]
|
|
13
|
+
#
|
|
14
|
+
# Adapters without an engine raise at compile; RelaxNG.supported?
|
|
15
|
+
# answers without raising (the XSD facade's pattern).
|
|
16
|
+
module RelaxNG
|
|
17
|
+
class << self
|
|
18
|
+
# Whether the context's adapter ships a RELAX NG validator.
|
|
19
|
+
def supported?(context)
|
|
20
|
+
adapter = context.config.adapter
|
|
21
|
+
adapter.const_defined?(:RELAXNG_SUPPORTED) &&
|
|
22
|
+
adapter.const_get(:RELAXNG_SUPPORTED)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# A compiled schema (adapter-owned handle).
|
|
27
|
+
class Schema
|
|
28
|
+
def initialize(handle, context)
|
|
29
|
+
@handle = handle
|
|
30
|
+
@context = context
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def valid?(document)
|
|
34
|
+
@context.config.adapter.relaxng_valid?(@handle, native(document))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Structured report: [{line:, column:, message:}] (empty = valid).
|
|
38
|
+
def validate_errors(document)
|
|
39
|
+
@context.config.adapter
|
|
40
|
+
.relaxng_validate_errors(@handle, native(document))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Nokogiri shape: error strings (empty = valid).
|
|
44
|
+
def validate(document)
|
|
45
|
+
validate_errors(document).map do |error|
|
|
46
|
+
line = error[:line]
|
|
47
|
+
if line&.to_i&.positive?
|
|
48
|
+
"#{line}:#{error[:column]}: error: #{error[:message]}"
|
|
49
|
+
else
|
|
50
|
+
error[:message]
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def native(document)
|
|
58
|
+
document.is_a?(Moxml::Document) ? document.native : document
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -191,6 +191,13 @@ module Moxml
|
|
|
191
191
|
def on_warning(message)
|
|
192
192
|
@handlers[:warning]&.call(message)
|
|
193
193
|
end
|
|
194
|
+
|
|
195
|
+
# A block handler subscribes to a family only when its DSL block
|
|
196
|
+
# was registered — it overrides every callback, so method
|
|
197
|
+
# ownership cannot answer interest here.
|
|
198
|
+
def handles?(family)
|
|
199
|
+
@handlers.key?(family)
|
|
200
|
+
end
|
|
194
201
|
end
|
|
195
202
|
end
|
|
196
203
|
end
|
data/lib/moxml/sax/handler.rb
CHANGED
|
@@ -98,6 +98,26 @@ module Moxml
|
|
|
98
98
|
raise error
|
|
99
99
|
end
|
|
100
100
|
|
|
101
|
+
# SAX event families that engines may or may not report as
|
|
102
|
+
# distinct events, with their handler methods. An engine that
|
|
103
|
+
# cannot report a family must raise the named error when the
|
|
104
|
+
# handler subscribes — never fold the events silently.
|
|
105
|
+
EVENT_METHODS = {
|
|
106
|
+
cdata: :on_cdata,
|
|
107
|
+
comment: :on_comment,
|
|
108
|
+
processing_instruction: :on_processing_instruction,
|
|
109
|
+
}.freeze
|
|
110
|
+
|
|
111
|
+
# Whether this handler subscribes to an event family: the
|
|
112
|
+
# callback was overridden (class handlers) or registered (block
|
|
113
|
+
# handlers).
|
|
114
|
+
#
|
|
115
|
+
# @param family [Symbol] one of EVENT_METHODS keys
|
|
116
|
+
# @return [Boolean]
|
|
117
|
+
def handles?(family)
|
|
118
|
+
method(EVENT_METHODS.fetch(family)).owner != Moxml::SAX::Handler
|
|
119
|
+
end
|
|
120
|
+
|
|
101
121
|
# Called when a non-fatal warning occurs
|
|
102
122
|
#
|
|
103
123
|
# Default implementation ignores warnings.
|
data/lib/moxml/version.rb
CHANGED
data/lib/moxml/xpath.rb
CHANGED
|
@@ -26,5 +26,33 @@ module Moxml
|
|
|
26
26
|
autoload :Node, "moxml/xpath/ruby/node"
|
|
27
27
|
autoload :Generator, "moxml/xpath/ruby/generator"
|
|
28
28
|
end
|
|
29
|
+
|
|
30
|
+
module_function
|
|
31
|
+
|
|
32
|
+
# Evaluate an expression through the pure-Ruby XPath 1.0 engine —
|
|
33
|
+
# the model's reference lane. Adapters route shapes their native
|
|
34
|
+
# engine cannot answer here (the `xmlns:` idiom, `text()` node
|
|
35
|
+
# tests, ...). Returns the engine's answer: an Array of wrapped
|
|
36
|
+
# nodes in document order, a NodeSet, or a scalar.
|
|
37
|
+
def evaluate(node, expression, namespaces = {}, adapter: nil)
|
|
38
|
+
context = node.is_a?(Moxml::Node) ? node.context : ::Moxml::Context.new(adapter)
|
|
39
|
+
wrapped = node.is_a?(Moxml::Node) ? node : Moxml::Node.wrap(node, context)
|
|
40
|
+
compiled = Compiler.compile_with_cache(Parser.parse(expression),
|
|
41
|
+
namespaces: namespaces)
|
|
42
|
+
compiled.call(wrapped)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# The adapter-contract form of #evaluate: Array<native> | scalar.
|
|
46
|
+
def evaluate_natives(node, expression, namespaces = {}, adapter: nil)
|
|
47
|
+
result = evaluate(node, expression, namespaces, adapter: adapter)
|
|
48
|
+
case result
|
|
49
|
+
when Array
|
|
50
|
+
result.map { |n| n.is_a?(Moxml::Node) ? n.native : n }
|
|
51
|
+
when NodeSet
|
|
52
|
+
result.to_a.map(&:native)
|
|
53
|
+
else
|
|
54
|
+
result
|
|
55
|
+
end
|
|
56
|
+
end
|
|
29
57
|
end
|
|
30
58
|
end
|
data/lib/moxml.rb
CHANGED
|
@@ -105,6 +105,7 @@ module Moxml
|
|
|
105
105
|
autoload :Reference, "moxml/reference"
|
|
106
106
|
autoload :XSLT, "moxml/xslt"
|
|
107
107
|
autoload :XSD, "moxml/xsd"
|
|
108
|
+
autoload :RelaxNG, "moxml/relaxng"
|
|
108
109
|
|
|
109
110
|
# Error hierarchy — each subclass autoloads from the same file
|
|
110
111
|
autoload :Error, "moxml/error"
|
|
@@ -5,6 +5,28 @@
|
|
|
5
5
|
RSpec.describe "Adapter Examples" do
|
|
6
6
|
let(:xml) { "<root><child>text</child></root>" }
|
|
7
7
|
|
|
8
|
+
describe "Document children listing" do
|
|
9
|
+
it "never lists the XML declaration as a child" do
|
|
10
|
+
Moxml::Adapter::AVAILABLE_ADAPTERS.each do |adapter|
|
|
11
|
+
doc = Moxml.new(adapter)
|
|
12
|
+
.parse(%(<?xml version="1.0"?><r/>))
|
|
13
|
+
expect(doc.children.map(&:node_type)).to eq([:element]),
|
|
14
|
+
"#{adapter}: declaration leaked " \
|
|
15
|
+
"into children (model: state, not a node)"
|
|
16
|
+
expect(doc.has_xml_declaration).to be(true),
|
|
17
|
+
"#{adapter}: declaration state lost"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it "lists the doctype per the Nokogiri contract" do
|
|
22
|
+
Moxml::Adapter::AVAILABLE_ADAPTERS.each do |adapter|
|
|
23
|
+
doc = Moxml.new(adapter).parse(%(<!DOCTYPE r><r/>))
|
|
24
|
+
expect(doc.children.map(&:node_type)).to eq(%i[doctype element]),
|
|
25
|
+
"#{adapter}: doctype listing"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
8
30
|
describe "Serialization consistency" do
|
|
9
31
|
it "produces equivalent XML across adapters",
|
|
10
32
|
skip: "No easy way to exclude the declaration from Nokogiri documents" do
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# Citation hygiene (TODO.sota/07): a spec or doc that calls a
|
|
6
|
+
# divergence "pre-existing" or "documented" must resolve it to a
|
|
7
|
+
# register ID (D-NN) on the same line. The register is the only place
|
|
8
|
+
# such a claim is allowed to stand on its own.
|
|
9
|
+
CITATION_PHRASE = /(?:\bpre-existing\b|\bpreexisting\b|\bdocumented (?:divergence|limitation)\b)/i
|
|
10
|
+
|
|
11
|
+
RSpec.describe "divergence citations" do
|
|
12
|
+
def offenses(paths)
|
|
13
|
+
paths.flat_map do |path|
|
|
14
|
+
File.readlines(path).each_with_index.filter_map do |line, idx|
|
|
15
|
+
claim = line.match?(CITATION_PHRASE)
|
|
16
|
+
cited = line.match?(/D-\d+/)
|
|
17
|
+
"#{path}:#{idx + 1}: #{line.strip}" if claim && !cited
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it "resolves every pre-existing/documented divergence claim to a register ID" do
|
|
23
|
+
paths = (Dir.glob("spec/**/*.rb") + Dir.glob("docs/**/*.adoc"))
|
|
24
|
+
.reject { |path| path.end_with?("citation_hygiene_spec.rb") }
|
|
25
|
+
found = offenses(paths)
|
|
26
|
+
expect(found).to be_empty,
|
|
27
|
+
"name a D-NN (docs/_pages/divergences.adoc) or drop " \
|
|
28
|
+
"the claim:\n#{found.join("\n")}"
|
|
29
|
+
end
|
|
30
|
+
end
|