moxml 0.1.13 → 0.1.15

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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +117 -66
  3. data/Gemfile +1 -0
  4. data/README.adoc +11 -9
  5. data/Rakefile +3 -1
  6. data/docs/_pages/configuration.adoc +22 -19
  7. data/docs/_tutorials/namespace-handling.adoc +5 -5
  8. data/lib/moxml/adapter/base.rb +8 -3
  9. data/lib/moxml/adapter/customized_libxml/entity_reference.rb +23 -0
  10. data/lib/moxml/adapter/customized_libxml.rb +18 -0
  11. data/lib/moxml/adapter/customized_oga/xml_generator.rb +2 -2
  12. data/lib/moxml/adapter/customized_oga.rb +10 -0
  13. data/lib/moxml/adapter/customized_ox/entity_reference.rb +25 -0
  14. data/lib/moxml/adapter/customized_ox.rb +12 -0
  15. data/lib/moxml/adapter/customized_rexml/entity_reference.rb +19 -0
  16. data/lib/moxml/adapter/customized_rexml/formatter.rb +2 -0
  17. data/lib/moxml/adapter/customized_rexml.rb +11 -0
  18. data/lib/moxml/adapter/headed_ox.rb +9 -3
  19. data/lib/moxml/adapter/libxml.rb +76 -62
  20. data/lib/moxml/adapter/nokogiri.rb +4 -5
  21. data/lib/moxml/adapter/oga.rb +50 -26
  22. data/lib/moxml/adapter/ox.rb +189 -41
  23. data/lib/moxml/adapter/rexml.rb +27 -8
  24. data/lib/moxml/attribute.rb +3 -0
  25. data/lib/moxml/builder.rb +1 -0
  26. data/lib/moxml/config.rb +7 -7
  27. data/lib/moxml/document.rb +5 -1
  28. data/lib/moxml/document_builder.rb +37 -31
  29. data/lib/moxml/element.rb +13 -5
  30. data/lib/moxml/entity_registry.rb +36 -0
  31. data/lib/moxml/node.rb +23 -2
  32. data/lib/moxml/node_set.rb +43 -15
  33. data/lib/moxml/version.rb +1 -1
  34. data/lib/moxml/xml_utils.rb +1 -1
  35. data/spec/integration/shared_examples/edge_cases.rb +3 -0
  36. data/spec/moxml/adapter/oga_spec.rb +62 -0
  37. data/spec/moxml/adapter/shared_examples/adapter_contract.rb +1 -12
  38. data/spec/moxml/allocation_benchmark_spec.rb +96 -0
  39. data/spec/moxml/allocation_guard_spec.rb +282 -0
  40. data/spec/moxml/builder_spec.rb +22 -0
  41. data/spec/moxml/config_spec.rb +11 -11
  42. data/spec/moxml/doctype_spec.rb +41 -0
  43. data/spec/moxml/lazy_parse_spec.rb +115 -0
  44. data/spec/moxml/namespace_uri_validation_spec.rb +11 -3
  45. data/spec/moxml/node_cache_spec.rb +110 -0
  46. data/spec/moxml/node_set_cache_spec.rb +90 -0
  47. data/spec/moxml/xml_utils_spec.rb +32 -0
  48. data/spec/support/allocation_helper.rb +165 -0
  49. data/spec/support/w3c_namespace_helpers.rb +2 -1
  50. metadata +15 -2
@@ -3,9 +3,7 @@
3
3
  require_relative "base"
4
4
  require "ox"
5
5
  require "stringio"
6
- require_relative "customized_ox/text"
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
@@ -17,16 +15,19 @@ module Moxml
17
15
  replace_children(doc, [element])
18
16
  end
19
17
 
20
- def parse(xml, _options = {}, _context = nil)
18
+ def parse(xml, options = {}, _context = nil)
21
19
  native_doc = begin
22
20
  result = ::Ox.parse(xml)
23
21
 
24
22
  # result can be either Document or Element
25
23
  if result.is_a?(::Ox::Document)
24
+ assign_parents(result)
25
+ validate_single_root(result) if options[:strict]
26
26
  result
27
27
  else
28
28
  doc = ::Ox::Document.new
29
29
  doc << result
