moxml 0.1.26 → 0.2.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 +5 -0
- data/lib/moxml/adapter/base.rb +53 -49
- 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 +131 -99
- data/lib/moxml/adapter/nokogiri.rb +29 -8
- data/lib/moxml/adapter/oga.rb +91 -54
- data/lib/moxml/adapter/ox.rb +93 -137
- data/lib/moxml/adapter/rexml.rb +61 -50
- 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/config.rb +29 -3
- data/lib/moxml/context.rb +12 -0
- data/lib/moxml/element.rb +29 -12
- data/lib/moxml/entity/reference.rb +28 -0
- data/lib/moxml/entity.rb +125 -0
- data/lib/moxml/node.rb +90 -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 +60 -7
- data/lib/moxml/xpath/parser.rb +22 -15
- data/lib/moxml.rb +3 -0
- 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 +17 -0
- data/spec/integration/sax_parity_spec.rb +58 -0
- data/spec/integration/shared_examples/edge_cases.rb +0 -10
- 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 +0 -8
- data/spec/moxml/adapter/leptris_spec.rb +75 -0
- 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/doctype_spec.rb +1 -1
- data/spec/moxml/entity_spec.rb +76 -0
- 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 +72 -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 +21 -2
data/lib/moxml/adapter/oga.rb
CHANGED
|
@@ -5,6 +5,16 @@ require "oga"
|
|
|
5
5
|
module Moxml
|
|
6
6
|
module Adapter
|
|
7
7
|
class Oga < Base
|
|
8
|
+
# Trigger autoloads that prepend override modules onto Oga's
|
|
9
|
+
# native classes before any parse / read runs.
|
|
10
|
+
# - EntityDecoderOverride: replace Oga 3.4's multi-pass decoder
|
|
11
|
+
# with a single-pass decoder on Oga::EntityDecoder.
|
|
12
|
+
# - RawValueOverride: route Attribute#value / Text#text through
|
|
13
|
+
# the NativeAttachment sidecar so user-authored values bypass
|
|
14
|
+
# the lazy decoder and are returned verbatim.
|
|
15
|
+
::Moxml::Adapter::CustomizedOga::EntityDecoderOverride
|
|
16
|
+
::Moxml::Adapter::CustomizedOga::RawValueOverride
|
|
17
|
+
|
|
8
18
|
class << self
|
|
9
19
|
def attachments
|
|
10
20
|
@attachments ||= Moxml::NativeAttachment.new
|
|
@@ -34,26 +44,29 @@ module Moxml
|
|
|
34
44
|
DocumentBuilder.new(ctx).build(native_doc)
|
|
35
45
|
end
|
|
36
46
|
|
|
37
|
-
# SAX parsing implementation for Oga
|
|
47
|
+
# SAX parsing implementation for Oga.
|
|
48
|
+
#
|
|
49
|
+
# Driven off `Oga.parse_xml` plus a DOM walk because Oga 3.4's
|
|
50
|
+
# native SAX parser decodes `on_text` content before delivery and
|
|
51
|
+
# exposes no hook to override. The prepended EntityDecoderOverride
|
|
52
|
+
# ensures `node.text` invoked during the walk produces the correct
|
|
53
|
+
# single-pass decode, so no separate entity pass is needed here.
|
|
54
|
+
# Trade-off: peak memory scales with document size (Oga 3.4 is the
|
|
55
|
+
# final release of an unmaintained gem; the trade is accepted).
|
|
38
56
|
#
|
|
39
57
|
# @param xml [String, IO] XML to parse
|
|
40
58
|
# @param handler [Moxml::SAX::Handler] Moxml SAX handler
|
|
41
59
|
# @return [void]
|
|
42
60
|
def sax_parse(xml, handler)
|
|
43
|
-
bridge = OgaSAXBridge.new(handler)
|
|
44
|
-
|
|
45
61
|
xml_string = xml.is_a?(IO) || xml.is_a?(StringIO) ? xml.read : xml.to_s
|
|
62
|
+
native_doc = ::Oga.parse_xml(xml_string)
|
|
46
63
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
# Manually call end_document (Oga doesn't)
|
|
53
|
-
handler.on_end_document
|
|
64
|
+
bridge = OgaSAXBridge.new(handler)
|
|
65
|
+
bridge.on_start_document
|
|
66
|
+
native_doc.children.each { |child| bridge.emit(child) }
|
|
67
|
+
bridge.on_end_document
|
|
54
68
|
rescue StandardError => e
|
|
55
|
-
|
|
56
|
-
handler.on_error(error)
|
|
69
|
+
handler.on_error(Moxml::ParseError.new(e.message))
|
|
57
70
|
end
|
|
58
71
|
|
|
59
72
|
def create_document(_native_doc = nil)
|
|
@@ -65,7 +78,14 @@ module Moxml
|
|
|
65
78
|
end
|
|
66
79
|
|
|
67
80
|
def create_native_text(content, _owner_doc = nil)
|
|
68
|
-
|
|
81
|
+
processed = preprocess_entities(content)
|
|
82
|
+
text = ::Oga::XML::Text.new(text: processed)
|
|
83
|
+
# Oga::XML::Text.new stores the value via ivar, bypassing the
|
|
84
|
+
# RawValueOverride setter. Set the sidecar explicitly so reads
|
|
85
|
+
# return the user-supplied value verbatim rather than running
|
|
86
|
+
# it through the lazy decoder.
|
|
87
|
+
attachments.set(text, :raw_text, processed)
|
|
88
|
+
text
|
|
69
89
|
end
|
|
70
90
|
|
|
71
91
|
def create_native_entity_reference(name)
|
|
@@ -136,6 +156,7 @@ module Moxml
|
|
|
136
156
|
|
|
137
157
|
def set_namespace(element, ns_or_string)
|
|
138
158
|
element.namespace_name = ns_or_string.to_s
|
|
159
|
+
element
|
|
139
160
|
end
|
|
140
161
|
|
|
141
162
|
def namespace(element)
|
|
@@ -257,11 +278,15 @@ module Moxml
|
|
|
257
278
|
2)
|
|
258
279
|
end
|
|
259
280
|
|
|
281
|
+
processed = preprocess_entities(value.to_s)
|
|
260
282
|
attr = ::Oga::XML::Attribute.new(
|
|
261
283
|
name: name.to_s,
|
|
262
284
|
namespace_name: namespace_name,
|
|
263
|
-
value:
|
|
285
|
+
value: processed,
|
|
264
286
|
)
|
|
287
|
+
# See create_native_text: Attribute.new bypasses the override
|
|
288
|
+
# setter, so populate the sidecar here.
|
|
289
|
+
attachments.set(attr, :raw_value, processed)
|
|
265
290
|
element.add_attribute(attr)
|
|
266
291
|
end
|
|
267
292
|
|
|
@@ -270,7 +295,8 @@ module Moxml
|
|
|
270
295
|
end
|
|
271
296
|
|
|
272
297
|
def get_attribute_value(element, name)
|
|
273
|
-
element
|
|
298
|
+
attr = element.attribute(name.to_s)
|
|
299
|
+
attr&.value
|
|
274
300
|
end
|
|
275
301
|
|
|
276
302
|
def remove_attribute(element, name)
|
|
@@ -278,6 +304,12 @@ module Moxml
|
|
|
278
304
|
element.attributes.delete(attr) if attr
|
|
279
305
|
end
|
|
280
306
|
|
|
307
|
+
def remove_attribute_native(attr)
|
|
308
|
+
element = attr.element
|
|
309
|
+
element&.attributes&.delete(attr)
|
|
310
|
+
attr
|
|
311
|
+
end
|
|
312
|
+
|
|
281
313
|
def add_child(element, child_or_text)
|
|
282
314
|
child =
|
|
283
315
|
if child_or_text.is_a?(String)
|
|
@@ -438,8 +470,10 @@ module Moxml
|
|
|
438
470
|
end
|
|
439
471
|
|
|
440
472
|
def xpath(node, expression, namespaces = nil)
|
|
441
|
-
node.xpath(expression, {},
|
|
442
|
-
|
|
473
|
+
result = node.xpath(expression, {},
|
|
474
|
+
namespaces: namespaces&.transform_keys(&:to_s))
|
|
475
|
+
# Adapter contract: Array<native> | scalar
|
|
476
|
+
result.is_a?(Enumerable) ? result.to_a : result
|
|
443
477
|
rescue ::LL::ParserError => e
|
|
444
478
|
raise Moxml::XPathError.new(
|
|
445
479
|
e.message,
|
|
@@ -528,50 +562,53 @@ module Moxml
|
|
|
528
562
|
::Moxml::Adapter::CustomizedOga::XmlGenerator.new(node).to_xml
|
|
529
563
|
end
|
|
530
564
|
end
|
|
531
|
-
end
|
|
532
565
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
# @private
|
|
539
|
-
class OgaSAXBridge
|
|
540
|
-
include Moxml::SAX::NamespaceSplitter
|
|
541
|
-
|
|
542
|
-
def initialize(handler)
|
|
543
|
-
@handler = handler
|
|
544
|
-
end
|
|
566
|
+
# Bridge between a parsed Oga DOM and Moxml SAX events.
|
|
567
|
+
#
|
|
568
|
+
# @private
|
|
569
|
+
class OgaSAXBridge
|
|
570
|
+
include Moxml::SAX::NamespaceSplitter
|
|
545
571
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
def on_element(namespace, name, attributes)
|
|
550
|
-
element_name = namespace ? "#{namespace}:#{name}" : name
|
|
551
|
-
attr_hash, ns_hash = split_attributes_and_namespaces(attributes)
|
|
552
|
-
@handler.on_start_element(element_name, attr_hash, ns_hash)
|
|
553
|
-
end
|
|
572
|
+
def initialize(handler)
|
|
573
|
+
@handler = handler
|
|
574
|
+
end
|
|
554
575
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
@handler.on_end_element(element_name)
|
|
559
|
-
end
|
|
576
|
+
def on_start_document
|
|
577
|
+
@handler.on_start_document
|
|
578
|
+
end
|
|
560
579
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
580
|
+
def on_end_document
|
|
581
|
+
@handler.on_end_document
|
|
582
|
+
end
|
|
564
583
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
584
|
+
# Walk a parsed Oga node and emit Moxml SAX events.
|
|
585
|
+
def emit(node)
|
|
586
|
+
case node
|
|
587
|
+
when ::Oga::XML::Element
|
|
588
|
+
element_name = qualified_name(node.namespace_name, node.name)
|
|
589
|
+
pairs = node.attributes.map do |a|
|
|
590
|
+
[qualified_name(a.namespace_name, a.name), a.value]
|
|
591
|
+
end
|
|
592
|
+
attrs, namespaces = split_attributes_and_namespaces(pairs)
|
|
593
|
+
@handler.on_start_element(element_name, attrs, namespaces)
|
|
594
|
+
node.children.each { |c| emit(c) }
|
|
595
|
+
@handler.on_end_element(element_name)
|
|
596
|
+
when ::Oga::XML::Text
|
|
597
|
+
@handler.on_characters(node.text)
|
|
598
|
+
when ::Oga::XML::Cdata
|
|
599
|
+
@handler.on_cdata(node.text)
|
|
600
|
+
when ::Oga::XML::Comment
|
|
601
|
+
@handler.on_comment(node.text)
|
|
602
|
+
when ::Oga::XML::ProcessingInstruction
|
|
603
|
+
@handler.on_processing_instruction(node.name, node.text || "")
|
|
604
|
+
end
|
|
605
|
+
end
|
|
568
606
|
|
|
569
|
-
|
|
570
|
-
@handler.on_comment(text)
|
|
571
|
-
end
|
|
607
|
+
private
|
|
572
608
|
|
|
573
|
-
|
|
574
|
-
|
|
609
|
+
def qualified_name(namespace, local)
|
|
610
|
+
namespace ? "#{namespace}:#{local}" : local
|
|
611
|
+
end
|
|
575
612
|
end
|
|
576
613
|
end
|
|
577
614
|
end
|
data/lib/moxml/adapter/ox.rb
CHANGED
|
@@ -51,7 +51,7 @@ module Moxml
|
|
|
51
51
|
)
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
-
ctx = _context || Context.new(
|
|
54
|
+
ctx = _context || Context.new(context_adapter_name)
|
|
55
55
|
Document.new(native_doc, ctx)
|
|
56
56
|
end
|
|
57
57
|
|
|
@@ -147,7 +147,7 @@ module Moxml
|
|
|
147
147
|
end
|
|
148
148
|
|
|
149
149
|
def set_namespace(element, ns)
|
|
150
|
-
return unless element.is_a?(::Ox::Element) || element.is_a?(::Ox::Node)
|
|
150
|
+
return element unless element.is_a?(::Ox::Element) || element.is_a?(::Ox::Node)
|
|
151
151
|
|
|
152
152
|
prefix = ns.prefix
|
|
153
153
|
# attributes don't have attributes but can have a namespace prefix
|
|
@@ -157,7 +157,7 @@ module Moxml
|
|
|
157
157
|
end
|
|
158
158
|
element.name = [prefix,
|
|
159
159
|
element.name.delete_prefix("xmlns:")].compact.join(":")
|
|
160
|
-
|
|
160
|
+
element
|
|
161
161
|
end
|
|
162
162
|
|
|
163
163
|
def namespace(element)
|
|
@@ -167,6 +167,19 @@ module Moxml
|
|
|
167
167
|
elsif element.name.include?(":")
|
|
168
168
|
element.name.split(":").first
|
|
169
169
|
end
|
|
170
|
+
|
|
171
|
+
# Unprefixed attributes are in no namespace — the default
|
|
172
|
+
# namespace declaration does not apply to attribute names
|
|
173
|
+
# (Namespaces 1.0 §5.2). Unprefixed elements DO take it.
|
|
174
|
+
return nil if element.is_a?(::Moxml::Adapter::CustomizedOx::Attribute) && prefix.nil?
|
|
175
|
+
|
|
176
|
+
# Namespaces 1.0 §4: xml is prebound; no declaration required.
|
|
177
|
+
if prefix == "xml"
|
|
178
|
+
return ::Moxml::Adapter::CustomizedOx::Namespace.new(
|
|
179
|
+
prefix, Moxml::AttributeResolver::XML_NAMESPACE_URI, element
|
|
180
|
+
)
|
|
181
|
+
end
|
|
182
|
+
|
|
170
183
|
attr_name = ["xmlns", prefix].compact.join(":")
|
|
171
184
|
|
|
172
185
|
([element] + ancestors(element)).each do |node|
|
|
@@ -341,6 +354,7 @@ module Moxml
|
|
|
341
354
|
element = attribute.parent
|
|
342
355
|
element.attributes.delete(old_name)
|
|
343
356
|
element.attributes[name] = attribute.value
|
|
357
|
+
attribute
|
|
344
358
|
end
|
|
345
359
|
|
|
346
360
|
def set_attribute_value(attribute, new_value)
|
|
@@ -348,9 +362,16 @@ module Moxml
|
|
|
348
362
|
# Ox converts all values to strings
|
|
349
363
|
remove_attribute(attribute.parent, attribute.name)
|
|
350
364
|
else
|
|
365
|
+
element = attribute.parent
|
|
366
|
+
# Rekey by the expanded name (Ox hash keys carry the
|
|
367
|
+
# prefix) and collapse any symbol/string spelling of the
|
|
368
|
+
# same key, or a stale symbol key shadows the new value.
|
|
369
|
+
expanded = attribute.expanded_name
|
|
370
|
+
element.attributes.delete(expanded.to_sym)
|
|
371
|
+
element.attributes[expanded] = new_value
|
|
351
372
|
attribute.value = new_value
|
|
352
|
-
attribute.parent.attributes[attribute.name] = new_value
|
|
353
373
|
end
|
|
374
|
+
attribute
|
|
354
375
|
end
|
|
355
376
|
|
|
356
377
|
def get_attribute(element, name)
|
|
@@ -378,6 +399,14 @@ module Moxml
|
|
|
378
399
|
element.attributes.delete(name.to_s.to_sym)
|
|
379
400
|
end
|
|
380
401
|
|
|
402
|
+
def remove_attribute_native(attribute)
|
|
403
|
+
expanded = attribute.expanded_name
|
|
404
|
+
element = attribute.parent
|
|
405
|
+
element.attributes.delete(expanded)
|
|
406
|
+
element.attributes.delete(expanded.to_sym)
|
|
407
|
+
attribute
|
|
408
|
+
end
|
|
409
|
+
|
|
381
410
|
def add_child(element, child)
|
|
382
411
|
if element.is_a?(::Ox::Document) && child.is_a?(::Ox::Instruct) && child.target == "xml"
|
|
383
412
|
element.attributes ||= {}
|
|
@@ -504,6 +533,7 @@ module Moxml
|
|
|
504
533
|
case node
|
|
505
534
|
when String then node.to_s
|
|
506
535
|
when ::Moxml::Adapter::CustomizedOx::Text then node.value
|
|
536
|
+
when ::Ox::CData then node.value.to_s
|
|
507
537
|
when ::Moxml::Adapter::CustomizedOx::EntityReference then ""
|
|
508
538
|
else
|
|
509
539
|
return "" unless node.is_a?(::Ox::Element) || node.is_a?(::Ox::Document)
|
|
@@ -601,34 +631,34 @@ module Moxml
|
|
|
601
631
|
matches.last&.first
|
|
602
632
|
end
|
|
603
633
|
|
|
634
|
+
# XPath runs through Moxml's pure-Ruby XPath 1.0 engine (shared
|
|
635
|
+
# with HeadedOx and Leptris). Ox's locate() only covers a
|
|
636
|
+
# small XPath subset; the engine provides all axes,
|
|
637
|
+
# predicates, functions and variables.
|
|
604
638
|
def xpath(node, expression, namespaces = {})
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
639
|
+
wrapped = if node.is_a?(Moxml::Node)
|
|
640
|
+
node
|
|
641
|
+
else
|
|
642
|
+
Moxml::Node.wrap(node, Context.new(context_adapter_name))
|
|
643
|
+
end
|
|
644
|
+
|
|
645
|
+
ast = XPath::Parser.parse(expression)
|
|
646
|
+
proc = XPath::Compiler.compile_with_cache(ast, namespaces: namespaces)
|
|
647
|
+
result = proc.call(wrapped)
|
|
648
|
+
|
|
649
|
+
case result
|
|
650
|
+
when Array
|
|
651
|
+
dedupe_natives(result.map do |n|
|
|
652
|
+
n.is_a?(Moxml::Node) ? n.native : n
|
|
653
|
+
end)
|
|
654
|
+
when NodeSet
|
|
655
|
+
dedupe_natives(result.to_a.map(&:native))
|
|
656
|
+
else
|
|
657
|
+
result
|
|
622
658
|
end
|
|
623
|
-
|
|
624
|
-
# Use Ox's locate method for other cases
|
|
625
|
-
results = node.locate(locate_expr)
|
|
626
|
-
|
|
627
|
-
# Wrap results and set their parents by finding them in the tree
|
|
628
|
-
results.map { |n| patch_node(n, find_parent_in_tree(n, node)) }
|
|
629
659
|
rescue StandardError => e
|
|
630
660
|
raise Moxml::XPathError.new(
|
|
631
|
-
"XPath
|
|
661
|
+
"XPath execution failed: #{e.message}",
|
|
632
662
|
expression: expression,
|
|
633
663
|
adapter: "Ox",
|
|
634
664
|
node: node,
|
|
@@ -636,7 +666,32 @@ module Moxml
|
|
|
636
666
|
end
|
|
637
667
|
|
|
638
668
|
def at_xpath(node, expression, namespaces = {})
|
|
639
|
-
xpath(node, expression, namespaces)
|
|
669
|
+
result = xpath(node, expression, namespaces)
|
|
670
|
+
result.is_a?(Array) ? result.first : result
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
def xpath_supported?
|
|
674
|
+
true
|
|
675
|
+
end
|
|
676
|
+
|
|
677
|
+
# Identity-based dedup: uniq's value equality would collapse
|
|
678
|
+
# equal String text nodes.
|
|
679
|
+
def dedupe_natives(natives)
|
|
680
|
+
seen = {}
|
|
681
|
+
natives.select do |native|
|
|
682
|
+
id = native.object_id
|
|
683
|
+
if seen[id]
|
|
684
|
+
false
|
|
685
|
+
else
|
|
686
|
+
seen[id] = true
|
|
687
|
+
end
|
|
688
|
+
end
|
|
689
|
+
end
|
|
690
|
+
|
|
691
|
+
# Adapter name used when wrapping bare native nodes; kept
|
|
692
|
+
# overridable so HeadedOx wraps into its own adapter.
|
|
693
|
+
def context_adapter_name
|
|
694
|
+
:ox
|
|
640
695
|
end
|
|
641
696
|
|
|
642
697
|
def serialize(node, options = {})
|
|
@@ -644,7 +699,7 @@ module Moxml
|
|
|
644
699
|
# back-reference, but that makes it unknown to Ox.dump's XML emitter,
|
|
645
700
|
# which then falls back to generic object marshalling. Short-circuit
|
|
646
701
|
# here with proper XML escaping.
|
|
647
|
-
return
|
|
702
|
+
return XmlEmitter.escape_text(node.value) if node.is_a?(CustomizedOx::Text)
|
|
648
703
|
|
|
649
704
|
needs_custom = needs_custom_serialize?(node)
|
|
650
705
|
|
|
@@ -782,10 +837,7 @@ module Moxml
|
|
|
782
837
|
version = node[:version] || "1.0"
|
|
783
838
|
encoding = resolve_decl_attr(node, :encoding, options[:encoding])
|
|
784
839
|
standalone = resolve_decl_attr(node, :standalone, nil)
|
|
785
|
-
output <<
|
|
786
|
-
output << " encoding=\"#{encoding}\"" if encoding
|
|
787
|
-
output << " standalone=\"#{standalone}\"" if standalone
|
|
788
|
-
output << "?>"
|
|
840
|
+
output << XmlEmitter.declaration_xml(version, encoding, standalone)
|
|
789
841
|
end
|
|
790
842
|
(node.nodes || []).each do |child|
|
|
791
843
|
output << serialize_node_custom(child)
|
|
@@ -799,10 +851,10 @@ module Moxml
|
|
|
799
851
|
def serialize_node_custom(node)
|
|
800
852
|
case node
|
|
801
853
|
when ::Ox::Element then serialize_element_custom(node)
|
|
802
|
-
when String then
|
|
803
|
-
when ::Moxml::Adapter::CustomizedOx::Text then
|
|
854
|
+
when String then XmlEmitter.escape_text(node)
|
|
855
|
+
when ::Moxml::Adapter::CustomizedOx::Text then XmlEmitter.escape_text(node.value)
|
|
804
856
|
when ::Moxml::Adapter::CustomizedOx::EntityReference then "&#{node.name};"
|
|
805
|
-
when ::Ox::CData then
|
|
857
|
+
when ::Ox::CData then XmlEmitter.cdata(node.value)
|
|
806
858
|
when ::Ox::Comment then "<!--#{node.value}-->"
|
|
807
859
|
when ::Ox::Instruct then "<?#{node.target} #{node.value || ''}?>"
|
|
808
860
|
when ::Ox::DocType then "<!DOCTYPE #{node.value}>"
|
|
@@ -813,7 +865,7 @@ module Moxml
|
|
|
813
865
|
def serialize_element_custom(elem)
|
|
814
866
|
output = "<#{elem.name}"
|
|
815
867
|
elem.attributes.each do |name, value|
|
|
816
|
-
output << " #{name}=\"#{
|
|
868
|
+
output << " #{name}=\"#{XmlEmitter.escape_attribute(value)}\""
|
|
817
869
|
end
|
|
818
870
|
|
|
819
871
|
if elem.nodes.nil? || elem.nodes.empty?
|
|
@@ -828,104 +880,6 @@ module Moxml
|
|
|
828
880
|
output << "</#{elem.name}>"
|
|
829
881
|
output
|
|
830
882
|
end
|
|
831
|
-
|
|
832
|
-
def serialize_cdata(content)
|
|
833
|
-
escaped = content.gsub("]]>", "]]]]><![CDATA[>")
|
|
834
|
-
"<![CDATA[#{escaped}]]>"
|
|
835
|
-
end
|
|
836
|
-
|
|
837
|
-
def escape_xml_text(text)
|
|
838
|
-
text.to_s.gsub(/[<>&]/) do |match|
|
|
839
|
-
case match
|
|
840
|
-
when "<" then "<"
|
|
841
|
-
when ">" then ">"
|
|
842
|
-
when "&" then "&"
|
|
843
|
-
end
|
|
844
|
-
end
|
|
845
|
-
end
|
|
846
|
-
|
|
847
|
-
def escape_xml_attribute(value)
|
|
848
|
-
value.to_s.gsub(/[<>&"]/) do |match|
|
|
849
|
-
case match
|
|
850
|
-
when "<" then "<"
|
|
851
|
-
when ">" then ">"
|
|
852
|
-
when "&" then "&"
|
|
853
|
-
when '"' then """
|
|
854
|
-
end
|
|
855
|
-
end
|
|
856
|
-
end
|
|
857
|
-
|
|
858
|
-
# Translate a subset of XPath to Ox locate() syntax
|
|
859
|
-
# Supports: //element, /path/to/element, .//element, element[@attr]
|
|
860
|
-
# Note: Ox locate() doesn't support namespace prefixes in the path
|
|
861
|
-
def translate_xpath_to_locate(xpath, namespaces = {})
|
|
862
|
-
expr = xpath.dup
|
|
863
|
-
|
|
864
|
-
# Strip namespace prefixes from element names
|
|
865
|
-
# XPath: //ns:element → locate: element
|
|
866
|
-
if namespaces && !namespaces.empty?
|
|
867
|
-
namespaces.each_key do |prefix|
|
|
868
|
-
expr = expr.gsub("/#{prefix}:", "/")
|
|
869
|
-
expr = expr.gsub("/*#{prefix}:", "/*")
|
|
870
|
-
expr = expr.gsub("//*#{prefix}:", "//")
|
|
871
|
-
expr = expr.gsub("//#{prefix}:", "//")
|
|
872
|
-
expr = expr.gsub("///#{prefix}:", "///")
|
|
873
|
-
end
|
|
874
|
-
end
|
|
875
|
-
|
|
876
|
-
# Remove any remaining namespace prefixes
|
|
877
|
-
# Use possessive quantifier to prevent ReDoS
|
|
878
|
-
expr = expr.gsub(/[a-zA-Z_][\w-]*+:/, "")
|
|
879
|
-
|
|
880
|
-
# Remove attribute predicates for now - we'll filter manually
|
|
881
|
-
# Save the attribute name if present
|
|
882
|
-
expr = expr.gsub(/\[@(\w+)\]/, "")
|
|
883
|
-
|
|
884
|
-
# XPath: //element → locate: ?/element (any depth)
|
|
885
|
-
# Note: In Ox, ?/ means "any path"
|
|
886
|
-
expr = expr.sub(%r{^//}, "?/") if expr.start_with?("//")
|
|
887
|
-
|
|
888
|
-
# XPath: .//element → locate: ?/element (relative any depth)
|
|
889
|
-
# For relative paths from an element, we still use ?/ which searches
|
|
890
|
-
# descendants
|
|
891
|
-
expr = expr.sub(%r{^\.//}, "?/") if expr.start_with?(".//")
|
|
892
|
-
|
|
893
|
-
# XPath: /root/child → locate: root/child (absolute path)
|
|
894
|
-
# Remove leading / for Ox
|
|
895
|
-
expr = expr.sub(%r{^/}, "")
|
|
896
|
-
|
|
897
|
-
# XPath: ./element → locate: element (direct child, just remove ./)
|
|
898
|
-
expr.sub(%r{^\./}, "")
|
|
899
|
-
end
|
|
900
|
-
|
|
901
|
-
# Find the actual parent of a node by searching the tree
|
|
902
|
-
def find_parent_in_tree(target_node, search_root)
|
|
903
|
-
# Start from the document root if we have a document
|
|
904
|
-
root = search_root.is_a?(::Ox::Document) ? search_root : document(search_root)
|
|
905
|
-
|
|
906
|
-
result = nil
|
|
907
|
-
traverse(root) do |node|
|
|
908
|
-
next unless node.is_a?(::Ox::Element) || node.is_a?(::Ox::Document)
|
|
909
|
-
|
|
910
|
-
node.nodes&.each do |child|
|
|
911
|
-
if child.equal?(target_node)
|
|
912
|
-
result = node
|
|
913
|
-
break
|
|
914
|
-
end
|
|
915
|
-
end
|
|
916
|
-
break if result
|
|
917
|
-
end
|
|
918
|
-
result
|
|
919
|
-
end
|
|
920
|
-
|
|
921
|
-
def traverse(node, &block)
|
|
922
|
-
return unless node
|
|
923
|
-
|
|
924
|
-
yield node
|
|
925
|
-
return unless node.is_a?(::Ox::Element) || node.is_a?(::Ox::Document)
|
|
926
|
-
|
|
927
|
-
node.nodes&.each { |child| traverse(child, &block) }
|
|
928
|
-
end
|
|
929
883
|
end
|
|
930
884
|
end
|
|
931
885
|
|
|
@@ -948,7 +902,9 @@ module Moxml
|
|
|
948
902
|
|
|
949
903
|
# Ox delivers attributes AFTER start_element
|
|
950
904
|
def attr(name, value)
|
|
951
|
-
|
|
905
|
+
# Attributes arriving before the first start_element belong
|
|
906
|
+
# to the XML declaration, not to any element
|
|
907
|
+
@pending_attrs[name.to_s] = value if @pending_element_name
|
|
952
908
|
end
|
|
953
909
|
|
|
954
910
|
# Called when element starts (but attributes come AFTER this)
|