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/adapter/libxml.rb
CHANGED
|
@@ -8,7 +8,6 @@ module Moxml
|
|
|
8
8
|
module Adapter
|
|
9
9
|
class Libxml < Base
|
|
10
10
|
autoload :EntityRefRegistry, "moxml/adapter/libxml/entity_ref_registry"
|
|
11
|
-
autoload :EntityRestorer, "moxml/adapter/libxml/entity_restorer"
|
|
12
11
|
|
|
13
12
|
# Wrapper class to store DOCTYPE information
|
|
14
13
|
class DoctypeWrapper
|
|
@@ -28,15 +27,7 @@ module Moxml
|
|
|
28
27
|
end
|
|
29
28
|
|
|
30
29
|
def to_xml
|
|
31
|
-
|
|
32
|
-
if @external_id && !@external_id.empty?
|
|
33
|
-
output << " PUBLIC \"#{@external_id}\""
|
|
34
|
-
output << " \"#{@system_id}\"" if @system_id
|
|
35
|
-
elsif @system_id && !@system_id.empty?
|
|
36
|
-
output << " SYSTEM \"#{@system_id}\""
|
|
37
|
-
end
|
|
38
|
-
output << ">"
|
|
39
|
-
output
|
|
30
|
+
XmlEmitter.doctype_xml(@name, @external_id, @system_id)
|
|
40
31
|
end
|
|
41
32
|
end
|
|
42
33
|
|
|
@@ -57,6 +48,7 @@ module Moxml
|
|
|
57
48
|
|
|
58
49
|
WRAPPER_NODE_TYPE_MAP = {
|
|
59
50
|
DoctypeWrapper => :doctype,
|
|
51
|
+
CustomizedLibxml::Declaration => :declaration,
|
|
60
52
|
CustomizedLibxml::Element => :element,
|
|
61
53
|
CustomizedLibxml::Text => :text,
|
|
62
54
|
CustomizedLibxml::Cdata => :cdata,
|
|
@@ -91,7 +83,7 @@ module Moxml
|
|
|
91
83
|
xml_string = preprocess_entities(xml_string)
|
|
92
84
|
|
|
93
85
|
# Extract DOCTYPE before parsing
|
|
94
|
-
doctype_match = xml_string.match(/<!DOCTYPE\s+(
|
|
86
|
+
doctype_match = xml_string.match(/<!DOCTYPE\s+([^\s>]+)(?:\s+PUBLIC\s+"([^"]+)"\s+"([^"]+)"|\s+SYSTEM\s+"([^"]+)")?\s*>/i)
|
|
95
87
|
|
|
96
88
|
native_doc = begin
|
|
97
89
|
# Handle both string and file inputs
|
|
@@ -128,10 +120,7 @@ module Moxml
|
|
|
128
120
|
|
|
129
121
|
ctx = _context || Context.new(:libxml)
|
|
130
122
|
# Single parse path: wrap libxml's already-complete C-parsed tree
|
|
131
|
-
# directly (same pattern as
|
|
132
|
-
# DocumentBuilder.build path walked the entire parsed tree and
|
|
133
|
-
# re-added every node to a fresh moxml-managed document, which
|
|
134
|
-
# made parse O(N) Ruby work on top of an already-complete parse.
|
|
123
|
+
# directly (same pattern as the other adapters).
|
|
135
124
|
# Doctype/declaration/PI attachments set above remain on
|
|
136
125
|
# native_doc, so the serialize path still sees them.
|
|
137
126
|
#
|
|
@@ -141,7 +130,7 @@ module Moxml
|
|
|
141
130
|
# path; the restoration walk is just one of potentially several
|
|
142
131
|
# post-processing steps and doesn't fork the construction.
|
|
143
132
|
doc = Document.new(native_doc, ctx)
|
|
144
|
-
|
|
133
|
+
Entity::Restorer.new(doc).run if ctx.config.restore_entities
|
|
145
134
|
doc
|
|
146
135
|
end
|
|
147
136
|
|
|
@@ -226,7 +215,7 @@ module Moxml
|
|
|
226
215
|
return :unknown unless node
|
|
227
216
|
|
|
228
217
|
# Fast path: native libxml nodes are the vast majority during
|
|
229
|
-
# parse traversal (
|
|
218
|
+
# parse traversal (wrapping visits raw libxml children).
|
|
230
219
|
# Skip the wrapper checks below for them.
|
|
231
220
|
if node.is_a?(::LibXML::XML::Node)
|
|
232
221
|
return NATIVE_NODE_TYPE_MAP[node.node_type] || :unknown
|
|
@@ -379,6 +368,11 @@ module Moxml
|
|
|
379
368
|
native_doc&.root
|
|
380
369
|
end
|
|
381
370
|
|
|
371
|
+
def line_number(node)
|
|
372
|
+
native = unpatch_node(node)
|
|
373
|
+
native.is_a?(::LibXML::XML::Node) ? native.line_num : nil
|
|
374
|
+
end
|
|
375
|
+
|
|
382
376
|
def attributes(element)
|
|
383
377
|
native_elem = unpatch_node(element)
|
|
384
378
|
return [] unless native_elem
|
|
@@ -437,9 +431,13 @@ module Moxml
|
|
|
437
431
|
native_elem.attributes.get_attribute(name_str)
|
|
438
432
|
end
|
|
439
433
|
else
|
|
440
|
-
#
|
|
441
|
-
|
|
442
|
-
|
|
434
|
+
# node[]= is ambiguous when a namespace-bound attribute
|
|
435
|
+
# shares the local name (it clobbers that attribute);
|
|
436
|
+
# create or replace the no-namespace attribute explicitly.
|
|
437
|
+
native_elem.each_attr do |attr|
|
|
438
|
+
attr.remove! if !attr.ns && attr.name == name_str
|
|
439
|
+
end if native_elem.attributes?
|
|
440
|
+
::LibXML::XML::Attr.new(native_elem, name_str, value_str)
|
|
443
441
|
end
|
|
444
442
|
end
|
|
445
443
|
|
|
@@ -467,30 +465,46 @@ module Moxml
|
|
|
467
465
|
native_elem = unpatch_node(element)
|
|
468
466
|
return nil unless native_elem
|
|
469
467
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
# If name contains ':', try to get as namespaced attribute
|
|
475
|
-
if name.to_s.include?(":")
|
|
476
|
-
prefix, local_name = name.to_s.split(":", 2)
|
|
468
|
+
name_s = name.to_s
|
|
469
|
+
if name_s.include?(":")
|
|
470
|
+
prefix, local_name = name_s.split(":", 2)
|
|
477
471
|
# Try to find attribute by namespace
|
|
478
472
|
if native_elem.attributes?
|
|
479
473
|
native_elem.each_attr do |attr|
|
|
480
|
-
if
|
|
474
|
+
if [local_name, name_s].include?(attr.name)
|
|
481
475
|
# Check if attribute's namespace matches the prefix
|
|
482
476
|
if attr.ns && attr.ns.prefix == prefix
|
|
483
477
|
return attr.value
|
|
484
|
-
elsif attr.name ==
|
|
478
|
+
elsif attr.name == name_s
|
|
485
479
|
# Fallback: attribute name includes the prefix
|
|
486
480
|
return attr.value
|
|
487
481
|
end
|
|
488
482
|
end
|
|
489
483
|
end
|
|
490
484
|
end
|
|
485
|
+
|
|
486
|
+
return nil
|
|
491
487
|
end
|
|
492
488
|
|
|
493
|
-
|
|
489
|
+
# Bare name: node[name_s] is ambiguous when a namespaced
|
|
490
|
+
# attribute shares the local name (issue #94), so prefer the
|
|
491
|
+
# namespace-less attribute explicitly.
|
|
492
|
+
if native_elem.attributes?
|
|
493
|
+
namespaced_value = nil
|
|
494
|
+
native_elem.each_attr do |attr|
|
|
495
|
+
next if attr.name.to_s.start_with?("xmlns")
|
|
496
|
+
next unless attr.name == name_s
|
|
497
|
+
|
|
498
|
+
if attr.ns.nil?
|
|
499
|
+
return attr.value
|
|
500
|
+
elsif namespaced_value.nil?
|
|
501
|
+
namespaced_value = attr.value
|
|
502
|
+
end
|
|
503
|
+
end
|
|
504
|
+
return namespaced_value unless namespaced_value.nil?
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
native_elem[name_s]
|
|
494
508
|
end
|
|
495
509
|
|
|
496
510
|
def remove_attribute(element, name)
|
|
@@ -502,6 +516,10 @@ module Moxml
|
|
|
502
516
|
attr&.remove!
|
|
503
517
|
end
|
|
504
518
|
|
|
519
|
+
def remove_attribute_native(attr)
|
|
520
|
+
attr&.remove!
|
|
521
|
+
end
|
|
522
|
+
|
|
505
523
|
def set_attribute_name(attribute, new_name)
|
|
506
524
|
# LibXML attributes cannot be renamed directly
|
|
507
525
|
# We must work at the element level
|
|
@@ -730,26 +748,52 @@ module Moxml
|
|
|
730
748
|
|
|
731
749
|
def set_text_content(node, content)
|
|
732
750
|
native_node = unpatch_node(node)
|
|
733
|
-
|
|
751
|
+
return unless native_node.is_a?(::LibXML::XML::Node)
|
|
752
|
+
|
|
753
|
+
# Text wrapper: swap with new_text to preserve verbatim storage.
|
|
754
|
+
# Element wrapper: let libxml manage the child text node.
|
|
755
|
+
if native_node.text?
|
|
756
|
+
replace_native_verbatim(node, ::LibXML::XML::Node.new_text(content.to_s))
|
|
757
|
+
else
|
|
758
|
+
native_node.content = content.to_s
|
|
759
|
+
end
|
|
734
760
|
end
|
|
735
761
|
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
762
|
+
# Replace `old_native` with `fresh`, preserving position. Both
|
|
763
|
+
# neighbor pointers are set explicitly to match `replace`'s
|
|
764
|
+
# semantics (see #replace above).
|
|
765
|
+
def swap_native_in_place(old_native, fresh)
|
|
766
|
+
parent = old_native.parent
|
|
767
|
+
return if parent.nil?
|
|
768
|
+
|
|
769
|
+
prev_sibling = old_native.prev
|
|
770
|
+
next_sibling = old_native.next
|
|
771
|
+
old_native.remove!
|
|
772
|
+
prev_sibling.next = fresh if prev_sibling
|
|
773
|
+
next_sibling.prev = fresh if next_sibling
|
|
774
|
+
parent << fresh unless prev_sibling || next_sibling
|
|
775
|
+
end
|
|
776
|
+
private :swap_native_in_place
|
|
777
|
+
|
|
778
|
+
# libxml-ruby's node.content= pre-escapes; building a fresh node via
|
|
779
|
+
# new_<kind> stores content verbatim. Swap in place and re-point the
|
|
780
|
+
# wrapper.
|
|
781
|
+
def replace_native_verbatim(node, fresh)
|
|
782
|
+
native = unpatch_node(node)
|
|
783
|
+
return unless native.is_a?(::LibXML::XML::Node)
|
|
784
|
+
|
|
785
|
+
swap_native_in_place(native, fresh)
|
|
786
|
+
node.replace_native!(fresh) if node.is_a?(CustomizedLibxml::Node)
|
|
787
|
+
end
|
|
788
|
+
private :replace_native_verbatim
|
|
741
789
|
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
.gsub(">", ">")
|
|
746
|
-
.gsub("&", "&")
|
|
790
|
+
def cdata_content(node)
|
|
791
|
+
# libxml stores CDATA payload verbatim; no decoding needed.
|
|
792
|
+
unpatch_node(node)&.content
|
|
747
793
|
end
|
|
748
794
|
|
|
749
795
|
def set_cdata_content(node, content)
|
|
750
|
-
|
|
751
|
-
# CDATA content should NOT be escaped
|
|
752
|
-
native_node.content = content.to_s if native_node
|
|
796
|
+
replace_native_verbatim(node, ::LibXML::XML::Node.new_cdata(content.to_s))
|
|
753
797
|
end
|
|
754
798
|
|
|
755
799
|
def comment_content(node)
|
|
@@ -758,8 +802,7 @@ module Moxml
|
|
|
758
802
|
end
|
|
759
803
|
|
|
760
804
|
def set_comment_content(node, content)
|
|
761
|
-
|
|
762
|
-
native_node.content = content.to_s if native_node
|
|
805
|
+
replace_native_verbatim(node, ::LibXML::XML::Node.new_comment(content.to_s))
|
|
763
806
|
end
|
|
764
807
|
|
|
765
808
|
def processing_instruction_target(node)
|
|
@@ -768,22 +811,18 @@ module Moxml
|
|
|
768
811
|
end
|
|
769
812
|
|
|
770
813
|
def processing_instruction_content(node)
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
# LibXML may HTML-escape the content, un-escape it
|
|
774
|
-
return nil unless content
|
|
775
|
-
|
|
776
|
-
content.gsub(""", '"')
|
|
777
|
-
.gsub("'", "'")
|
|
778
|
-
.gsub("<", "<")
|
|
779
|
-
.gsub(">", ">")
|
|
780
|
-
.gsub("&", "&")
|
|
814
|
+
# XML 1.0 §2.6: PI content is verbatim — no entity resolution.
|
|
815
|
+
unpatch_node(node)&.content
|
|
781
816
|
end
|
|
782
817
|
|
|
783
818
|
def set_processing_instruction_content(node, content)
|
|
784
819
|
native_node = unpatch_node(node)
|
|
785
|
-
|
|
786
|
-
|
|
820
|
+
return unless native_node.is_a?(::LibXML::XML::Node)
|
|
821
|
+
|
|
822
|
+
replace_native_verbatim(
|
|
823
|
+
node,
|
|
824
|
+
::LibXML::XML::Node.new_pi(native_node.name, content.to_s),
|
|
825
|
+
)
|
|
787
826
|
end
|
|
788
827
|
|
|
789
828
|
def create_native_namespace(element, prefix, uri)
|
|
@@ -804,9 +843,10 @@ module Moxml
|
|
|
804
843
|
|
|
805
844
|
def set_namespace(element, ns)
|
|
806
845
|
native_elem = unpatch_node(element)
|
|
807
|
-
return unless native_elem
|
|
846
|
+
return element unless native_elem
|
|
808
847
|
|
|
809
|
-
native_elem.namespaces.namespace = ns
|
|
848
|
+
native_elem.namespaces.namespace = ns if ns
|
|
849
|
+
element
|
|
810
850
|
end
|
|
811
851
|
|
|
812
852
|
def namespace(element)
|
|
@@ -867,14 +907,18 @@ module Moxml
|
|
|
867
907
|
# LibXML requires ALL prefixes in the XPath to be registered
|
|
868
908
|
ns_context = build_xpath_namespaces(native_node, namespaces)
|
|
869
909
|
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
910
|
+
found = if ns_context.empty?
|
|
911
|
+
native_node.find(expression)
|
|
912
|
+
else
|
|
913
|
+
native_node.find(expression, ns_context)
|
|
914
|
+
end
|
|
915
|
+
|
|
916
|
+
# find returns an XPath::Object for node results, scalars for
|
|
917
|
+
# count()/string()/boolean functions (adapter contract)
|
|
918
|
+
return found if found.is_a?(Float) || found.is_a?(String) ||
|
|
919
|
+
[true, false].include?(found)
|
|
875
920
|
|
|
876
|
-
|
|
877
|
-
results.map { |n| patch_node(n) }
|
|
921
|
+
found.to_a.map { |n| patch_node(n) }
|
|
878
922
|
rescue ::LibXML::XML::Error => e
|
|
879
923
|
raise Moxml::XPathError.new(
|
|
880
924
|
e.message,
|
|
@@ -987,7 +1031,7 @@ module Moxml
|
|
|
987
1031
|
end
|
|
988
1032
|
|
|
989
1033
|
# Shallow duplication: copies the node itself (name, attrs, namespaces)
|
|
990
|
-
# but NOT its descendants
|
|
1034
|
+
# but NOT its descendants (deep duplication composes this).
|
|
991
1035
|
# walks the source tree and re-adds children one at a time via
|
|
992
1036
|
# add_child, so a deep copy here would be done only to be stripped
|
|
993
1037
|
# by replace_children, then rebuilt — O(N²) waste on parse.
|
|
@@ -1013,7 +1057,8 @@ module Moxml
|
|
|
1013
1057
|
node
|
|
1014
1058
|
end
|
|
1015
1059
|
when :element
|
|
1016
|
-
|
|
1060
|
+
# Public dup contract: a full deep copy of the subtree
|
|
1061
|
+
deep_duplicate_node(native_node)
|
|
1017
1062
|
when :text
|
|
1018
1063
|
::LibXML::XML::Node.new_text(native_node.content)
|
|
1019
1064
|
when :cdata
|
|
@@ -1058,15 +1103,6 @@ module Moxml
|
|
|
1058
1103
|
end
|
|
1059
1104
|
end
|
|
1060
1105
|
|
|
1061
|
-
def prepare_for_new_document(node, target_doc)
|
|
1062
|
-
return node unless node && target_doc
|
|
1063
|
-
|
|
1064
|
-
# For LibXML, we need to duplicate ALL nodes to avoid
|
|
1065
|
-
# document ownership issues. Simply importing doesn't work
|
|
1066
|
-
# because nodes from the parsed document still have references.
|
|
1067
|
-
duplicate_node(node)
|
|
1068
|
-
end
|
|
1069
|
-
|
|
1070
1106
|
def has_declaration?(native_doc, wrapper)
|
|
1071
1107
|
decl = attachments.get(native_doc, :declaration)
|
|
1072
1108
|
if decl
|
|
@@ -1109,9 +1145,9 @@ module Moxml
|
|
|
1109
1145
|
|
|
1110
1146
|
seen_ns[prefix] = true
|
|
1111
1147
|
output << if prefix.nil? || prefix.empty?
|
|
1112
|
-
" xmlns=\"#{
|
|
1148
|
+
" xmlns=\"#{XmlEmitter.escape_attribute(uri)}\""
|
|
1113
1149
|
else
|
|
1114
|
-
" xmlns:#{prefix}=\"#{
|
|
1150
|
+
" xmlns:#{prefix}=\"#{XmlEmitter.escape_attribute(uri)}\""
|
|
1115
1151
|
end
|
|
1116
1152
|
end
|
|
1117
1153
|
end
|
|
@@ -1127,7 +1163,7 @@ module Moxml
|
|
|
1127
1163
|
else
|
|
1128
1164
|
attr.name
|
|
1129
1165
|
end
|
|
1130
|
-
output << " #{attr_name}=\"#{
|
|
1166
|
+
output << " #{attr_name}=\"#{XmlEmitter.escape_attribute(attr.value)}\""
|
|
1131
1167
|
end
|
|
1132
1168
|
end
|
|
1133
1169
|
|
|
@@ -1167,7 +1203,7 @@ module Moxml
|
|
|
1167
1203
|
when ::LibXML::XML::Node::ELEMENT_NODE
|
|
1168
1204
|
serialize_element(node)
|
|
1169
1205
|
when ::LibXML::XML::Node::TEXT_NODE
|
|
1170
|
-
escape_text(node.content)
|
|
1206
|
+
XmlEmitter.escape_text(node.content)
|
|
1171
1207
|
when ::LibXML::XML::Node::CDATA_SECTION_NODE
|
|
1172
1208
|
"<![CDATA[#{node.content}]]>"
|
|
1173
1209
|
when ::LibXML::XML::Node::COMMENT_NODE
|
|
@@ -1179,28 +1215,6 @@ module Moxml
|
|
|
1179
1215
|
end
|
|
1180
1216
|
end
|
|
1181
1217
|
|
|
1182
|
-
def escape_text(text)
|
|
1183
|
-
text.to_s
|
|
1184
|
-
.gsub("&", "&")
|
|
1185
|
-
.gsub("<", "<")
|
|
1186
|
-
.gsub(">", ">")
|
|
1187
|
-
end
|
|
1188
|
-
|
|
1189
|
-
ESCAPE_XML_RE = /[&<>"]/
|
|
1190
|
-
ESCAPE_XML_MAP = { "&" => "&", "<" => "<", ">" => ">",
|
|
1191
|
-
'"' => """ }.freeze
|
|
1192
|
-
private_constant :ESCAPE_XML_RE, :ESCAPE_XML_MAP
|
|
1193
|
-
|
|
1194
|
-
def escape_xml(text)
|
|
1195
|
-
# One gsub pass with a Hash replacement allocates a single new
|
|
1196
|
-
# string. The previous chained gsubs allocated three throwaway
|
|
1197
|
-
# strings on every call (very hot for attribute-heavy XML).
|
|
1198
|
-
str = text.is_a?(String) ? text : text.to_s
|
|
1199
|
-
return str unless str.match?(ESCAPE_XML_RE)
|
|
1200
|
-
|
|
1201
|
-
str.gsub(ESCAPE_XML_RE, ESCAPE_XML_MAP)
|
|
1202
|
-
end
|
|
1203
|
-
|
|
1204
1218
|
def import_and_add(doc, element, child)
|
|
1205
1219
|
return unless element && child
|
|
1206
1220
|
|
|
@@ -1357,9 +1371,9 @@ module Moxml
|
|
|
1357
1371
|
|
|
1358
1372
|
def format_ns_declaration(prefix, uri)
|
|
1359
1373
|
if prefix.nil? || prefix.empty?
|
|
1360
|
-
" xmlns=\"#{
|
|
1374
|
+
" xmlns=\"#{XmlEmitter.escape_attribute(uri)}\""
|
|
1361
1375
|
else
|
|
1362
|
-
" xmlns:#{prefix}=\"#{
|
|
1376
|
+
" xmlns:#{prefix}=\"#{XmlEmitter.escape_attribute(uri)}\""
|
|
1363
1377
|
end
|
|
1364
1378
|
end
|
|
1365
1379
|
|
|
@@ -1370,7 +1384,7 @@ module Moxml
|
|
|
1370
1384
|
next if attr.name.start_with?("xmlns")
|
|
1371
1385
|
|
|
1372
1386
|
attr_name = attr.ns&.prefix ? "#{attr.ns.prefix}:#{attr.name}" : attr.name
|
|
1373
|
-
output << " #{attr_name}=\"#{
|
|
1387
|
+
output << " #{attr_name}=\"#{XmlEmitter.escape_attribute(attr.value)}\""
|
|
1374
1388
|
end
|
|
1375
1389
|
end
|
|
1376
1390
|
|
|
@@ -1510,11 +1524,6 @@ module Moxml
|
|
|
1510
1524
|
end
|
|
1511
1525
|
end
|
|
1512
1526
|
|
|
1513
|
-
def remove_indentation(xml_string)
|
|
1514
|
-
# Remove all newlines and extra spaces between tags
|
|
1515
|
-
xml_string.gsub(/>\s+</, "><").gsub(/\n\s*/, "")
|
|
1516
|
-
end
|
|
1517
|
-
|
|
1518
1527
|
def collect_namespace_definitions(node)
|
|
1519
1528
|
ns_defs = {}
|
|
1520
1529
|
|
|
@@ -1693,13 +1702,24 @@ module Moxml
|
|
|
1693
1702
|
@handler.on_end_document
|
|
1694
1703
|
end
|
|
1695
1704
|
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1705
|
+
# libxml strips prefixes and namespace declarations from the
|
|
1706
|
+
# plain on_start_element callback; the _ns variants carry
|
|
1707
|
+
# them. The namespaces hash uses nil for the default
|
|
1708
|
+
# declaration — the same key convention as every other
|
|
1709
|
+
# bridge's split. Attribute name prefixes are lost by the
|
|
1710
|
+
# binding in both callbacks (documented limitation).
|
|
1711
|
+
def on_start_element_ns(name, attributes, prefix, _uri, namespaces)
|
|
1712
|
+
normalized = (attributes || {}).map { |k, v| [k.to_s, v] }
|
|
1713
|
+
attr_hash, = split_attributes_and_namespaces(normalized) do |v|
|
|
1714
|
+
Moxml::Adapter::Base.decode_entities(v)
|
|
1715
|
+
end
|
|
1716
|
+
qualified = prefix ? "#{prefix}:#{name}" : name.to_s
|
|
1717
|
+
@handler.on_start_element(qualified, attr_hash, namespaces || {})
|
|
1699
1718
|
end
|
|
1700
1719
|
|
|
1701
|
-
def
|
|
1702
|
-
|
|
1720
|
+
def on_end_element_ns(name, prefix, _uri)
|
|
1721
|
+
qualified = prefix ? "#{prefix}:#{name}" : name.to_s
|
|
1722
|
+
@handler.on_end_element(qualified)
|
|
1703
1723
|
end
|
|
1704
1724
|
|
|
1705
1725
|
def on_characters(chars)
|
|
@@ -144,6 +144,7 @@ module Moxml
|
|
|
144
144
|
|
|
145
145
|
def set_namespace(element, ns)
|
|
146
146
|
element.namespace = ns
|
|
147
|
+
element
|
|
147
148
|
end
|
|
148
149
|
|
|
149
150
|
def namespace(element)
|
|
@@ -216,21 +217,36 @@ module Moxml
|
|
|
216
217
|
document.is_a?(::Nokogiri::XML::Document) ? document.root : document.children.first
|
|
217
218
|
end
|
|
218
219
|
|
|
219
|
-
def
|
|
220
|
-
|
|
220
|
+
def line_number(node)
|
|
221
|
+
node.line
|
|
221
222
|
end
|
|
222
223
|
|
|
223
|
-
def
|
|
224
|
-
|
|
224
|
+
def attribute_element(attr)
|
|
225
|
+
attr.parent
|
|
225
226
|
end
|
|
226
227
|
|
|
227
228
|
def set_attribute(element, name, value)
|
|
228
229
|
element[name.to_s] = value.to_s
|
|
229
230
|
end
|
|
230
231
|
|
|
232
|
+
def attributes(element)
|
|
233
|
+
# attribute_nodes, not attributes.values: the attributes Hash
|
|
234
|
+
# is keyed by local name, so xmi:type and type collide and
|
|
235
|
+
# one is silently dropped (issue #94).
|
|
236
|
+
element.attribute_nodes
|
|
237
|
+
end
|
|
238
|
+
|
|
231
239
|
def get_attribute(element, name)
|
|
232
|
-
|
|
233
|
-
|
|
240
|
+
name_s = name.to_s
|
|
241
|
+
if name_s.include?(":")
|
|
242
|
+
prefix, local = name_s.split(":", 2)
|
|
243
|
+
href = element.namespace_definitions
|
|
244
|
+
.find { |ns| ns.prefix == prefix }&.href
|
|
245
|
+
element.attribute_with_ns(local, href)
|
|
246
|
+
else
|
|
247
|
+
element.attribute_with_ns(name_s, nil) ||
|
|
248
|
+
element.attributes[name_s]
|
|
249
|
+
end
|
|
234
250
|
end
|
|
235
251
|
|
|
236
252
|
def get_attribute_value(element, name)
|
|
@@ -360,7 +376,10 @@ module Moxml
|
|
|
360
376
|
end
|
|
361
377
|
|
|
362
378
|
def xpath(node, expression, namespaces = nil)
|
|
363
|
-
node.xpath(expression, namespaces)
|
|
379
|
+
result = node.xpath(expression, namespaces)
|
|
380
|
+
# Adapter contract: Array<native> | scalar (count(),
|
|
381
|
+
# string-length(), boolean functions return scalars).
|
|
382
|
+
result.is_a?(Enumerable) ? result.to_a : result
|
|
364
383
|
rescue ::Nokogiri::XML::XPath::SyntaxError => e
|
|
365
384
|
raise Moxml::XPathError.new(
|
|
366
385
|
e.message,
|
|
@@ -471,7 +490,9 @@ module Moxml
|
|
|
471
490
|
end
|
|
472
491
|
|
|
473
492
|
def start_element(name, attributes = [])
|
|
474
|
-
attr_hash, ns_hash = split_attributes_and_namespaces(attributes)
|
|
493
|
+
attr_hash, ns_hash = split_attributes_and_namespaces(attributes) do |v|
|
|
494
|
+
Moxml::Adapter::Base.decode_entities(v)
|
|
495
|
+
end
|
|
475
496
|
@handler.on_start_element(name, attr_hash, ns_hash)
|
|
476
497
|
end
|
|
477
498
|
|