30
+ assign_parents(doc)
30
31
  doc
31
32
  end
32
33
  rescue ::Ox::ParseError => e
@@ -37,7 +38,7 @@ module Moxml
37
38
  end
38
39
 
39
40
  ctx = _context || Context.new(:ox)
40
- DocumentBuilder.new(ctx).build(native_doc)
41
+ Document.new(native_doc, ctx)
41
42
  end
42
43
 
43
44
  # SAX parsing implementation for Ox
@@ -50,7 +51,7 @@ module Moxml
50
51
  bridge = OxSAXBridge.new(handler)
51
52
 
52
53
  # Parse using Ox's SAX parser
53
- xml_string = xml.respond_to?(:read) ? xml.read : xml.to_s
54
+ xml_string = xml.is_a?(IO) || xml.is_a?(StringIO) ? xml.read : xml.to_s
54
55
 
55
56
  begin
56
57
  ::Ox.sax_parse(bridge, StringIO.new(xml_string))
@@ -77,6 +78,14 @@ module Moxml
77
78
  content
78
79
  end
79
80
 
81
+ def create_native_entity_reference(name)
82
+ ::Moxml::Adapter::CustomizedOx::EntityReference.new(name)
83
+ end
84
+
85
+ def entity_reference_name(node)
86
+ node.name if node.is_a?(::Moxml::Adapter::CustomizedOx::EntityReference)
87
+ end
88
+
80
89
  def create_native_cdata(content, _owner_doc = nil)
81
90
  ::Ox::CData.new(content)
82
91
  end
@@ -121,11 +130,11 @@ module Moxml
121
130
  end
122
131
 
123
132
  def set_namespace(element, ns)
124
- return unless element.respond_to?(:name)
133
+ return unless element.is_a?(::Ox::Element) || element.is_a?(::Ox::Node)
125
134
 
126
135
  prefix = ns.prefix
127
136
  # attributes don't have attributes but can have a namespace prefix
128
- if element.respond_to?(:attributes)
137
+ if element.is_a?(::Ox::Element)
129
138
  set_attribute(element, ns.expanded_prefix,
130
139
  ns.uri)
131
140
  end
@@ -136,8 +145,7 @@ module Moxml
136
145
 
137
146
  def namespace(element)
138
147
  prefix =
139
- if element.respond_to?(:prefix)
140
- # attribute
148
+ if element.is_a?(::Moxml::Adapter::CustomizedOx::Attribute)
141
149
  element.prefix
142
150
  elsif element.name.include?(":")
143
151
  element.name.split(":").first
@@ -145,7 +153,7 @@ module Moxml
145
153
  attr_name = ["xmlns", prefix].compact.join(":")
146
154
 
147
155
  ([element] + ancestors(element)).each do |node|
148
- next unless node.respond_to?(:attributes) && node.attributes
156
+ next unless node.is_a?(::Ox::Element) && node.attributes
149
157
 
150
158
  if node[attr_name]
151
159
  return ::Moxml::Adapter::CustomizedOx::Namespace.new(
@@ -176,6 +184,7 @@ module Moxml
176
184
  when ::Ox::Instruct then :processing_instruction
177
185
  when ::Ox::Element then :element
178
186
  when ::Ox::DocType then :doctype
187
+ when ::Moxml::Adapter::CustomizedOx::EntityReference then :entity_reference
179
188
  when ::Moxml::Adapter::CustomizedOx::Namespace then :banespace
180
189
  when ::Moxml::Adapter::CustomizedOx::Attribute then :attribute
181
190
  else :unknown
@@ -194,10 +203,9 @@ module Moxml
194
203
  end
195
204
 
196
205
  def set_node_name(node, name)
197
- if node.respond_to?(:name=)
198
- node.name = name
199
- elsif node.respond_to?(:value=)
200
- node.value = name
206
+ case node
207
+ when ::Ox::Element then node.name = name
208
+ when ::Ox::Instruct then node.value = name
201
209
  end
202
210
  end
203
211
 
@@ -219,7 +227,7 @@ module Moxml
219
227
  else node
220
228
  end
221
229
 
222
- new_node.parent = parent if new_node.respond_to?(:parent)
230
+ new_node.parent = parent if new_node.is_a?(::Ox::Node)
223
231
 
224
232
  new_node
225
233
  end
@@ -231,18 +239,25 @@ module Moxml
231
239
  node.value]
