moxml 0.1.14 → 0.1.16
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/.rubocop_todo.yml +117 -66
- data/Gemfile +1 -0
- data/README.adoc +11 -9
- data/Rakefile +34 -1
- data/TODO.remaining/1-entity-reference-adapter-support.md +157 -0
- data/TODO.remaining/2-entity-restoration-model-driven.md +169 -0
- data/TODO.remaining/3-entity-reference-test-coverage.md +170 -0
- data/TODO.remaining/4-lenient-entities-mode.md +106 -0
- data/TODO.remaining/5-fixture-integrity.md +65 -0
- data/TODO.remaining/6-ox-element-ordering-bug.md +36 -0
- data/TODO.remaining/7-headed-ox-limitations.md +95 -0
- data/TODO.remaining/8-xpath-predicate-gaps.md +68 -0
- data/TODO.remaining/9-cleanup-hygiene.md +42 -0
- data/TODO.remaining/README.md +54 -0
- data/benchmarks/generate_report.rb +1 -1
- data/docs/_pages/configuration.adoc +22 -19
- data/docs/_tutorials/namespace-handling.adoc +5 -5
- data/lib/moxml/adapter/base.rb +22 -3
- data/lib/moxml/adapter/customized_libxml/declaration.rb +1 -1
- data/lib/moxml/adapter/customized_libxml/entity_reference.rb +23 -0
- data/lib/moxml/adapter/customized_libxml.rb +18 -0
- data/lib/moxml/adapter/customized_oga.rb +10 -0
- data/lib/moxml/adapter/customized_ox/entity_reference.rb +25 -0
- data/lib/moxml/adapter/customized_ox.rb +12 -0
- data/lib/moxml/adapter/customized_rexml/entity_reference.rb +19 -0
- data/lib/moxml/adapter/customized_rexml/formatter.rb +44 -20
- data/lib/moxml/adapter/customized_rexml.rb +11 -0
- data/lib/moxml/adapter/headed_ox.rb +37 -14
- data/lib/moxml/adapter/libxml.rb +233 -119
- data/lib/moxml/adapter/nokogiri.rb +22 -11
- data/lib/moxml/adapter/oga.rb +64 -25
- data/lib/moxml/adapter/ox.rb +198 -42
- data/lib/moxml/adapter/rexml.rb +64 -13
- data/lib/moxml/attribute.rb +3 -0
- data/lib/moxml/builder.rb +78 -24
- data/lib/moxml/config.rb +24 -7
- data/lib/moxml/declaration.rb +4 -2
- data/lib/moxml/document.rb +8 -1
- data/lib/moxml/document_builder.rb +44 -37
- data/lib/moxml/element.rb +18 -5
- data/lib/moxml/entity_registry.rb +51 -1
- data/lib/moxml/native_attachment.rb +65 -0
- data/lib/moxml/node.rb +39 -50
- data/lib/moxml/node_set.rb +43 -15
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xml_utils.rb +1 -1
- data/lib/moxml/xpath/compiler.rb +4 -1
- data/lib/moxml.rb +1 -0
- data/scripts/format_xml.rb +16 -0
- data/scripts/pretty_format_xml.rb +14 -0
- data/spec/consistency/round_trip_spec.rb +3 -30
- data/spec/integration/all_adapters_spec.rb +1 -0
- data/spec/integration/headed_ox_integration_spec.rb +0 -2
- data/spec/integration/shared_examples/edge_cases.rb +7 -4
- data/spec/integration/shared_examples/integration_workflows.rb +3 -3
- data/spec/integration/shared_examples/node_wrappers/cdata_behavior.rb +1 -1
- data/spec/integration/shared_examples/node_wrappers/entity_reference_behavior.rb +224 -0
- data/spec/integration/shared_examples/node_wrappers/node_behavior.rb +1 -1
- data/spec/moxml/adapter/headed_ox_spec.rb +8 -8
- data/spec/moxml/adapter/oga_spec.rb +46 -0
- data/spec/moxml/adapter/shared_examples/adapter_contract.rb +1 -12
- data/spec/moxml/allocation_benchmark_spec.rb +96 -0
- data/spec/moxml/allocation_guard_spec.rb +282 -0
- data/spec/moxml/builder_spec.rb +256 -0
- data/spec/moxml/config_spec.rb +11 -11
- data/spec/moxml/doctype_spec.rb +41 -0
- data/spec/moxml/lazy_parse_spec.rb +115 -0
- data/spec/moxml/namespace_uri_validation_spec.rb +11 -3
- data/spec/moxml/node_cache_spec.rb +110 -0
- data/spec/moxml/node_set_cache_spec.rb +90 -0
- data/spec/moxml/xml_utils_spec.rb +32 -0
- data/spec/moxml/xpath/axes_spec.rb +1 -1
- data/spec/moxml/xpath/compiler_spec.rb +2 -2
- data/spec/moxml/xpath/functions/position_functions_spec.rb +5 -5
- data/spec/moxml/xpath/functions/special_functions_spec.rb +1 -1
- data/spec/performance/memory_usage_spec.rb +0 -4
- data/spec/support/allocation_helper.rb +165 -0
- data/spec/support/w3c_namespace_helpers.rb +2 -1
- metadata +29 -2
data/lib/moxml/adapter/ox.rb
CHANGED
|
@@ -3,9 +3,7 @@
|
|
|
3
3
|
require_relative "base"
|
|
4
4
|
require "ox"
|
|
5
5
|
require "stringio"
|
|
6
|
-
require_relative "customized_ox
|
|
7
|
-
require_relative "customized_ox/attribute"
|
|
8
|
-
require_relative "customized_ox/namespace"
|
|
6
|
+
require_relative "customized_ox"
|
|
9
7
|
|
|
10
8
|
# insert :parent methods to all Ox classes inherit the Node class
|
|
11
9
|
Ox::Node.attr_accessor :parent
|
|
@@ -13,20 +11,27 @@ module Moxml
|
|
|
13
11
|
module Adapter
|
|
14
12
|
class Ox < Base
|
|
15
13
|
class << self
|
|
14
|
+
def attachments
|
|
15
|
+
@attachments ||= Moxml::NativeAttachment.new
|
|
16
|
+
end
|
|
17
|
+
|
|
16
18
|
def set_root(doc, element)
|
|
17
19
|
replace_children(doc, [element])
|
|
18
20
|
end
|
|
19
21
|
|
|
20
|
-
def parse(xml,
|
|
22
|
+
def parse(xml, options = {}, _context = nil)
|
|
21
23
|
native_doc = begin
|
|
22
24
|
result = ::Ox.parse(xml)
|
|
23
25
|
|
|
24
26
|
# result can be either Document or Element
|
|
25
27
|
if result.is_a?(::Ox::Document)
|
|
28
|
+
assign_parents(result)
|
|
29
|
+
validate_single_root(result) if options[:strict]
|
|
26
30
|
result
|
|
27
31
|
else
|
|
28
32
|
doc = ::Ox::Document.new
|
|
29
33
|
doc << result
|
|
34
|
+
assign_parents(doc)
|
|
30
35
|
doc
|
|
31
36
|
end
|
|
32
37
|
rescue ::Ox::ParseError => e
|
|
@@ -37,7 +42,7 @@ module Moxml
|
|
|
37
42
|
end
|
|
38
43
|
|
|
39
44
|
ctx = _context || Context.new(:ox)
|
|
40
|
-
|
|
45
|
+
Document.new(native_doc, ctx)
|
|
41
46
|
end
|
|
42
47
|
|
|
43
48
|
# SAX parsing implementation for Ox
|
|
@@ -50,7 +55,7 @@ module Moxml
|
|
|
50
55
|
bridge = OxSAXBridge.new(handler)
|
|
51
56
|
|
|
52
57
|
# Parse using Ox's SAX parser
|
|
53
|
-
xml_string = xml.
|
|
58
|
+
xml_string = xml.is_a?(IO) || xml.is_a?(StringIO) ? xml.read : xml.to_s
|
|
54
59
|
|
|
55
60
|
begin
|
|
56
61
|
::Ox.sax_parse(bridge, StringIO.new(xml_string))
|
|
@@ -69,7 +74,6 @@ module Moxml
|
|
|
69
74
|
|
|
70
75
|
def create_native_element(name, _owner_doc = nil)
|
|
71
76
|
element = ::Ox::Element.new(name)
|
|
72
|
-
element.instance_variable_set(:@attributes, {})
|
|
73
77
|
element
|
|
74
78
|
end
|
|
75
79
|
|
|
@@ -77,6 +81,14 @@ module Moxml
|
|
|
77
81
|
content
|
|
78
82
|
end
|
|
79
83
|
|
|
84
|
+
def create_native_entity_reference(name)
|
|
85
|
+
::Moxml::Adapter::CustomizedOx::EntityReference.new(name)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def entity_reference_name(node)
|
|
89
|
+
node.name if node.is_a?(::Moxml::Adapter::CustomizedOx::EntityReference)
|
|
90
|
+
end
|
|
91
|
+
|
|
80
92
|
def create_native_cdata(content, _owner_doc = nil)
|
|
81
93
|
::Ox::CData.new(content)
|
|
82
94
|
end
|
|
@@ -121,11 +133,11 @@ module Moxml
|
|
|
121
133
|
end
|
|
122
134
|
|
|
123
135
|
def set_namespace(element, ns)
|
|
124
|
-
return unless element.
|
|
136
|
+
return unless element.is_a?(::Ox::Element) || element.is_a?(::Ox::Node)
|
|
125
137
|
|
|
126
138
|
prefix = ns.prefix
|
|
127
139
|
# attributes don't have attributes but can have a namespace prefix
|
|
128
|
-
if element.
|
|
140
|
+
if element.is_a?(::Ox::Element)
|
|
129
141
|
set_attribute(element, ns.expanded_prefix,
|
|
130
142
|
ns.uri)
|
|
131
143
|
end
|
|
@@ -136,8 +148,7 @@ module Moxml
|
|
|
136
148
|
|
|
137
149
|
def namespace(element)
|
|
138
150
|
prefix =
|
|
139
|
-
if element.
|
|
140
|
-
# attribute
|
|
151
|
+
if element.is_a?(::Moxml::Adapter::CustomizedOx::Attribute)
|
|
141
152
|
element.prefix
|
|
142
153
|
elsif element.name.include?(":")
|
|
143
154
|
element.name.split(":").first
|
|
@@ -145,7 +156,7 @@ module Moxml
|
|
|
145
156
|
attr_name = ["xmlns", prefix].compact.join(":")
|
|
146
157
|
|
|
147
158
|
([element] + ancestors(element)).each do |node|
|
|
148
|
-
next unless node.
|
|
159
|
+
next unless node.is_a?(::Ox::Element) && node.attributes
|
|
149
160
|
|
|
150
161
|
if node[attr_name]
|
|
151
162
|
return ::Moxml::Adapter::CustomizedOx::Namespace.new(
|
|
@@ -176,6 +187,7 @@ module Moxml
|
|
|
176
187
|
when ::Ox::Instruct then :processing_instruction
|
|
177
188
|
when ::Ox::Element then :element
|
|
178
189
|
when ::Ox::DocType then :doctype
|
|
190
|
+
when ::Moxml::Adapter::CustomizedOx::EntityReference then :entity_reference
|
|
179
191
|
when ::Moxml::Adapter::CustomizedOx::Namespace then :banespace
|
|
180
192
|
when ::Moxml::Adapter::CustomizedOx::Attribute then :attribute
|
|
181
193
|
else :unknown
|
|
@@ -194,10 +206,9 @@ module Moxml
|
|
|
194
206
|
end
|
|
195
207
|
|
|
196
208
|
def set_node_name(node, name)
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
node.value = name
|
|
209
|
+
case node
|
|
210
|
+
when ::Ox::Element then node.name = name
|
|
211
|
+
when ::Ox::Instruct then node.value = name
|
|
201
212
|
end
|
|
202
213
|
end
|
|
203
214
|
|
|
@@ -219,7 +230,7 @@ module Moxml
|
|
|
219
230
|
else node
|
|
220
231
|
end
|
|
221
232
|
|
|
222
|
-
new_node.parent = parent if new_node.
|
|
233
|
+
new_node.parent = parent if new_node.is_a?(::Ox::Node)
|
|
223
234
|
|
|
224
235
|
new_node
|
|
225
236
|
end
|
|
@@ -231,18 +242,25 @@ module Moxml
|
|
|
231
242
|
node.value]
|
|
232
243
|
# when ::Moxml::Adapter::CustomizedOx::Attribute then { node.name => node.value }
|
|
233
244
|
when ::Moxml::Adapter::CustomizedOx::Text then node.value
|
|
245
|
+
when ::Moxml::Adapter::CustomizedOx::EntityReference then node
|
|
234
246
|
else node
|
|
235
247
|
end
|
|
236
248
|
end
|
|
237
249
|
|
|
238
250
|
def children(node)
|
|
239
|
-
return [] unless node.
|
|
251
|
+
return [] unless node.is_a?(::Ox::Element) || node.is_a?(::Ox::Document)
|
|
240
252
|
|
|
241
|
-
node.nodes || []
|
|
253
|
+
result = node.nodes || []
|
|
254
|
+
# Ox doesn't set parent references during parsing.
|
|
255
|
+
# Set them here so parent/sibling navigation works.
|
|
256
|
+
result.each do |child|
|
|
257
|
+
child.parent = node if child.respond_to?(:parent=)
|
|
258
|
+
end
|
|
259
|
+
result
|
|
242
260
|
end
|
|
243
261
|
|
|
244
262
|
def parent(node)
|
|
245
|
-
node.parent if node.
|
|
263
|
+
node.parent if node.is_a?(::Ox::Node)
|
|
246
264
|
end
|
|
247
265
|
|
|
248
266
|
def next_sibling(node)
|
|
@@ -272,9 +290,7 @@ module Moxml
|
|
|
272
290
|
end
|
|
273
291
|
|
|
274
292
|
def attributes(element)
|
|
275
|
-
unless element.
|
|
276
|
-
return []
|
|
277
|
-
end
|
|
293
|
+
return [] unless element.is_a?(::Ox::Element) && element.attributes
|
|
278
294
|
|
|
279
295
|
element.attributes.filter_map do |name, value|
|
|
280
296
|
next if name.to_s.start_with?("xmlns")
|
|
@@ -324,7 +340,7 @@ module Moxml
|
|
|
324
340
|
end
|
|
325
341
|
|
|
326
342
|
def get_attribute(element, name)
|
|
327
|
-
return unless element.
|
|
343
|
+
return unless element.is_a?(::Ox::HasAttrs) && element.attributes
|
|
328
344
|
unless element.attributes.key?(name.to_s) || element.attributes.key?(name.to_s.to_sym)
|
|
329
345
|
return
|
|
330
346
|
end
|
|
@@ -342,7 +358,7 @@ module Moxml
|
|
|
342
358
|
end
|
|
343
359
|
|
|
344
360
|
def remove_attribute(element, name)
|
|
345
|
-
return unless element.
|
|
361
|
+
return unless element.is_a?(::Ox::HasAttrs) && element.attributes
|
|
346
362
|
|
|
347
363
|
element.attributes.delete(name.to_s)
|
|
348
364
|
element.attributes.delete(name.to_s.to_sym)
|
|
@@ -367,15 +383,24 @@ module Moxml
|
|
|
367
383
|
end
|
|
368
384
|
end
|
|
369
385
|
|
|
370
|
-
child.parent = element if child.
|
|
386
|
+
child.parent = element if child.is_a?(::Ox::Node)
|
|
371
387
|
element.nodes ||= []
|
|
372
388
|
element.nodes << child
|
|
389
|
+
|
|
390
|
+
# Mark document if EntityReference is added (avoids tree scan in serialize)
|
|
391
|
+
if child.is_a?(::Moxml::Adapter::CustomizedOx::EntityReference)
|
|
392
|
+
root = element
|
|
393
|
+
while root.is_a?(::Ox::Node) && root.parent
|
|
394
|
+
root = root.parent
|
|
395
|
+
end
|
|
396
|
+
attachments.set(root, :has_entity_refs, true) if root
|
|
397
|
+
end
|
|
373
398
|
end
|
|
374
399
|
|
|
375
400
|
def add_previous_sibling(node, sibling)
|
|
376
401
|
return unless (parent = parent(node))
|
|
377
402
|
|
|
378
|
-
if sibling.
|
|
403
|
+
if sibling.is_a?(::Ox::Node)
|
|
379
404
|
sibling.parent&.nodes&.delete(sibling)
|
|
380
405
|
sibling.parent = parent
|
|
381
406
|
end
|
|
@@ -386,7 +411,7 @@ module Moxml
|
|
|
386
411
|
def add_next_sibling(node, sibling)
|
|
387
412
|
return unless (parent = parent(node))
|
|
388
413
|
|
|
389
|
-
if sibling.
|
|
414
|
+
if sibling.is_a?(::Ox::Node)
|
|
390
415
|
sibling.parent&.nodes&.delete(sibling)
|
|
391
416
|
sibling.parent = parent
|
|
392
417
|
end
|
|
@@ -421,7 +446,7 @@ module Moxml
|
|
|
421
446
|
|
|
422
447
|
return unless (parent = parent(node))
|
|
423
448
|
|
|
424
|
-
new_node.parent = parent if new_node.
|
|
449
|
+
new_node.parent = parent if new_node.is_a?(::Ox::Node)
|
|
425
450
|
idx = parent.nodes.index(node)
|
|
426
451
|
parent.nodes[idx] = new_node if idx
|
|
427
452
|
end
|
|
@@ -429,20 +454,40 @@ module Moxml
|
|
|
429
454
|
def replace_children(node, new_children)
|
|
430
455
|
node.remove_children_by_path("*")
|
|
431
456
|
new_children.each do |child|
|
|
432
|
-
child.parent = node if child.
|
|
457
|
+
child.parent = node if child.is_a?(::Ox::Node)
|
|
433
458
|
node << child
|
|
434
459
|
end
|
|
435
460
|
node
|
|
436
461
|
end
|
|
437
462
|
|
|
463
|
+
def assign_parents(node, parent = nil)
|
|
464
|
+
node.parent = parent if node.respond_to?(:parent=) && parent
|
|
465
|
+
return unless node.respond_to?(:nodes)
|
|
466
|
+
|
|
467
|
+
node.nodes&.each do |child|
|
|
468
|
+
assign_parents(child, node)
|
|
469
|
+
end
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
def validate_single_root(document)
|
|
473
|
+
elements = document.nodes&.grep(::Ox::Element) || []
|
|
474
|
+
return unless elements.size > 1
|
|
475
|
+
|
|
476
|
+
raise Moxml::ParseError.new(
|
|
477
|
+
"Multiple root elements found",
|
|
478
|
+
source: nil,
|
|
479
|
+
)
|
|
480
|
+
end
|
|
481
|
+
|
|
438
482
|
def text_content(node)
|
|
439
483
|
return "" if node.nil?
|
|
440
484
|
|
|
441
485
|
case node
|
|
442
486
|
when String then node.to_s
|
|
443
487
|
when ::Moxml::Adapter::CustomizedOx::Text then node.value
|
|
488
|
+
when ::Moxml::Adapter::CustomizedOx::EntityReference then ""
|
|
444
489
|
else
|
|
445
|
-
return "" unless node.
|
|
490
|
+
return "" unless node.is_a?(::Ox::Element) || node.is_a?(::Ox::Document)
|
|
446
491
|
|
|
447
492
|
node.nodes.map do |n|
|
|
448
493
|
text_content(n)
|
|
@@ -451,7 +496,7 @@ module Moxml
|
|
|
451
496
|
end
|
|
452
497
|
|
|
453
498
|
def inner_text(node)
|
|
454
|
-
return "" unless node.
|
|
499
|
+
return "" unless node.is_a?(::Ox::Element) || node.is_a?(::Ox::Document)
|
|
455
500
|
|
|
456
501
|
node.nodes.grep(String).join
|
|
457
502
|
end
|
|
@@ -499,7 +544,7 @@ module Moxml
|
|
|
499
544
|
|
|
500
545
|
def namespace_definitions(node)
|
|
501
546
|
([node] + ancestors(node)).reverse.each_with_object({}) do |n, namespaces|
|
|
502
|
-
next unless n.
|
|
547
|
+
next unless n.is_a?(::Ox::Element) && n.attributes
|
|
503
548
|
|
|
504
549
|
n.attributes.each do |name, value|
|
|
505
550
|
next unless name.to_s.start_with?("xmlns")
|
|
@@ -575,18 +620,35 @@ module Moxml
|
|
|
575
620
|
end
|
|
576
621
|
|
|
577
622
|
def serialize(node, options = {})
|
|
623
|
+
# Fast path: skip EntityReference scan for documents (most common case)
|
|
624
|
+
if node.is_a?(::Ox::Document) &&
|
|
625
|
+
!attachments.get(node, :has_entity_refs)
|
|
626
|
+
return serialize_standard(node, options)
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
if tree_has_entity_references?(node)
|
|
630
|
+
serialize_custom(node, options)
|
|
631
|
+
else
|
|
632
|
+
serialize_standard(node, options)
|
|
633
|
+
end
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
def has_declaration?(native_doc, _wrapper)
|
|
637
|
+
# Ox stores declaration in document attributes
|
|
638
|
+
native_doc[:version] || native_doc[:encoding] || native_doc[:standalone]
|
|
639
|
+
end
|
|
640
|
+
|
|
641
|
+
private
|
|
642
|
+
|
|
643
|
+
def serialize_standard(node, options = {})
|
|
578
644
|
output = ""
|
|
579
645
|
if node.is_a?(::Ox::Document)
|
|
580
|
-
# Check if we should include declaration
|
|
581
|
-
# Priority: explicit option > document attributes
|
|
582
646
|
should_include_decl = if options.key?(:no_declaration)
|
|
583
647
|
!options[:no_declaration]
|
|
584
648
|
else
|
|
585
|
-
# Check if document has declaration attributes
|
|
586
649
|
node[:version] || node[:encoding] || node[:standalone]
|
|
587
650
|
end
|
|
588
651
|
|
|
589
|
-
# Only add declaration if should_include_decl is true
|
|
590
652
|
if should_include_decl
|
|
591
653
|
version = node[:version] || "1.0"
|
|
592
654
|
encoding = options[:encoding] || node[:encoding]
|
|
@@ -598,8 +660,7 @@ module Moxml
|
|
|
598
660
|
end
|
|
599
661
|
|
|
600
662
|
ox_options = {
|
|
601
|
-
indent: -1,
|
|
602
|
-
# with_xml: true,
|
|
663
|
+
indent: -1,
|
|
603
664
|
with_instructions: true,
|
|
604
665
|
encoding: options[:encoding],
|
|
605
666
|
no_empty: options[:expand_empty],
|
|
@@ -607,7 +668,102 @@ module Moxml
|
|
|
607
668
|
output + ::Ox.dump(node, ox_options)
|
|
608
669
|
end
|
|
609
670
|
|
|
610
|
-
|
|
671
|
+
def tree_has_entity_references?(node)
|
|
672
|
+
case node
|
|
673
|
+
when ::Moxml::Adapter::CustomizedOx::EntityReference
|
|
674
|
+
true
|
|
675
|
+
when ::Ox::Element
|
|
676
|
+
node.nodes&.any? do |child|
|
|
677
|
+
tree_has_entity_references?(child)
|
|
678
|
+
end || false
|
|
679
|
+
when ::Ox::Document
|
|
680
|
+
node.nodes&.any? do |child|
|
|
681
|
+
tree_has_entity_references?(child)
|
|
682
|
+
end || false
|
|
683
|
+
else
|
|
684
|
+
false
|
|
685
|
+
end
|
|
686
|
+
end
|
|
687
|
+
|
|
688
|
+
def serialize_custom(node, options = {})
|
|
689
|
+
output = +""
|
|
690
|
+
if node.is_a?(::Ox::Document)
|
|
691
|
+
should_include_decl = if options.key?(:no_declaration)
|
|
692
|
+
!options[:no_declaration]
|
|
693
|
+
else
|
|
694
|
+
node[:version] || node[:encoding] || node[:standalone]
|
|
695
|
+
end
|
|
696
|
+
if should_include_decl
|
|
697
|
+
version = node[:version] || "1.0"
|
|
698
|
+
encoding = options[:encoding] || node[:encoding]
|
|
699
|
+
standalone = node[:standalone]
|
|
700
|
+
output << "<?xml version=\"#{version}\""
|
|
701
|
+
output << " encoding=\"#{encoding}\"" if encoding
|
|
702
|
+
output << " standalone=\"#{standalone}\"" if standalone
|
|
703
|
+
output << "?>"
|
|
704
|
+
end
|
|
705
|
+
(node.nodes || []).each do |child|
|
|
706
|
+
output << serialize_node_custom(child)
|
|
707
|
+
end
|
|
708
|
+
else
|
|
709
|
+
output << serialize_node_custom(node)
|
|
710
|
+
end
|
|
711
|
+
output
|
|
712
|
+
end
|
|
713
|
+
|
|
714
|
+
def serialize_node_custom(node)
|
|
715
|
+
case node
|
|
716
|
+
when ::Ox::Element then serialize_element_custom(node)
|
|
717
|
+
when String then escape_xml_text(node)
|
|
718
|
+
when ::Moxml::Adapter::CustomizedOx::Text then escape_xml_text(node.value)
|
|
719
|
+
when ::Moxml::Adapter::CustomizedOx::EntityReference then "&#{node.name};"
|
|
720
|
+
when ::Ox::CData then "<![CDATA[#{node.value}]]>"
|
|
721
|
+
when ::Ox::Comment then "<!--#{node.value}-->"
|
|
722
|
+
when ::Ox::Instruct then "<?#{node.target} #{node.value || ''}?>"
|
|
723
|
+
when ::Ox::DocType then "<!DOCTYPE #{node.value}>"
|
|
724
|
+
else ""
|
|
725
|
+
end
|
|
726
|
+
end
|
|
727
|
+
|
|
728
|
+
def serialize_element_custom(elem)
|
|
729
|
+
output = "<#{elem.name}"
|
|
730
|
+
elem.attributes.each do |name, value|
|
|
731
|
+
output << " #{name}=\"#{escape_xml_attribute(value.to_s)}\""
|
|
732
|
+
end
|
|
733
|
+
|
|
734
|
+
if elem.nodes.nil? || elem.nodes.empty?
|
|
735
|
+
output << "/>"
|
|
736
|
+
return output
|
|
737
|
+
end
|
|
738
|
+
|
|
739
|
+
output << ">"
|
|
740
|
+
elem.nodes.each do |child|
|
|
741
|
+
output << serialize_node_custom(child)
|
|
742
|
+
end
|
|
743
|
+
output << "</#{elem.name}>"
|
|
744
|
+
output
|
|
745
|
+
end
|
|
746
|
+
|
|
747
|
+
def escape_xml_text(text)
|
|
748
|
+
text.to_s.gsub(/[<>&]/) do |match|
|
|
749
|
+
case match
|
|
750
|
+
when "<" then "<"
|
|
751
|
+
when ">" then ">"
|
|
752
|
+
when "&" then "&"
|
|
753
|
+
end
|
|
754
|
+
end
|
|
755
|
+
end
|
|
756
|
+
|
|
757
|
+
def escape_xml_attribute(value)
|
|
758
|
+
value.to_s.gsub(/[<>&"]/) do |match|
|
|
759
|
+
case match
|
|
760
|
+
when "<" then "<"
|
|
761
|
+
when ">" then ">"
|
|
762
|
+
when "&" then "&"
|
|
763
|
+
when '"' then """
|
|
764
|
+
end
|
|
765
|
+
end
|
|
766
|
+
end
|
|
611
767
|
|
|
612
768
|
# Translate a subset of XPath to Ox locate() syntax
|
|
613
769
|
# Supports: //element, /path/to/element, .//element, element[@attr]
|
|
@@ -659,7 +815,7 @@ module Moxml
|
|
|
659
815
|
|
|
660
816
|
result = nil
|
|
661
817
|
traverse(root) do |node|
|
|
662
|
-
next unless node.
|
|
818
|
+
next unless node.is_a?(::Ox::Element) || node.is_a?(::Ox::Document)
|
|
663
819
|
|
|
664
820
|
node.nodes&.each do |child|
|
|
665
821
|
if child.equal?(target_node)
|
|
@@ -676,7 +832,7 @@ module Moxml
|
|
|
676
832
|
return unless node
|
|
677
833
|
|
|
678
834
|
yield node
|
|
679
|
-
return unless node.
|
|
835
|
+
return unless node.is_a?(::Ox::Element) || node.is_a?(::Ox::Document)
|
|
680
836
|
|
|
681
837
|
node.nodes&.each { |child| traverse(child, &block) }
|
|
682
838
|
end
|
data/lib/moxml/adapter/rexml.rb
CHANGED
|
@@ -4,12 +4,16 @@ require_relative "base"
|
|
|
4
4
|
require "rexml/document"
|
|
5
5
|
require "rexml/xpath"
|
|
6
6
|
require "set"
|
|
7
|
-
require_relative "customized_rexml
|
|
7
|
+
require_relative "customized_rexml"
|
|
8
8
|
|
|
9
9
|
module Moxml
|
|
10
10
|
module Adapter
|
|
11
11
|
class Rexml < Base
|
|
12
12
|
class << self
|
|
13
|
+
def attachments
|
|
14
|
+
@attachments ||= Moxml::NativeAttachment.new
|
|
15
|
+
end
|
|
16
|
+
|
|
13
17
|
def parse(xml, options = {}, _context = nil)
|
|
14
18
|
# Handle frozen strings by creating a mutable copy
|
|
15
19
|
processed_xml = if xml.frozen?
|
|
@@ -54,7 +58,7 @@ module Moxml
|
|
|
54
58
|
|
|
55
59
|
bridge = REXMLSAX2Bridge.new(handler)
|
|
56
60
|
|
|
57
|
-
xml_string = xml.
|
|
61
|
+
xml_string = xml.is_a?(IO) || xml.is_a?(StringIO) ? xml.read : xml.to_s
|
|
58
62
|
source = ::REXML::IOSource.new(StringIO.new(xml_string))
|
|
59
63
|
|
|
60
64
|
parser = ::REXML::Parsers::SAX2Parser.new(source)
|
|
@@ -77,6 +81,14 @@ module Moxml
|
|
|
77
81
|
::REXML::Text.new(content.to_s, true, nil)
|
|
78
82
|
end
|
|
79
83
|
|
|
84
|
+
def create_native_entity_reference(name)
|
|
85
|
+
::Moxml::Adapter::CustomizedRexml::EntityReference.new(name)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def entity_reference_name(node)
|
|
89
|
+
node.name if node.is_a?(::Moxml::Adapter::CustomizedRexml::EntityReference)
|
|
90
|
+
end
|
|
91
|
+
|
|
80
92
|
def create_native_cdata(content, _owner_doc = nil)
|
|
81
93
|
::REXML::CData.new(content.to_s)
|
|
82
94
|
end
|
|
@@ -124,6 +136,7 @@ module Moxml
|
|
|
124
136
|
when ::REXML::Instruction then :processing_instruction
|
|
125
137
|
when ::REXML::DocType then :doctype
|
|
126
138
|
when ::REXML::XMLDecl then :declaration
|
|
139
|
+
when ::Moxml::Adapter::CustomizedRexml::EntityReference then :entity_reference
|
|
127
140
|
else :unknown
|
|
128
141
|
end
|
|
129
142
|
end
|
|
@@ -149,13 +162,15 @@ module Moxml
|
|
|
149
162
|
end
|
|
150
163
|
|
|
151
164
|
def duplicate_node(node)
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
165
|
+
if node.respond_to?(:deep_clone)
|
|
166
|
+
node.deep_clone
|
|
167
|
+
else
|
|
168
|
+
Marshal.load(Marshal.dump(node))
|
|
169
|
+
end
|
|
155
170
|
end
|
|
156
171
|
|
|
157
172
|
def children(node)
|
|
158
|
-
return [] unless node.
|
|
173
|
+
return [] unless node.is_a?(::REXML::Parent)
|
|
159
174
|
|
|
160
175
|
# Get all children and filter out empty text nodes between elements
|
|
161
176
|
result = node.children.reject do |child|
|
|
@@ -164,6 +179,10 @@ module Moxml
|
|
|
164
179
|
!(child.next_sibling.nil? && child.previous_sibling.nil?)
|
|
165
180
|
end
|
|
166
181
|
|
|
182
|
+
# Include any EntityReference wrappers stored alongside native children
|
|
183
|
+
entity_refs = attachments.get(node, :entity_refs)
|
|
184
|
+
result.concat(entity_refs) if entity_refs
|
|
185
|
+
|
|
167
186
|
# Ensure uniqueness by object_id to prevent duplicates
|
|
168
187
|
result.uniq(&:object_id)
|
|
169
188
|
end
|
|
@@ -229,7 +248,7 @@ module Moxml
|
|
|
229
248
|
end
|
|
230
249
|
|
|
231
250
|
def attributes(element)
|
|
232
|
-
return [] unless element.
|
|
251
|
+
return [] unless element.is_a?(::REXML::Element)
|
|
233
252
|
|
|
234
253
|
# Only return non-namespace attributes
|
|
235
254
|
element.attributes.values
|
|
@@ -273,18 +292,33 @@ module Moxml
|
|
|
273
292
|
def add_child(element, child)
|
|
274
293
|
# Special handling for declarations on REXML documents
|
|
275
294
|
if element.is_a?(::REXML::Document) && child.is_a?(::REXML::XMLDecl)
|
|
276
|
-
#
|
|
277
|
-
|
|
295
|
+
# Track declaration state in attachment map
|
|
296
|
+
attachments.set(element, :xml_declaration, child)
|
|
278
297
|
end
|
|
279
298
|
|
|
280
299
|
case child
|
|
281
300
|
when String
|
|
282
301
|
element.add_text(child)
|
|
302
|
+
append_child_sequence(element, :native)
|
|
303
|
+
when ::Moxml::Adapter::CustomizedRexml::EntityReference
|
|
304
|
+
# REXML doesn't support custom node types in its tree.
|
|
305
|
+
# Store alongside native children via attachment map.
|
|
306
|
+
refs = attachments.get(element, :entity_refs) || []
|
|
307
|
+
refs << child
|
|
308
|
+
attachments.set(element, :entity_refs, refs)
|
|
309
|
+
append_child_sequence(element, :eref)
|
|
283
310
|
else
|
|
284
311
|
element.add(child)
|
|
312
|
+
append_child_sequence(element, :native)
|
|
285
313
|
end
|
|
286
314
|
end
|
|
287
315
|
|
|
316
|
+
def append_child_sequence(element, type)
|
|
317
|
+
seq = attachments.get(element, :child_sequence) || []
|
|
318
|
+
seq << type
|
|
319
|
+
attachments.set(element, :child_sequence, seq)
|
|
320
|
+
end
|
|
321
|
+
|
|
288
322
|
def add_previous_sibling(node, sibling)
|
|
289
323
|
parent = node.parent
|
|
290
324
|
# caveat: Rexml fails if children belong to the same parent and are already in a correct order
|
|
@@ -303,8 +337,8 @@ module Moxml
|
|
|
303
337
|
def remove(node)
|
|
304
338
|
# Special handling for declarations on REXML documents
|
|
305
339
|
if node.is_a?(::REXML::XMLDecl) && node.parent.is_a?(::REXML::Document)
|
|
306
|
-
# Clear
|
|
307
|
-
node.parent
|
|
340
|
+
# Clear declaration state in attachment map
|
|
341
|
+
attachments.set(node.parent, :xml_declaration, nil)
|
|
308
342
|
end
|
|
309
343
|
|
|
310
344
|
node.remove
|
|
@@ -373,6 +407,8 @@ module Moxml
|
|
|
373
407
|
case node
|
|
374
408
|
when ::REXML::Text, ::REXML::CData
|
|
375
409
|
node.value.to_s
|
|
410
|
+
when ::Moxml::Adapter::CustomizedRexml::EntityReference
|
|
411
|
+
""
|
|
376
412
|
when ::REXML::Element
|
|
377
413
|
# Extract text recursively from all children to match other adapters
|
|
378
414
|
extract_text_recursively(node)
|
|
@@ -428,7 +464,7 @@ module Moxml
|
|
|
428
464
|
# add a namespace prefix to the element name AND a namespace definition
|
|
429
465
|
def set_namespace(element, ns)
|
|
430
466
|
prefix = ns.name.to_s.empty? ? "xmlns" : ns.name.to_s
|
|
431
|
-
if element.
|
|
467
|
+
if element.is_a?(::REXML::Element)
|
|
432
468
|
element.add_namespace(prefix,
|
|
433
469
|
ns.value)
|
|
434
470
|
end
|
|
@@ -556,11 +592,26 @@ module Moxml
|
|
|
556
592
|
output.strip
|
|
557
593
|
end
|
|
558
594
|
|
|
595
|
+
def has_declaration?(native_doc, wrapper)
|
|
596
|
+
xml_decl = attachments.get(native_doc, :xml_declaration)
|
|
597
|
+
if xml_decl.nil?
|
|
598
|
+
# Attachment key doesn't exist - check native doc or wrapper flag
|
|
599
|
+
if attachments.key?(native_doc, :xml_declaration)
|
|
600
|
+
# Explicitly set to nil (was removed)
|
|
601
|
+
false
|
|
602
|
+
else
|
|
603
|
+
wrapper.has_xml_declaration
|
|
604
|
+
end
|
|
605
|
+
else
|
|
606
|
+
true
|
|
607
|
+
end
|
|
608
|
+
end
|
|
609
|
+
|
|
559
610
|
private
|
|
560
611
|
|
|
561
612
|
def write_with_formatter(node, output, indent = 2)
|
|
562
613
|
formatter = ::Moxml::Adapter::CustomizedRexml::Formatter.new(
|
|
563
|
-
indentation: indent, self_close_empty: false,
|
|
614
|
+
indentation: indent, self_close_empty: false, adapter: self,
|
|
564
615
|
)
|
|
565
616
|
formatter.write(node, output)
|
|
566
617
|
end
|