moxml 0.1.26 → 0.3.0
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/windows.yml +62 -0
- data/Gemfile +3 -1
- data/README.adoc +69 -6
- data/lib/compat/opal/moxml_boot.rb +6 -1
- data/lib/moxml/adapter/base.rb +53 -56
- data/lib/moxml/adapter/customized_leptris/declaration.rb +32 -0
- data/lib/moxml/adapter/customized_leptris/doctype.rb +31 -0
- data/lib/moxml/adapter/customized_leptris/entity_reference.rb +12 -0
- data/lib/moxml/adapter/customized_leptris/text_segment.rb +26 -0
- data/lib/moxml/adapter/customized_leptris.rb +16 -0
- data/lib/moxml/adapter/customized_libxml/cdata.rb +3 -12
- data/lib/moxml/adapter/customized_libxml/comment.rb +2 -9
- data/lib/moxml/adapter/customized_libxml/declaration.rb +1 -10
- data/lib/moxml/adapter/customized_libxml/node.rb +8 -0
- data/lib/moxml/adapter/customized_libxml/processing_instruction.rb +2 -10
- data/lib/moxml/adapter/customized_libxml/text.rb +2 -9
- data/lib/moxml/adapter/customized_oga/entity_decoder.rb +37 -0
- data/lib/moxml/adapter/customized_oga/raw_value_override.rb +52 -0
- data/lib/moxml/adapter/customized_oga/xml_generator.rb +4 -34
- data/lib/moxml/adapter/customized_oga.rb +2 -0
- data/lib/moxml/adapter/customized_ox/entity_reference.rb +1 -17
- data/lib/moxml/adapter/customized_rexml/entity_reference.rb +1 -11
- data/lib/moxml/adapter/headed_ox.rb +9 -128
- data/lib/moxml/adapter/leptris.rb +965 -0
- data/lib/moxml/adapter/libxml.rb +137 -117
- data/lib/moxml/adapter/nokogiri.rb +29 -8
- data/lib/moxml/adapter/oga.rb +139 -55
- data/lib/moxml/adapter/ox.rb +93 -137
- data/lib/moxml/adapter/rexml.rb +64 -51
- data/lib/moxml/adapter.rb +2 -1
- data/lib/moxml/attribute.rb +8 -4
- data/lib/moxml/attribute_resolver.rb +189 -0
- data/lib/moxml/builder.rb +0 -18
- data/lib/moxml/config.rb +29 -5
- data/lib/moxml/context.rb +33 -0
- data/lib/moxml/document.rb +1 -1
- data/lib/moxml/element.rb +29 -12
- data/lib/moxml/entity/reference.rb +28 -0
- data/lib/moxml/entity/restorer.rb +95 -0
- data/lib/moxml/entity.rb +126 -0
- data/lib/moxml/entity_registry.rb +1 -1
- data/lib/moxml/error.rb +0 -42
- data/lib/moxml/node.rb +135 -10
- data/lib/moxml/node_set.rb +1 -1
- data/lib/moxml/sax/namespace_splitter.rb +5 -2
- data/lib/moxml/signature/model/key/dsa_key_value.rb +1 -2
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xml_emitter.rb +71 -0
- data/lib/moxml/xpath/compiler.rb +143 -8
- data/lib/moxml/xpath/parser.rb +22 -15
- data/lib/moxml.rb +3 -3
- data/spec/examples/readme_examples_spec.rb +0 -4
- data/spec/examples/xpath_examples_spec.rb +0 -11
- data/spec/integration/all_adapters_spec.rb +18 -1
- data/spec/integration/sax_parity_spec.rb +58 -0
- data/spec/integration/shared_examples/edge_cases.rb +0 -10
- data/spec/integration/shared_examples/high_level/{document_builder_behavior.rb → parse_behavior.rb} +1 -2
- data/spec/integration/shared_examples/integration_workflows.rb +0 -10
- data/spec/integration/shared_examples/node_wrappers/attribute_behavior.rb +98 -0
- data/spec/integration/shared_examples/node_wrappers/namespace_behavior.rb +26 -0
- data/spec/integration/shared_examples/node_wrappers/node_behavior.rb +15 -8
- data/spec/moxml/adapter/leptris_spec.rb +75 -0
- data/spec/moxml/adapter/libxml_internals_spec.rb +1 -1
- data/spec/moxml/adapter/ox_spec.rb +20 -6
- data/spec/moxml/adapter/platform_spec.rb +15 -2
- data/spec/moxml/adapter/shared_examples/adapter_contract.rb +180 -0
- data/spec/moxml/attribute_resolver_spec.rb +108 -0
- data/spec/moxml/config_spec.rb +0 -1
- data/spec/moxml/context_spec.rb +22 -0
- data/spec/moxml/doctype_spec.rb +1 -1
- data/spec/moxml/entity_spec.rb +76 -0
- data/spec/moxml/error_spec.rb +0 -50
- data/spec/moxml/node_spec.rb +104 -0
- data/spec/moxml/sax_entity_parity_spec.rb +358 -0
- data/spec/moxml/xml_emitter_spec.rb +64 -0
- data/spec/moxml/xpath/axes_spec.rb +112 -0
- data/spec/moxml/xpath/parser_spec.rb +6 -0
- data/spec/moxml/xpath_capabilities_spec.rb +5 -3
- data/spec/performance/benchmark_spec.rb +13 -6
- data/spec/performance/thread_safety_spec.rb +0 -4
- data/spec/spec_helper.rb +5 -1
- metadata +23 -6
- data/lib/moxml/adapter/libxml/entity_restorer.rb +0 -94
- data/lib/moxml/document_builder.rb +0 -167
- data/spec/moxml/document_builder_spec.rb +0 -30
data/lib/moxml/xpath/compiler.rb
CHANGED
|
@@ -294,6 +294,12 @@ module Moxml
|
|
|
294
294
|
|
|
295
295
|
handler = axis_name.gsub("-", "_")
|
|
296
296
|
|
|
297
|
+
unless respond_to?(:"on_axis_#{handler}", true)
|
|
298
|
+
raise XPath::Error.new(
|
|
299
|
+
"XPath axis #{axis_name.inspect} is not supported by this engine",
|
|
300
|
+
)
|
|
301
|
+
end
|
|
302
|
+
|
|
297
303
|
send(:"on_axis_#{handler}", test, input, &block)
|
|
298
304
|
end
|
|
299
305
|
|
|
@@ -362,6 +368,78 @@ module Moxml
|
|
|
362
368
|
end
|
|
363
369
|
end
|
|
364
370
|
|
|
371
|
+
# AXIS: ancestor - all ancestors up to the document
|
|
372
|
+
def on_axis_ancestor(ast, input)
|
|
373
|
+
ancestor = unique_literal(:ancestor)
|
|
374
|
+
|
|
375
|
+
document_or_node(input).if_true do
|
|
376
|
+
input.ancestors.each.add_block(ancestor) do
|
|
377
|
+
condition = process(ast, ancestor)
|
|
378
|
+
if block_given?
|
|
379
|
+
condition.if_true { yield ancestor }
|
|
380
|
+
else
|
|
381
|
+
condition.if_true { ancestor }
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
# AXIS: ancestor-or-self - the context node and its ancestors
|
|
388
|
+
def on_axis_ancestor_or_self(ast, input)
|
|
389
|
+
ancestor = unique_literal(:ancestor)
|
|
390
|
+
|
|
391
|
+
document_or_node(input).if_true do
|
|
392
|
+
self_result = process(ast, input)
|
|
393
|
+
self_collection = if block_given?
|
|
394
|
+
self_result.if_true { yield input }
|
|
395
|
+
else
|
|
396
|
+
self_result.if_true { input }
|
|
397
|
+
end
|
|
398
|
+
self_collection.followed_by do
|
|
399
|
+
input.ancestors.each.add_block(ancestor) do
|
|
400
|
+
condition = process(ast, ancestor)
|
|
401
|
+
if block_given?
|
|
402
|
+
condition.if_true { yield ancestor }
|
|
403
|
+
else
|
|
404
|
+
condition.if_true { ancestor }
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
# AXIS: following-sibling - siblings after the context node
|
|
412
|
+
def on_axis_following_sibling(ast, input)
|
|
413
|
+
sibling = unique_literal(:sibling)
|
|
414
|
+
|
|
415
|
+
document_or_node(input).if_true do
|
|
416
|
+
input.following_siblings.each.add_block(sibling) do
|
|
417
|
+
condition = process(ast, sibling)
|
|
418
|
+
if block_given?
|
|
419
|
+
condition.if_true { yield sibling }
|
|
420
|
+
else
|
|
421
|
+
condition.if_true { sibling }
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
# AXIS: preceding-sibling - siblings before the context node
|
|
428
|
+
def on_axis_preceding_sibling(ast, input)
|
|
429
|
+
sibling = unique_literal(:sibling)
|
|
430
|
+
|
|
431
|
+
document_or_node(input).if_true do
|
|
432
|
+
input.preceding_siblings.each.add_block(sibling) do
|
|
433
|
+
condition = process(ast, sibling)
|
|
434
|
+
if block_given?
|
|
435
|
+
condition.if_true { yield sibling }
|
|
436
|
+
else
|
|
437
|
+
condition.if_true { sibling }
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
|
|
365
443
|
# AXIS: descendant-or-self - Enables // operator
|
|
366
444
|
def on_axis_descendant_or_self(ast, input)
|
|
367
445
|
node = unique_literal(:descendant)
|
|
@@ -425,15 +503,22 @@ module Moxml
|
|
|
425
503
|
end
|
|
426
504
|
end
|
|
427
505
|
|
|
428
|
-
# AXIS: attribute - Enables @attribute syntax
|
|
506
|
+
# AXIS: attribute - Enables @attribute syntax. Name tests share
|
|
507
|
+
# the resolver's expanded-name semantics (Namespaces 1.0): bare
|
|
508
|
+
# names match no-namespace attributes only; prefixed names match
|
|
509
|
+
# by namespace URI, never by prefix spelling.
|
|
429
510
|
def on_axis_attribute(ast, input)
|
|
430
511
|
elem_class = const_ref("Moxml", "Element")
|
|
431
512
|
attribute = unique_literal(:attribute)
|
|
432
513
|
|
|
433
514
|
input.is_a?(elem_class).if_true do
|
|
434
515
|
input.attributes.each.add_block(attribute) do
|
|
435
|
-
|
|
436
|
-
|
|
516
|
+
condition = if ast.type == :test
|
|
517
|
+
attribute_test_condition(ast, input, attribute)
|
|
518
|
+
else
|
|
519
|
+
# Wildcards and node types keep the generic path
|
|
520
|
+
process(ast, attribute)
|
|
521
|
+
end
|
|
437
522
|
|
|
438
523
|
if block_given?
|
|
439
524
|
condition.if_true { yield attribute }
|
|
@@ -444,6 +529,34 @@ module Moxml
|
|
|
444
529
|
end
|
|
445
530
|
end
|
|
446
531
|
|
|
532
|
+
# Attribute node-test condition built on AttributeResolver. The
|
|
533
|
+
# XPath context's prefix mapping (when provided) wins over the
|
|
534
|
+
# document's own declarations and is resolved at compile time.
|
|
535
|
+
# Returns nil for the both-wildcard test (@*): no name constraint.
|
|
536
|
+
def attribute_test_condition(ast, element, attribute)
|
|
537
|
+
resolver = const_ref("Moxml", "AttributeResolver")
|
|
538
|
+
ns = ast.value[:namespace]
|
|
539
|
+
name = ast.value[:name]
|
|
540
|
+
|
|
541
|
+
return nil if name == STAR && (!ns || ns == STAR)
|
|
542
|
+
|
|
543
|
+
local = name == STAR ? self_nil : string(name)
|
|
544
|
+
|
|
545
|
+
if ns && ns != STAR && @namespaces && (uri = @namespaces[ns])
|
|
546
|
+
return resolver.attribute_test_uri?(element, attribute,
|
|
547
|
+
string(uri.to_s), local)
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
prefix = if ns.nil?
|
|
551
|
+
self_nil
|
|
552
|
+
elsif ns == STAR
|
|
553
|
+
symbol(:any)
|
|
554
|
+
else
|
|
555
|
+
string(ns)
|
|
556
|
+
end
|
|
557
|
+
resolver.attribute_test?(element, attribute, prefix, local)
|
|
558
|
+
end
|
|
559
|
+
|
|
447
560
|
# AXIS: descendant - All descendant nodes (without self)
|
|
448
561
|
def on_axis_descendant(ast, input)
|
|
449
562
|
node = unique_literal(:descendant)
|
|
@@ -494,6 +607,13 @@ module Moxml
|
|
|
494
607
|
when "node"
|
|
495
608
|
# node() matches everything — use a truthy literal
|
|
496
609
|
Ruby::Node.new(:lit, ["true"])
|
|
610
|
+
when "text"
|
|
611
|
+
# CDATA sections are text nodes in the XPath data model
|
|
612
|
+
text_class = const_ref("Moxml", "Text")
|
|
613
|
+
cdata_class = const_ref("Moxml", "Cdata")
|
|
614
|
+
input.is_a?(text_class).or(input.is_a?(cdata_class))
|
|
615
|
+
when "comment"
|
|
616
|
+
input.is_a?(const_ref("Moxml", "Comment"))
|
|
497
617
|
else element_or_attribute(input)
|
|
498
618
|
end
|
|
499
619
|
end
|
|
@@ -531,9 +651,21 @@ module Moxml
|
|
|
531
651
|
# Match namespace if specified
|
|
532
652
|
if ns && ns != STAR
|
|
533
653
|
if @namespaces && @namespaces[ns]
|
|
534
|
-
# Resolve prefix to URI using namespace mappings
|
|
654
|
+
# Resolve prefix to URI using namespace mappings. An empty
|
|
655
|
+
# URI binding ("xmlns" => "") selects elements in no
|
|
656
|
+
# namespace, e.g. after an xmlns="" undeclaration.
|
|
535
657
|
ns_uri = @namespaces[ns]
|
|
536
|
-
ns_match =
|
|
658
|
+
ns_match = if ns_uri.to_s.empty?
|
|
659
|
+
input.namespace.nil?.or(input.namespace.uri.eq(string("")))
|
|
660
|
+
else
|
|
661
|
+
input.namespace.and(input.namespace.uri.eq(string(ns_uri)))
|
|
662
|
+
end
|
|
663
|
+
elsif ns == "xmlns"
|
|
664
|
+
# Nokogiri-compatible reserved prefix: xmlns:name addresses
|
|
665
|
+
# elements in the default namespace (no prefix).
|
|
666
|
+
ns_match = input.namespace.and(
|
|
667
|
+
input.namespace.prefix.to_s.eq(string("")),
|
|
668
|
+
)
|
|
537
669
|
else
|
|
538
670
|
# No mapping provided - check against element's namespace prefix
|
|
539
671
|
# Need to ensure input.namespace exists first
|
|
@@ -569,7 +701,11 @@ module Moxml
|
|
|
569
701
|
|
|
570
702
|
# Parent node (..)
|
|
571
703
|
def on_parent(_ast, input)
|
|
572
|
-
|
|
704
|
+
if block_given?
|
|
705
|
+
yield input.parent
|
|
706
|
+
else
|
|
707
|
+
input.parent
|
|
708
|
+
end
|
|
573
709
|
end
|
|
574
710
|
|
|
575
711
|
# ===== OPERATORS =====
|
|
@@ -977,10 +1113,9 @@ module Moxml
|
|
|
977
1113
|
conversions << conversion.to_string(arg_var)
|
|
978
1114
|
end
|
|
979
1115
|
|
|
980
|
-
# rubocop:disable
|
|
1116
|
+
# rubocop:disable-next Performance/Sum
|
|
981
1117
|
concatted = assigns.inject(:followed_by)
|
|
982
1118
|
.followed_by(conversions.inject(:+))
|
|
983
|
-
# rubocop:enable Style/RedundantSum, Performance/Sum
|
|
984
1119
|
|
|
985
1120
|
block_given? ? concatted.empty?.if_false { yield concatted } : concatted
|
|
986
1121
|
end
|
data/lib/moxml/xpath/parser.rb
CHANGED
|
@@ -327,11 +327,18 @@ module Moxml
|
|
|
327
327
|
def parse_relative_path
|
|
328
328
|
steps = [parse_step]
|
|
329
329
|
|
|
330
|
-
|
|
330
|
+
# Continue on both step separators: "/step" and the
|
|
331
|
+
# descendant-or-self shorthand "//step". A "//" arrives as a
|
|
332
|
+
# single :dslash (".//step", "a//step") or occasionally as a
|
|
333
|
+
# :slash/:slash or :slash/:dslash pair.
|
|
334
|
+
while !at_end? && match?(:slash, :dslash)
|
|
335
|
+
is_dslash = match?(:dslash)
|
|
331
336
|
advance
|
|
332
|
-
if match?(:slash)
|
|
333
|
-
# Double slash within path: expands to descendant-or-self::node()
|
|
337
|
+
if !is_dslash && match?(:slash, :dslash)
|
|
334
338
|
advance
|
|
339
|
+
is_dslash = true
|
|
340
|
+
end
|
|
341
|
+
if is_dslash
|
|
335
342
|
steps << AST::Node.axis("descendant-or-self", AST::Node.node_type("node"))
|
|
336
343
|
end
|
|
337
344
|
steps << parse_step unless at_end? || match?(:pipe, :rbracket,
|
|
@@ -352,14 +359,10 @@ module Moxml
|
|
|
352
359
|
return AST::Node.parent
|
|
353
360
|
elsif match?(:at)
|
|
354
361
|
advance
|
|
355
|
-
# Attribute: @name or @*
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
else
|
|
360
|
-
name = consume(:name, "Expected attribute name after @")
|
|
361
|
-
node_test = AST::Node.test(nil, name[1])
|
|
362
|
-
end
|
|
362
|
+
# Attribute: @name, @prefix:name, @prefix:* or @* (XPath 1.0
|
|
363
|
+
# §3.2, NameTest). parse_node_test handles prefixed and
|
|
364
|
+
# wildcard names; bare names land in its plain-name branch.
|
|
365
|
+
node_test = parse_node_test
|
|
363
366
|
step = AST::Node.axis("attribute", node_test)
|
|
364
367
|
return parse_predicates(step)
|
|
365
368
|
end
|
|
@@ -385,14 +388,18 @@ module Moxml
|
|
|
385
388
|
if match?(:star)
|
|
386
389
|
advance
|
|
387
390
|
return AST::Node.wildcard
|
|
388
|
-
elsif match?(:node_type)
|
|
391
|
+
elsif match?(:node_type) && peek_is?(:lparen)
|
|
389
392
|
type_name = current_value
|
|
390
393
|
advance
|
|
391
394
|
consume(:lparen, "Expected '(' after node type")
|
|
392
395
|
consume(:rparen, "Expected ')' after node type")
|
|
393
396
|
return AST::Node.node_type(type_name)
|
|
394
|
-
elsif match?(:name, :and, :or, :mod, :div)
|
|
395
|
-
# Accept keywords as valid element names (they're valid XML
|
|
397
|
+
elsif match?(:name, :node_type, :and, :or, :mod, :div)
|
|
398
|
+
# Accept keywords as valid element names (they're valid XML
|
|
399
|
+
# names). Node-type words (text, comment, node) also land here
|
|
400
|
+
# when not followed by '(' — they name elements, either alone
|
|
401
|
+
# ("//text") or as the local part of a prefixed name
|
|
402
|
+
# ("//svg:text").
|
|
396
403
|
name = current_value
|
|
397
404
|
advance
|
|
398
405
|
|
|
@@ -402,7 +409,7 @@ module Moxml
|
|
|
402
409
|
if match?(:star)
|
|
403
410
|
advance
|
|
404
411
|
return AST::Node.test(name, "*")
|
|
405
|
-
elsif match?(:name, :and, :or, :mod, :div)
|
|
412
|
+
elsif match?(:name, :node_type, :and, :or, :mod, :div)
|
|
406
413
|
# Accept keywords as local names too
|
|
407
414
|
local_name = current_value
|
|
408
415
|
advance
|
data/lib/moxml.rb
CHANGED
|
@@ -26,7 +26,6 @@ module Moxml
|
|
|
26
26
|
default_indent: original.default_indent,
|
|
27
27
|
default_line_ending: original.default_line_ending,
|
|
28
28
|
entity_load_mode: original.entity_load_mode,
|
|
29
|
-
entity_encoding: original.entity_encoding,
|
|
30
29
|
restore_entities: original.restore_entities,
|
|
31
30
|
namespace_validation_mode: original.namespace_validation_mode,
|
|
32
31
|
entity_restoration_mode: original.entity_restoration_mode,
|
|
@@ -48,7 +47,6 @@ module Moxml
|
|
|
48
47
|
config.default_indent = saved_values[:default_indent]
|
|
49
48
|
config.default_line_ending = saved_values[:default_line_ending]
|
|
50
49
|
config.entity_load_mode = saved_values[:entity_load_mode]
|
|
51
|
-
config.entity_encoding = saved_values[:entity_encoding]
|
|
52
50
|
config.restore_entities = saved_values[:restore_entities]
|
|
53
51
|
config.namespace_validation_mode = saved_values[:namespace_validation_mode]
|
|
54
52
|
config.entity_restoration_mode = saved_values[:entity_restoration_mode]
|
|
@@ -76,16 +74,18 @@ module Moxml
|
|
|
76
74
|
autoload :Cdata, "moxml/cdata"
|
|
77
75
|
autoload :Comment, "moxml/comment"
|
|
78
76
|
autoload :Attribute, "moxml/attribute"
|
|
77
|
+
autoload :AttributeResolver, "moxml/attribute_resolver"
|
|
79
78
|
autoload :ProcessingInstruction, "moxml/processing_instruction"
|
|
80
79
|
autoload :Declaration, "moxml/declaration"
|
|
81
80
|
autoload :Namespace, "moxml/namespace"
|
|
82
81
|
autoload :Doctype, "moxml/doctype"
|
|
83
82
|
autoload :EntityReference, "moxml/entity_reference"
|
|
84
|
-
autoload :DocumentBuilder, "moxml/document_builder"
|
|
85
83
|
autoload :Builder, "moxml/builder"
|
|
84
|
+
autoload :Entity, "moxml/entity"
|
|
86
85
|
autoload :EntityRegistry, "moxml/entity_registry"
|
|
87
86
|
autoload :NativeAttachment, "moxml/native_attachment"
|
|
88
87
|
autoload :XmlUtils, "moxml/xml_utils"
|
|
88
|
+
autoload :XmlEmitter, "moxml/xml_emitter"
|
|
89
89
|
autoload :Adapter, "moxml/adapter"
|
|
90
90
|
autoload :XPath, "moxml/xpath"
|
|
91
91
|
autoload :SAX, "moxml/sax"
|
|
@@ -110,10 +110,6 @@ RSpec.shared_examples "README Examples" do
|
|
|
110
110
|
describe "Error handling example" do
|
|
111
111
|
it "handles errors as shown in README" do
|
|
112
112
|
context = Moxml.new
|
|
113
|
-
if context.config.adapter_name == :ox
|
|
114
|
-
pending "Ox doesn't have a native XPath"
|
|
115
|
-
end
|
|
116
|
-
|
|
117
113
|
expect do
|
|
118
114
|
context.parse("<invalid>")
|
|
119
115
|
end.to raise_error(Moxml::ParseError)
|
|
@@ -24,20 +24,12 @@ RSpec.shared_examples "XPath Examples" do
|
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
it "finds nodes with namespaces" do
|
|
27
|
-
if context.config.adapter_name == :ox
|
|
28
|
-
pending "Ox doesn't have a native XPath"
|
|
29
|
-
end
|
|
30
|
-
|
|
31
27
|
titles = doc.xpath("//dc:title",
|
|
32
28
|
"dc" => "http://purl.org/dc/elements/1.1/")
|
|
33
29
|
expect(titles.map(&:text)).to eq(%w[First Second])
|
|
34
30
|
end
|
|
35
31
|
|
|
36
32
|
it "finds nodes by attributes" do
|
|
37
|
-
if %i[ox headed_ox].include?(context.config.adapter_name)
|
|
38
|
-
pending "Ox doesn't have native XPath, HeadedOx parser doesn't support .// syntax yet"
|
|
39
|
-
end
|
|
40
|
-
|
|
41
33
|
book = doc.at_xpath('//book[@id="2"]')
|
|
42
34
|
expect(book).not_to be_nil
|
|
43
35
|
title = book.at_xpath(".//dc:title",
|
|
@@ -46,9 +38,6 @@ RSpec.shared_examples "XPath Examples" do
|
|
|
46
38
|
end
|
|
47
39
|
|
|
48
40
|
it "finds nested attributes efficiently" do
|
|
49
|
-
if %i[ox headed_ox].include?(context.config.adapter_name)
|
|
50
|
-
pending "Ox doesn't have native XPath, HeadedOx parser doesn't support .// syntax yet"
|
|
51
|
-
end
|
|
52
41
|
# More efficient - specific path
|
|
53
42
|
titles1 = doc.xpath("//book/dc:title")
|
|
54
43
|
|
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# Leptris needs the unreleased programmatic-document-construction C
|
|
4
|
+
# API; skip its integration matrix until the binding ships it.
|
|
5
|
+
#
|
|
6
|
+
# const_defined? only — deliberately NO require here: loading a native
|
|
7
|
+
# gem at example-group definition time is a load-order side effect
|
|
8
|
+
# (and the only new load-time behavior on this branch). The leptris
|
|
9
|
+
# adapter spec is what loads the gem in its own process.
|
|
10
|
+
def leptris_ready?
|
|
11
|
+
return false unless Object.const_defined?(:Leptris)
|
|
12
|
+
|
|
13
|
+
Leptris::XML::Document.respond_to?(:create)
|
|
14
|
+
end
|
|
15
|
+
|
|
3
16
|
RSpec.describe "Cross-adapter integration" do
|
|
4
17
|
# Integration shared examples - use the names as defined in the files
|
|
5
18
|
all_shared_examples = [
|
|
@@ -18,7 +31,7 @@ RSpec.describe "Cross-adapter integration" do
|
|
|
18
31
|
"Moxml::EntityReference",
|
|
19
32
|
"Moxml::Context",
|
|
20
33
|
"Moxml::Builder",
|
|
21
|
-
"Moxml::
|
|
34
|
+
"Moxml::Parse Behavior",
|
|
22
35
|
"Moxml Integration",
|
|
23
36
|
"Moxml Edge Cases",
|
|
24
37
|
"Attribute Examples",
|
|
@@ -41,6 +54,10 @@ RSpec.describe "Cross-adapter integration" do
|
|
|
41
54
|
end
|
|
42
55
|
end
|
|
43
56
|
|
|
57
|
+
if adapter_name == :leptris && !leptris_ready?
|
|
58
|
+
before { skip "requires leptris with Leptris::XML::Document.create (unreleased C API)" }
|
|
59
|
+
end
|
|
60
|
+
|
|
44
61
|
all_shared_examples.each do |shared_example_name|
|
|
45
62
|
it_behaves_like shared_example_name
|
|
46
63
|
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SAX event parity across adapters: every bridge must translate its
|
|
4
|
+
# native SAX stream into the same Moxml event sequence — same element
|
|
5
|
+
# names (qualified), same attribute/declaration split (nil key = the
|
|
6
|
+
# default namespace), same characters.
|
|
7
|
+
RSpec.describe "SAX parity across adapters" do
|
|
8
|
+
let(:xml) do
|
|
9
|
+
<<~XML
|
|
10
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
11
|
+
<root xmlns="http://d.org" xmlns:p="http://p.org" kind="a"><p:child id="1">text</p:child></root>
|
|
12
|
+
XML
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def recorded_events(adapter_name)
|
|
16
|
+
events = []
|
|
17
|
+
handler = Class.new(Moxml::SAX::Handler) do
|
|
18
|
+
define_method(:track) { |*event| events << event }
|
|
19
|
+
define_method(:on_start_document) { track(:start_document) }
|
|
20
|
+
define_method(:on_end_document) { track(:end_document) }
|
|
21
|
+
define_method(:on_start_element) { |n, a, ns| track(:start_element, n, a, ns) }
|
|
22
|
+
define_method(:on_end_element) { |n| track(:end_element, n) }
|
|
23
|
+
define_method(:on_characters) { |t| track(:characters, t) }
|
|
24
|
+
end
|
|
25
|
+
Moxml.new(adapter_name).sax_parse(xml, handler.new)
|
|
26
|
+
events
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def assert_parity(adapter_name)
|
|
30
|
+
# REXML and Oga report document-level whitespace (between the
|
|
31
|
+
# declaration and the root) as characters; parity is over the
|
|
32
|
+
# meaningful event stream.
|
|
33
|
+
events = recorded_events(adapter_name)
|
|
34
|
+
.reject { |event| event[0] == :characters && event[1].to_s.strip.empty? }
|
|
35
|
+
|
|
36
|
+
expect(events.first).to eq([:start_document])
|
|
37
|
+
expect(events[1]).to eq(
|
|
38
|
+
[:start_element, "root",
|
|
39
|
+
{ "kind" => "a" },
|
|
40
|
+
{ nil => "http://d.org", "p" => "http://p.org" }],
|
|
41
|
+
)
|
|
42
|
+
expect(events[2]).to eq([:start_element, "p:child", { "id" => "1" }, {}])
|
|
43
|
+
expect(events).to include([:characters, "text"])
|
|
44
|
+
expect(events).to include([:end_element, "p:child"])
|
|
45
|
+
expect(events).to include([:end_element, "root"])
|
|
46
|
+
expect(events.last).to eq([:end_document])
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
adapters = %i[nokogiri oga rexml ox headed_ox libxml]
|
|
50
|
+
adapters << :leptris if Object.const_defined?(:Leptris) &&
|
|
51
|
+
Leptris::XML::Document.respond_to?(:create)
|
|
52
|
+
|
|
53
|
+
adapters.each do |adapter_name|
|
|
54
|
+
it "emits the same event stream via #{adapter_name}" do
|
|
55
|
+
assert_parity(adapter_name)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -80,9 +80,6 @@ RSpec.shared_examples "Moxml Edge Cases" do
|
|
|
80
80
|
|
|
81
81
|
describe "namespace edge cases" do
|
|
82
82
|
it "handles default namespace changes" do
|
|
83
|
-
if context.config.adapter_name == :ox
|
|
84
|
-
pending "Ox doesn't have a native XPath"
|
|
85
|
-
end
|
|
86
83
|
if context.config.adapter_name == :headed_ox
|
|
87
84
|
skip "HeadedOx limitation: Namespace methods not implemented in adapter. Requires Ox namespace API enhancement. See docs/_pages/headed-ox-limitations.adoc"
|
|
88
85
|
end
|
|
@@ -106,9 +103,6 @@ RSpec.shared_examples "Moxml Edge Cases" do
|
|
|
106
103
|
end
|
|
107
104
|
|
|
108
105
|
it "handles recursive namespace definitions" do
|
|
109
|
-
if context.config.adapter_name == :ox
|
|
110
|
-
pending "Ox doesn't have a native XPath"
|
|
111
|
-
end
|
|
112
106
|
if context.config.adapter_name == :headed_ox
|
|
113
107
|
skip "HeadedOx limitation: Namespace methods not implemented in adapter. Requires Ox namespace API enhancement. See docs/_pages/headed-ox-limitations.adoc"
|
|
114
108
|
end
|
|
@@ -132,10 +126,6 @@ RSpec.shared_examples "Moxml Edge Cases" do
|
|
|
132
126
|
if context.config.adapter_name == :headed_ox
|
|
133
127
|
skip "HeadedOx limitation: Namespace-prefixed attribute access needs Ox namespace API enhancement. See docs/_pages/headed-ox-limitations.adoc"
|
|
134
128
|
end
|
|
135
|
-
if context.config.adapter_name == :ox
|
|
136
|
-
skip "Ox doesn't have a native XPath"
|
|
137
|
-
end
|
|
138
|
-
|
|
139
129
|
xml = <<~XML
|
|
140
130
|
<root xmlns:a="http://a.org" xmlns:b="http://b.org">
|
|
141
131
|
<element a:id="1" b:id="2"/>
|
data/spec/integration/shared_examples/high_level/{document_builder_behavior.rb → parse_behavior.rb}
RENAMED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
RSpec.shared_examples "Moxml::
|
|
3
|
+
RSpec.shared_examples "Moxml::Parse Behavior" do
|
|
4
4
|
let(:context) { Moxml.new }
|
|
5
|
-
let(:builder) { Moxml::DocumentBuilder.new(context) }
|
|
6
5
|
|
|
7
6
|
describe "#build" do
|
|
8
7
|
it "builds a document model from native document" do
|
|
@@ -56,9 +56,6 @@ RSpec.shared_examples "Moxml Integration" do
|
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
it "handles xpath queries" do
|
|
59
|
-
if context.config.adapter_name == :ox
|
|
60
|
-
pending "Ox doesn't support namespace-aware XPath with predicates"
|
|
61
|
-
end
|
|
62
59
|
if context.config.adapter_name == :headed_ox
|
|
63
60
|
skip "HeadedOx limitation: Namespace-aware XPath with predicates needs investigation. See docs/_pages/headed-ox-limitations.adoc"
|
|
64
61
|
end
|
|
@@ -76,9 +73,6 @@ RSpec.shared_examples "Moxml Integration" do
|
|
|
76
73
|
|
|
77
74
|
describe "namespace handling" do
|
|
78
75
|
it "handles complex namespace scenarios" do
|
|
79
|
-
if context.config.adapter_name == :ox
|
|
80
|
-
pending "Ox doesn't have a native XPath"
|
|
81
|
-
end
|
|
82
76
|
if context.config.adapter_name == :headed_ox
|
|
83
77
|
skip "HeadedOx limitation: Namespace methods not implemented in adapter. Requires Ox namespace API enhancement. See docs/_pages/headed-ox-limitations.adoc"
|
|
84
78
|
end
|
|
@@ -125,10 +119,6 @@ RSpec.shared_examples "Moxml Integration" do
|
|
|
125
119
|
if context.config.adapter_name == :headed_ox
|
|
126
120
|
skip "HeadedOx limitation: Parent setter not implemented. Requires Ox node reparenting API. See docs/_pages/headed-ox-limitations.adoc"
|
|
127
121
|
end
|
|
128
|
-
if context.config.adapter_name == :ox
|
|
129
|
-
skip "Ox doesn't have a native XPath"
|
|
130
|
-
end
|
|
131
|
-
|
|
132
122
|
# Move nodes
|
|
133
123
|
b_node = doc.at_xpath("//b")
|
|
134
124
|
a_node = doc.at_xpath("//a")
|
|
@@ -65,6 +65,65 @@ RSpec.shared_examples "Moxml::Attribute" do
|
|
|
65
65
|
expect(attribute.namespace.prefix).to eq("other")
|
|
66
66
|
expect(attribute.to_s).to eq('other:attr="value"')
|
|
67
67
|
end
|
|
68
|
+
|
|
69
|
+
it "replaces by expanded name across prefix spellings" do
|
|
70
|
+
element["ns:attr"] = "first"
|
|
71
|
+
element.add_namespace("alias", "http://example.org")
|
|
72
|
+
element["alias:attr"] = "second"
|
|
73
|
+
|
|
74
|
+
expect(element["ns:attr"]).to eq("second")
|
|
75
|
+
expect(element.attributes.size).to eq(1)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
describe "expanded-name writes (AttributeResolver)" do
|
|
80
|
+
before do
|
|
81
|
+
element.add_namespace("p", "http://x.org")
|
|
82
|
+
element.add_namespace("q", "http://x.org")
|
|
83
|
+
element["q:type"] = "namespaced"
|
|
84
|
+
element["type"] = "bare"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it "does not clobber a namespaced attribute sharing the local name" do
|
|
88
|
+
element["type"] = "changed"
|
|
89
|
+
|
|
90
|
+
expect(element["type"]).to eq("changed")
|
|
91
|
+
expect(element["q:type"]).to eq("namespaced")
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it "overwrites by expanded name regardless of prefix spelling" do
|
|
95
|
+
element["p:type"] = "overwritten"
|
|
96
|
+
|
|
97
|
+
expect(element["q:type"]).to eq("overwritten")
|
|
98
|
+
expect(element.attributes.size).to eq(2)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it "stores writes through undeclared prefixes raw for later resolution" do
|
|
102
|
+
element["z:type"] = "value"
|
|
103
|
+
|
|
104
|
+
# No declaration in scope: the read resolves to nothing, but
|
|
105
|
+
# the attribute is stored (detached builds attach under the
|
|
106
|
+
# declaring ancestor later).
|
|
107
|
+
expect(element["z:type"]).to be_nil
|
|
108
|
+
expect(element.attributes.size).to eq(3)
|
|
109
|
+
|
|
110
|
+
element.add_namespace("z", "http://z.org")
|
|
111
|
+
expect(element["z:type"]).to eq("value")
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it "removes the bare attribute without touching the namespaced one" do
|
|
115
|
+
element.remove_attribute("type")
|
|
116
|
+
|
|
117
|
+
expect(element["type"]).to be_nil
|
|
118
|
+
expect(element["q:type"]).to eq("namespaced")
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
it "removes by expanded name regardless of prefix spelling" do
|
|
122
|
+
element.remove_attribute("p:type")
|
|
123
|
+
|
|
124
|
+
expect(element["q:type"]).to be_nil
|
|
125
|
+
expect(element["type"]).to eq("bare")
|
|
126
|
+
end
|
|
68
127
|
end
|
|
69
128
|
|
|
70
129
|
describe "manipulation" do
|
|
@@ -162,4 +221,43 @@ RSpec.shared_examples "Moxml::Attribute" do
|
|
|
162
221
|
end
|
|
163
222
|
end
|
|
164
223
|
end
|
|
224
|
+
|
|
225
|
+
describe "unified XML-correct name resolution" do
|
|
226
|
+
let(:context) { Moxml.new }
|
|
227
|
+
let(:doc) do
|
|
228
|
+
context.parse(
|
|
229
|
+
%(<root xmlns:p="urn:1"><child xmlns:q="urn:1" q:type="v" type="plain" xml:lang="en"/></root>),
|
|
230
|
+
)
|
|
231
|
+
end
|
|
232
|
+
let(:child) { doc.root.children.find(&:element?) }
|
|
233
|
+
|
|
234
|
+
it "matches prefixed lookups by expanded name across redeclared prefixes" do
|
|
235
|
+
# p and q both bind urn:1; the attribute is written q:type
|
|
236
|
+
expect(child["p:type"]).to eq("v")
|
|
237
|
+
expect(child["q:type"]).to eq("v")
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
it "keeps bare and namespaced attributes distinct" do
|
|
241
|
+
expect(child["type"]).to eq("plain")
|
|
242
|
+
expect(child[:type]).to eq("plain")
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
it "resolves the prebound xml prefix without any declaration" do
|
|
246
|
+
expect(child["xml:lang"]).to eq("en")
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
it "returns nil for undeclared prefixes — no prefix-string fallback" do
|
|
250
|
+
expect(child["undeclared:type"]).to be_nil
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
it "never treats namespace declarations as attributes" do
|
|
254
|
+
expect(child["xmlns:q"]).to be_nil
|
|
255
|
+
expect(child["xmlns"]).to be_nil
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
it "attribute(name) agrees with []" do
|
|
259
|
+
expect(child.attribute("p:type").value).to eq("v")
|
|
260
|
+
expect(child.attribute("undeclared:type")).to be_nil
|
|
261
|
+
end
|
|
262
|
+
end
|
|
165
263
|
end
|
|
@@ -170,6 +170,32 @@ RSpec.shared_examples "Moxml::Namespace" do
|
|
|
170
170
|
expect(prefixes).to include("xs")
|
|
171
171
|
end
|
|
172
172
|
|
|
173
|
+
it "sees ancestor declarations made after a first read (cache invalidation)" do
|
|
174
|
+
root = doc.create_element("root")
|
|
175
|
+
root.add_namespace("a", "http://a.org")
|
|
176
|
+
child = doc.create_element("child")
|
|
177
|
+
root.add_child(child)
|
|
178
|
+
child.in_scope_namespaces # materialize the scope cache
|
|
179
|
+
|
|
180
|
+
root.add_namespace("b", "http://b.org")
|
|
181
|
+
|
|
182
|
+
expect(child.in_scope_namespaces.map(&:prefix)).to include("b")
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
it "recomputes the scope after the subtree is reparented" do
|
|
186
|
+
source = doc.create_element("source")
|
|
187
|
+
source.add_namespace("s", "http://s.org")
|
|
188
|
+
child = doc.create_element("child")
|
|
189
|
+
source.add_child(child)
|
|
190
|
+
child.in_scope_namespaces # materialize the scope cache
|
|
191
|
+
|
|
192
|
+
target = doc.create_element("target")
|
|
193
|
+
child.remove
|
|
194
|
+
target.add_child(child)
|
|
195
|
+
|
|
196
|
+
expect(child.in_scope_namespaces.map(&:prefix)).not_to include("s")
|
|
197
|
+
end
|
|
198
|
+
|
|
173
199
|
it "collects namespaces from multiple ancestor levels" do
|
|
174
200
|
root = doc.create_element("root")
|
|
175
201
|
root.add_namespace("xs", "http://www.w3.org/2001/XMLSchema")
|