232
240
  # when ::Moxml::Adapter::CustomizedOx::Attribute then { node.name => node.value }
233
241
  when ::Moxml::Adapter::CustomizedOx::Text then node.value
242
+ when ::Moxml::Adapter::CustomizedOx::EntityReference then node
234
243
  else node
235
244
  end
236
245
  end
237
246
 
238
247
  def children(node)
239
- return [] unless node.respond_to?(:nodes)
248
+ return [] unless node.is_a?(::Ox::Element) || node.is_a?(::Ox::Document)
240
249
 
241
- node.nodes || []
250
+ result = node.nodes || []
251
+ # Ox doesn't set parent references during parsing.
252
+ # Set them here so parent/sibling navigation works.
253
+ result.each do |child|
254
+ child.parent = node if child.respond_to?(:parent=)
255
+ end
256
+ result
242
257
  end
243
258
 
244
259
  def parent(node)
245
- node.parent if node.respond_to?(:parent)
260
+ node.parent if node.is_a?(::Ox::Node)
246
261
  end
247
262
 
248
263
  def next_sibling(node)
@@ -272,9 +287,7 @@ module Moxml
272
287
  end
273
288
 
274
289
  def attributes(element)
275
- unless element.respond_to?(:attributes) && element.attributes
276
- return []
277
- end
290
+ return [] unless element.is_a?(::Ox::Element) && element.attributes
278
291
 
279
292
  element.attributes.filter_map do |name, value|
280
293
  next if name.to_s.start_with?("xmlns")
@@ -324,7 +337,7 @@ module Moxml
324
337
  end
325
338
 
326
339
  def get_attribute(element, name)
327
- return unless element.respond_to?(:attributes) && element.attributes
340
+ return unless element.is_a?(::Ox::HasAttrs) && element.attributes
328
341
  unless element.attributes.key?(name.to_s) || element.attributes.key?(name.to_s.to_sym)
329
342
  return
330
343
  end
@@ -342,7 +355,7 @@ module Moxml
342
355
  end
343
356
 
344
357
  def remove_attribute(element, name)
345
- return unless element.respond_to?(:attributes) && element.attributes
358
+ return unless element.is_a?(::Ox::HasAttrs) && element.attributes
346
359
 
347
360
  element.attributes.delete(name.to_s)
348
361
  element.attributes.delete(name.to_s.to_sym)
@@ -367,15 +380,24 @@ module Moxml
367
380
  end
368
381
  end
369
382
 
370
- child.parent = element if child.respond_to?(:parent)
383
+ child.parent = element if child.is_a?(::Ox::Node)
371
384
  element.nodes ||= []
372
385
  element.nodes << child
386
+
387
+ # Mark document if EntityReference is added (avoids tree scan in serialize)
388
+ if child.is_a?(::Moxml::Adapter::CustomizedOx::EntityReference)
389
+ root = element
390
+ while root.is_a?(::Ox::Node) && root.parent
391
+ root = root.parent
392
+ end
393
+ root&.instance_variable_set(:@moxml_entity_refs, true)
394
+ end
373
395
  end
374
396
 
375
397
  def add_previous_sibling(node, sibling)
376
398
  return unless (parent = parent(node))
377
399
 
378
- if sibling.respond_to?(:parent)
400
+ if sibling.is_a?(::Ox::Node)
379
401
  sibling.parent&.nodes&.delete(sibling)
380
402
  sibling.parent = parent
381
403
  end
@@ -386,7 +408,7 @@ module Moxml
386
408
  def add_next_sibling(node, sibling)
387
409
  return unless (parent = parent(node))
388
410
 
389
- if sibling.respond_to?(:parent)
411
+ if sibling.is_a?(::Ox::Node)
390
412
  sibling.parent&.nodes&.delete(sibling)
391
413
  sibling.parent = parent
392
414
  end
@@ -421,7 +443,7 @@ module Moxml
421
443
 
422
444
  return unless (parent = parent(node))
423
445
 
424
- new_node.parent = parent if new_node.respond_to?(:parent)
446
+ new_node.parent = parent if new_node.is_a?(::Ox::Node)
425
447
  idx = parent.nodes.index(node)
426
448
  parent.nodes[idx] = new_node if idx
427
449
  end
@@ -429,20 +451,40 @@ module Moxml
429
451
  def replace_children(node, new_children)
430
452
  node.remove_children_by_path("*")
431
453
  new_children.each do |child|
432
- child.parent = node if child.respond_to?(:parent)
454
+ child.parent = node if child.is_a?(::Ox::Node)
433
455
  node << child
434
456
  end
435
457
  node
436
458
  end
437
459
 
460
+ def assign_parents(node, parent = nil)
461
+ node.parent = parent if node.respond_to?(:parent=) && parent
462
+ return unless node.respond_to?(:nodes)
463
+
464
+ node.nodes&.each do |child|
465
+ assign_parents(child, node)
466
+ end
467
+ end
468
+
469
+ def validate_single_root(document)
470
+ elements = document.nodes&.grep(::Ox::Element) || []
471
+ return unless elements.size > 1
472
+
473
+ raise Moxml::ParseError.new(
474
+ "Multiple root elements found",
475
+ source: nil,
476
+ )
477
+ end
478
+
438
479
  def text_content(node)
439
480
  return "" if node.nil?
440
481
 
441
482
  case node
442
483
  when String then node.to_s
443
484
  when ::Moxml::Adapter::CustomizedOx::Text then node.value
485
+ when ::Moxml::Adapter::CustomizedOx::EntityReference then ""
444
486
  else
445
- return "" unless node.respond_to?(:nodes)
487
+ return "" unless node.is_a?(::Ox::Element) || node.is_a?(::Ox::Document)
446
488
 
447
489
  node.nodes.map do |n|
448
490
  text_content(n)
@@ -451,7 +493,7 @@ module Moxml
451
493
  end
452
494
 
453
495
  def inner_text(node)
454
- return "" unless node.respond_to?(:nodes)
496
+ return "" unless node.is_a?(::Ox::Element) || node.is_a?(::Ox::Document)
455
497
 
456
498
  node.nodes.grep(String).join
457
499
  end
@@ -499,7 +541,7 @@ module Moxml
499
541
 
500
542
  def namespace_definitions(node)
501
543
  ([node] + ancestors(node)).reverse.each_with_object({}) do |n, namespaces|
502
- next unless n.respond_to?(:attributes) && n.attributes
544
+ next unless n.is_a?(::Ox::Element) && n.attributes
503
545
 
504
546
  n.attributes.each do |name, value|
505
547
  next unless name.to_s.start_with?("xmlns")
@@ -575,18 +617,30 @@ module Moxml
575
617
  end
576
618
 
577
619
  def serialize(node, options = {})
620
+ # Fast path: skip EntityReference scan for documents (most common case)
621
+ if node.is_a?(::Ox::Document) &&
622
+ !node.instance_variable_get(:@moxml_entity_refs)
623
+ return serialize_standard(node, options)
624
+ end
625
+
626
+ if tree_has_entity_references?(node)
627
+ serialize_custom(node, options)
628
+ else
629
+ serialize_standard(node, options)
630
+ end
631
+ end
632
+
633
+ private
634
+
635
+ def serialize_standard(node, options = {})
578
636
  output = ""
579
637
  if node.is_a?(::Ox::Document)
580
- # Check if we should include declaration
581
- # Priority: explicit option > document attributes
582
638
  should_include_decl = if options.key?(:no_declaration)
583
639
  !options[:no_declaration]
584
640
  else
585
- # Check if document has declaration attributes
586
641
  node[:version] || node[:encoding] || node[:standalone]
587
642
  end
588
643
 
589
- # Only add declaration if should_include_decl is true
590
644
  if should_include_decl
591
645
  version = node[:version] || "1.0"
592
646
  encoding = options[:encoding] || node[:encoding]
@@ -598,8 +652,7 @@ module Moxml
598
652
  end
599
653
 
600
654
  ox_options = {
601
- indent: -1, # options[:indent] || -1, # indent is a beast
602
- # with_xml: true,
655
+ indent: -1,
603
656
  with_instructions: true,
604
657
  encoding: options[:encoding],
605
658
  no_empty: options[:expand_empty],
@@ -607,7 +660,102 @@ module Moxml
607
660
  output + ::Ox.dump(node, ox_options)
608
661
  end
609
662
 
610
- private
663
+ def tree_has_entity_references?(node)
664
+ case node
665
+ when ::Moxml::Adapter::CustomizedOx::EntityReference
666
+ true
667
+ when ::Ox::Element
668
+ node.nodes&.any? do |child|
669
+ tree_has_entity_references?(child)
670
+ end || false
671
+ when ::Ox::Document
672
+ node.nodes&.any? do |child|
673
+ tree_has_entity_references?(child)
674
+ end || false
675
+ else
676
+ false
677
+ end
678
+ end
679
+
680
+ def serialize_custom(node, options = {})
681
+ output = +""
682
+ if node.is_a?(::Ox::Document)
683
+ should_include_decl = if options.key?(:no_declaration)
684
+ !options[:no_declaration]
685
+ else
686
+ node[:version] || node[:encoding] || node[:standalone]
687
+ end
688
+ if should_include_decl
689
+ version = node[:version] || "1.0"
690
+ encoding = options[:encoding] || node[:encoding]
691
+ standalone = node[:standalone]
692
+ output << "<?xml version=\"#{version}\""
693
+ output << " encoding=\"#{encoding}\"" if encoding
694
+ output << " standalone=\"#{standalone}\"" if standalone
695
+ output << "?>"
696
+ end
697
+ (node.nodes || []).each do |child|
698
+ output << serialize_node_custom(child)
699
+ end
700
+ else
701
+ output << serialize_node_custom(node)
702
+ end
703
+ output
704
+ end
705
+
706
+ def serialize_node_custom(node)
707
+ case node
708
+ when ::Ox::Element then serialize_element_custom(node)
709
+ when String then escape_xml_text(node)
710
+ when ::Moxml::Adapter::CustomizedOx::Text then escape_xml_text(node.value)
711
+ when ::Moxml::Adapter::CustomizedOx::EntityReference then "&#{node.name};"
712
+ when ::Ox::CData then "<![CDATA[#{node.value}]]>"
713
+ when ::Ox::Comment then "<!--#{node.value}-->"
714
+ when ::Ox::Instruct then "<?#{node.target} #{node.value || ''}?>"
715
+ when ::Ox::DocType then "<!DOCTYPE #{node.value}>"
716
+ else ""
717
+ end
718
+ end
719
+
720
+ def serialize_element_custom(elem)
721
+ output = "<#{elem.name}"
722
+ elem.attributes.each do |name, value|
723
+ output << " #{name}=\"#{escape_xml_attribute(value.to_s)}\""
724
+ end
725
+
726
+ if elem.nodes.nil? || elem.nodes.empty?
727
+ output << "/>"
728
+ return output
729
+ end
730
+
731
+ output << ">"
732
+ elem.nodes.each do |child|
733
+ output << serialize_node_custom(child)
734
+ end
735
+ output << "</#{elem.name}>"
736
+ output
737
+ end
738
+
739
+ def escape_xml_text(text)
740
+ text.to_s.gsub(/[<>&]/) do |match|
741
+ case match
742
+ when "<" then "&lt;"
743
+ when ">" then "&gt;"
744
+ when "&" then "&amp;"
745
+ end
746
+ end
747
+ end
748
+
749
+ def escape_xml_attribute(value)
750
+ value.to_s.gsub(/[<>&"]/) do |match|
751
+ case match
752
+ when "<" then "&lt;"
753
+ when ">" then "&gt;"
754
+ when "&" then "&amp;"
755
+ when '"' then "&quot;"
756
+ end
757
+ end
758
+ end
611
759
 
612
760
  # Translate a subset of XPath to Ox locate() syntax
613
761
  # Supports: //element, /path/to/element, .//element, element[@attr]
@@ -659,7 +807,7 @@ module Moxml
659
807
 
660
808
  result = nil
661
809
  traverse(root) do |node|
662
- next unless node.respond_to?(:nodes)
810
+ next unless node.is_a?(::Ox::Element) || node.is_a?(::Ox::Document)
663
811
 
664
812
  node.nodes&.each do |child|
665
813
  if child.equal?(target_node)
@@ -676,7 +824,7 @@ module Moxml
676
824
  return unless node
677
825
 
678
826
  yield node
679
- return unless node.respond_to?(:nodes)
827
+ return unless node.is_a?(::Ox::Element) || node.is_a?(::Ox::Document)
680
828
 
681
829
  node.nodes&.each { |child| traverse(child, &block) }
682
830
  end
@@ -4,7 +4,7 @@ require_relative "base"
4
4
  require "rexml/document"
5
5
  require "rexml/xpath"
6
6
  require "set"
7
- require_relative "customized_rexml/formatter"
7
+ require_relative "customized_rexml"
8
8
 
9
9
  module Moxml
10
10
  module Adapter
@@ -54,7 +54,7 @@ module Moxml
54
54
 
55
55
  bridge = REXMLSAX2Bridge.new(handler)
56
56
 
57
- xml_string = xml.respond_to?(:read) ? xml.read : xml.to_s
57
+ xml_string = xml.is_a?(IO) || xml.is_a?(StringIO) ? xml.read : xml.to_s
58
58
  source = ::REXML::IOSource.new(StringIO.new(xml_string))
59
59
 
60
60
  parser = ::REXML::Parsers::SAX2Parser.new(source)
@@ -77,6 +77,14 @@ module Moxml
77
77
  ::REXML::Text.new(content.to_s, true, nil)
78
78
  end
79
79
 
80
+ def create_native_entity_reference(name)
81
+ ::Moxml::Adapter::CustomizedRexml::EntityReference.new(name)
82
+ end
83
+
84
+ def entity_reference_name(node)
85
+ node.name if node.is_a?(::Moxml::Adapter::CustomizedRexml::EntityReference)
86
+ end
87
+
80
88
  def create_native_cdata(content, _owner_doc = nil)
81
89
  ::REXML::CData.new(content.to_s)
82
90
  end
@@ -124,6 +132,7 @@ module Moxml
124
132
  when ::REXML::Instruction then :processing_instruction
125
133
  when ::REXML::DocType then :doctype
126
134
  when ::REXML::XMLDecl then :declaration
135
+ when ::Moxml::Adapter::CustomizedRexml::EntityReference then :entity_reference
127
136
  else :unknown
128
137
  end
129
138
  end
@@ -149,13 +158,15 @@ module Moxml
149
158
  end
150
159
 
151
160
  def duplicate_node(node)
152
- # Make a complete duplicate of the node
153
- # https://stackoverflow.com/questions/23878384/why-the-original-element-got-changed-when-i-modify-the-copy-created-by-dup-meth
154
- Marshal.load(Marshal.dump(node))
161
+ if node.respond_to?(:deep_clone)
162
+ node.deep_clone
163
+ else
164
+ Marshal.load(Marshal.dump(node))
165
+ end
155
166
  end
156
167
 
157
168
  def children(node)
158
- return [] unless node.respond_to?(:children)
169
+ return [] unless node.is_a?(::REXML::Parent)
159
170
 
160
171
  # Get all children and filter out empty text nodes between elements
161
172
  result = node.children.reject do |child|
@@ -229,7 +240,7 @@ module Moxml
229
240
  end
230
241
 
231
242
  def attributes(element)
232
- return [] unless element.respond_to?(:attributes)
243
+ return [] unless element.is_a?(::REXML::Element)
233
244
 
234
245
  # Only return non-namespace attributes
235
246
  element.attributes.values
@@ -280,6 +291,12 @@ module Moxml
280
291
  case child
281
292
  when String
282
293
  element.add_text(child)
294
+ when ::Moxml::Adapter::CustomizedRexml::EntityReference
295
+ # REXML doesn't support custom node types in its tree.
296
+ # Store alongside native children via instance variable.
297
+ refs = element.instance_variable_get(:@moxml_entity_refs) || []
298
+ refs << child
299
+ element.instance_variable_set(:@moxml_entity_refs, refs)
283
300
  else
284
301
  element.add(child)
285
302
  end
@@ -373,6 +390,8 @@ module Moxml
373
390
  case node
374
391
  when ::REXML::Text, ::REXML::CData
375
392
  node.value.to_s
393
+ when ::Moxml::Adapter::CustomizedRexml::EntityReference
394
+ ""
376
395
  when ::REXML::Element
377
396
  # Extract text recursively from all children to match other adapters
378
397
  extract_text_recursively(node)
@@ -428,7 +447,7 @@ module Moxml
428
447
  # add a namespace prefix to the element name AND a namespace definition
429
448
  def set_namespace(element, ns)
430
449
  prefix = ns.name.to_s.empty? ? "xmlns" : ns.name.to_s
431
- if element.respond_to?(:add_namespace)
450
+ if element.is_a?(::REXML::Element)
432
451
  element.add_namespace(prefix,
433
452
  ns.value)
434
453
  end
@@ -45,6 +45,9 @@ module Moxml
45
45
 
46
46
  def remove
47
47
  adapter.remove_attribute(element, name)
48
+ if @parent_node.is_a?(Moxml::Element)
49
+ @parent_node.instance_variable_set(:@attributes_cache, nil)
50
+ end
48
51
  self
49
52
  end
50
53
 
data/lib/moxml/builder.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  module Moxml
4
4
  class Builder
5
5
  attr_reader :document
6
+ alias_method :doc, :document
6
7
 
7
8
  def initialize(context)
8
9
  @context = context
data/lib/moxml/config.rb CHANGED
@@ -24,7 +24,7 @@ module Moxml
24
24
  end
25
25
  end
26
26
 
27
- NAMESPACE_URI_MODES = %i[strict lenient].freeze
27
+ NAMESPACE_VALIDATION_MODES = %i[strict lenient].freeze
28
28
 
29
29
  attr_reader :adapter_name
30
30
  attr_accessor :strict_parsing,
@@ -35,7 +35,7 @@ module Moxml
35
35
  :preload_entity_sets,
36
36
  :entity_load_mode,
37
37
  :entity_provider,
38
- :namespace_uri_mode
38
+ :namespace_validation_mode
39
39
 
40
40
  def initialize(adapter_name = nil, strict_parsing = nil,
41
41
  default_encoding = nil)
@@ -49,7 +49,7 @@ module Moxml
49
49
  @preload_entity_sets = []
50
50
  @entity_load_mode = :required
51
51
  @entity_provider = nil
52
- @namespace_uri_mode = :strict
52
+ @namespace_validation_mode = :strict
53
53
  end
54
54
 
55
55
  def adapter=(name)
@@ -86,14 +86,14 @@ module Moxml
86
86
  @entity_load_mode = mode
87
87
  end
88
88
 
89
- def namespace_uri_mode=(mode)
89
+ def namespace_validation_mode=(mode)
90
90
  mode = mode.to_sym
91
- unless NAMESPACE_URI_MODES.include?(mode)
91
+ unless NAMESPACE_VALIDATION_MODES.include?(mode)
92
92
  raise ArgumentError,
93
- "Invalid namespace_uri_mode: #{mode}. Must be one of: #{NAMESPACE_URI_MODES.join(', ')}"
93
+ "Invalid namespace_validation_mode: #{mode}. Must be one of: #{NAMESPACE_VALIDATION_MODES.join(', ')}"
94
94
  end
95
95
 
96
- @namespace_uri_mode = mode
96
+ @namespace_validation_mode = mode
97
97
  end
98
98
 
99
99
  # Backward compatibility: convert old boolean to new symbol
@@ -26,11 +26,13 @@ module Moxml
26
26
 
27
27
  def root=(element)
28
28
  adapter.set_root(@native, element.native)
29
+ element.instance_variable_set(:@parent_node, self)
30
+ invalidate_children_cache!
29
31
  end
30
32
 
31
33
  def root
32
34
  root_element = adapter.root(@native)
33
- root_element ? Element.wrap(root_element, context) : nil
35
+ root_element ? Element.new(root_element, context) : nil
34
36
  end
35
37
 
36
38
  def create_element(name)
@@ -91,6 +93,8 @@ module Moxml
91
93
  else
92
94
  adapter.add_child(@native, node.native)
93
95
  end
96
+ node.instance_variable_set(:@parent_node, self)
97
+ invalidate_children_cache!
94
98
  self
95
99
  end
96
100