moxml 0.1.21 → 0.1.23

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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/opal.yml +37 -0
  3. data/.gitignore +1 -0
  4. data/.rspec-opal +5 -0
  5. data/.rubocop.yml +1 -0
  6. data/.rubocop_todo.yml +680 -110
  7. data/Gemfile +6 -0
  8. data/Rakefile +70 -0
  9. data/lib/compat/opal/rexml/namespace.rb +59 -0
  10. data/lib/compat/opal/rexml/parsers/baseparser.rb +1016 -0
  11. data/lib/compat/opal/rexml/source.rb +214 -0
  12. data/lib/compat/opal/rexml/text.rb +426 -0
  13. data/lib/compat/opal/rexml/xmltokens.rb +45 -0
  14. data/lib/compat/opal/rexml_compat.rb +77 -0
  15. data/lib/moxml/adapter/customized_oga/xml_declaration.rb +8 -1
  16. data/lib/moxml/adapter/customized_rexml/formatter.rb +11 -10
  17. data/lib/moxml/adapter/headed_ox.rb +2 -6
  18. data/lib/moxml/adapter/libxml/entity_ref_registry.rb +4 -2
  19. data/lib/moxml/adapter/libxml/entity_restorer.rb +3 -1
  20. data/lib/moxml/adapter/libxml.rb +22 -24
  21. data/lib/moxml/adapter/nokogiri.rb +24 -33
  22. data/lib/moxml/adapter/oga.rb +47 -84
  23. data/lib/moxml/adapter/ox.rb +43 -41
  24. data/lib/moxml/adapter/rexml.rb +29 -33
  25. data/lib/moxml/adapter.rb +38 -8
  26. data/lib/moxml/config.rb +16 -3
  27. data/lib/moxml/document.rb +2 -8
  28. data/lib/moxml/entity_registry.rb +40 -31
  29. data/lib/moxml/entity_registry_opal_data.rb +2138 -0
  30. data/lib/moxml/node.rb +27 -26
  31. data/lib/moxml/sax/namespace_splitter.rb +54 -0
  32. data/lib/moxml/version.rb +1 -1
  33. data/lib/moxml/xml_utils.rb +10 -1
  34. data/lib/moxml.rb +7 -0
  35. data/spec/consistency/adapter_parity_spec.rb +1 -1
  36. data/spec/integration/all_adapters_spec.rb +2 -1
  37. data/spec/integration/shared_examples/line_ending_behavior.rb +56 -0
  38. data/spec/integration/w3c_namespace_spec.rb +1 -1
  39. data/spec/moxml/adapter/libxml_internals_spec.rb +4 -2
  40. data/spec/moxml/adapter/ox_spec.rb +8 -0
  41. data/spec/moxml/adapter/platform_spec.rb +70 -0
  42. data/spec/moxml/adapter/shared_examples/adapter_contract.rb +0 -6
  43. data/spec/moxml/config_spec.rb +33 -0
  44. data/spec/moxml/entity_registry_spec.rb +10 -0
  45. data/spec/moxml/native_attachment/opal_spec.rb +39 -2
  46. data/spec/moxml/node_type_map_spec.rb +43 -0
  47. data/spec/moxml/opal_rexml_adapter_spec.rb +14 -0
  48. data/spec/moxml/opal_smoke_spec.rb +61 -0
  49. data/spec/moxml/sax/namespace_splitter_spec.rb +67 -0
  50. data/spec/moxml/text_spec.rb +1 -1
  51. data/spec/spec_helper.rb +32 -13
  52. data/spec/support/opal.rb +16 -0
  53. metadata +19 -2
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ # backtick_javascript: true
4
+
5
+ require "corelib/array/pack"
6
+
7
+ unless defined?(StringScanner::Version)
8
+ class StringScanner
9
+ Version = "3.0.8"
10
+ end
11
+ end
12
+
13
+ unless String.method_defined?(:force_encoding)
14
+ class String
15
+ def force_encoding(*)
16
+ self
17
+ end
18
+ end
19
+ end
20
+
21
+ unless defined?(Encoding)
22
+ module ::Encoding
23
+ UTF_8 = "UTF-8"
24
+ ASCII_8BIT = "ASCII-8BIT"
25
+ end
26
+ end
27
+
28
+ unless String.method_defined?(:encode)
29
+ class String
30
+ def encode(*)
31
+ self
32
+ end
33
+ end
34
+ end
35
+
36
+ # Opal defines mutable String methods as raising NotImplementedError.
37
+ # Override with functional equivalents that return new strings.
38
+ class String
39
+ def <<(str)
40
+ `return self + #{str}.to_s`
41
+ end
42
+
43
+ def chomp!(sep = nil)
44
+ `
45
+ var r = #{chomp(sep)};
46
+ return r === self ? nil : r;
47
+ `
48
+ end
49
+
50
+ def gsub!(pattern, replacement, &block)
51
+ `
52
+ var r = #{gsub(pattern, replacement, &block)};
53
+ return r === self ? nil : r;
54
+ `
55
+ end
56
+
57
+ def squeeze!(*sets)
58
+ `
59
+ var r = #{squeeze(*sets)};
60
+ return r === self ? nil : r;
61
+ `
62
+ end
63
+
64
+ def strip!
65
+ `
66
+ var r = #{strip};
67
+ return r === self ? nil : r;
68
+ `
69
+ end
70
+ end
71
+
72
+ class StringIO
73
+ def <<(str)
74
+ write(str)
75
+ self
76
+ end
77
+ end
@@ -8,10 +8,17 @@ module Moxml
8
8
  class XmlDeclaration < ::Oga::XML::XmlDeclaration
9
9
  def initialize(options = {})
10
10
  @version = options[:version] || "1.0"
11
- # encoding is optional, but Oga sets it to UTF-8 by default
12
11
  @encoding = options[:encoding]
13
12
  @standalone = options[:standalone]
14
13
  end
14
+
15
+ def to_xml
16
+ parts = ["<?xml"]
17
+ parts << %( version="#{version}") if version
18
+ parts << %( encoding="#{encoding}") if encoding
19
+ parts << %( standalone="#{standalone}") if standalone
20
+ "#{parts.join}?>"
21
+ end
15
22
  end
16
23
  end
17
24
  end
@@ -27,8 +27,11 @@ module Moxml
27
27
  end
28
28
  end
29
29
 
30
+ def indented?
31
+ !@indentation.empty?
32
+ end
33
+
30
34
  def write_element(node, output)
31
- # output << ' ' * @level
32
35
  output << "<#{node.expanded_name}"
33
36
  write_attributes(node, output)
34
37
 
@@ -45,18 +48,16 @@ module Moxml
45
48
 
46
49
  output << ">"
47
50
 
48
- # Check for mixed content
49
51
  has_text = node.children.any? { |c| c.is_a?(::REXML::Text) && !c.to_s.strip.empty? }
50
52
  has_elements = node.children.any?(::REXML::Element)
51
- mixed = has_text && has_elements
53
+ indent_children = indented? && has_elements && !has_text
52
54
 
53
55
  # Handle children based on content type
54
56
  all_children_empty = node.children.empty? && !(entity_refs && !entity_refs.empty?)
55
57
  unless all_children_empty
56
- @level += @indentation.length unless mixed
58
+ @level += @indentation.length if indent_children
57
59
 
58
60
  if entity_refs && !entity_refs.empty? && child_sequence
59
- # Interleave native children with entity refs using tracked sequence
60
61
  eref_idx = 0
61
62
  native_idx = 0
62
63
  child_sequence.each do |type|
@@ -69,10 +70,12 @@ module Moxml
69
70
  child.to_s.strip.empty? &&
70
71
  !(child.next_sibling.nil? && child.previous_sibling.nil?)
71
72
 
73
+ output << "\n" << (" " * @level) if indent_children
72
74
  write(child, output)
73
75
  end
74
76
  when :eref
75
77
  if eref_idx < entity_refs.size
78
+ output << "\n" << (" " * @level) if indent_children
76
79
  write(entity_refs[eref_idx], output)
77
80
  eref_idx += 1
78
81
  end
@@ -80,24 +83,22 @@ module Moxml
80
83
  end
81
84
  else
82
85
  node.children.each_with_index do |child, _index|
83
- # Skip insignificant whitespace
84
86
  next if child.is_a?(::REXML::Text) &&
85
87
  child.to_s.strip.empty? &&
86
88
  !(child.next_sibling.nil? && child.previous_sibling.nil?)
87
89
 
90
+ output << "\n" << (" " * @level) if indent_children
88
91
  write(child, output)
89
92
  end
90
93
  end
91
94
 
92
- # Reset indentation for closing tag in non-mixed content
93
- unless mixed
95
+ if indent_children
94
96
  @level -= @indentation.length
95
- # output << ' ' * @level
97
+ output << "\n" << (" " * @level)
96
98
  end
97
99
  end
98
100
 
99
101
  output << "</#{node.expanded_name}>"
100
- # output << "\n" unless mixed
101
102
  end
102
103
 
103
104
  def write_text(node, output)
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ return if RUBY_ENGINE == "opal"
4
+
3
5
  require_relative "ox"
4
6
  require_relative "../xpath"
5
7
  # Force load XPath modules (autoload doesn't work well with relative requires in examples)
@@ -66,14 +68,8 @@ module Moxml
66
68
  # @param [Hash] namespaces Namespace prefix mappings
67
69
  # @return [Array, Object] Native node array or scalar value
68
70
  def xpath(node, expression, namespaces = {})
69
- # If we receive a native node, wrap it first
70
- # Document#xpath passes @native, but our compiled XPath needs Moxml nodes
71
71
  unless node.is_a?(Moxml::Node)
72
- # Determine the context from the node if possible
73
- # For now, create a basic context for wrapped nodes
74
72
  ctx = Context.new(:headed_ox)
75
-
76
- # Wrap the native node - don't rebuild the whole document
77
73
  node = Moxml::Node.wrap(node, ctx)
78
74
  end
79
75
 
@@ -9,7 +9,8 @@ module Moxml
9
9
  ENTITY_REFS_KEY = :_entity_ref_pairs
10
10
  CHILD_SEQUENCE_KEY = :_child_seq_pairs
11
11
  NON_WHITESPACE_RE = /\S/
12
- private_constant :ENTITY_REFS_KEY, :CHILD_SEQUENCE_KEY, :NON_WHITESPACE_RE
12
+ private_constant :ENTITY_REFS_KEY, :CHILD_SEQUENCE_KEY,
13
+ :NON_WHITESPACE_RE
13
14
 
14
15
  def initialize(attachments, doc)
15
16
  @attachments = attachments
@@ -34,7 +35,8 @@ module Moxml
34
35
  if existing
35
36
  existing << :eref
36
37
  else
37
- seq_by_path[path] = Array.new(count_native_children(element), :native)
38
+ seq_by_path[path] =
39
+ Array.new(count_native_children(element), :native)
38
40
  seq_by_path[path] << :eref
39
41
  @attachments.set(@doc, CHILD_SEQUENCE_KEY, seq_by_path)
40
42
  end
@@ -76,7 +76,9 @@ module Moxml
76
76
  def append_chunk(parent, type, payload)
77
77
  case type
78
78
  when :text
79
- parent.add_child(::Moxml::Text.new(@adapter.create_native_text(payload), @ctx))
79
+ parent.add_child(::Moxml::Text.new(
80
+ @adapter.create_native_text(payload), @ctx
81
+ ))
80
82
  when :eref
81
83
  parent.add_child(
82
84
  ::Moxml::EntityReference.new(
@@ -1,8 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ return if RUBY_ENGINE == "opal"
4
+
3
5
  require_relative "base"
4
6
  require "libxml"
5
7
  require_relative "customized_libxml"
8
+ require_relative "../sax/namespace_splitter"
6
9
 
7
10
  module Moxml
8
11
  module Adapter
@@ -1179,7 +1182,8 @@ module Moxml
1179
1182
  end
1180
1183
 
1181
1184
  ESCAPE_XML_RE = /[&<>"]/
1182
- ESCAPE_XML_MAP = { "&" => "&amp;", "<" => "&lt;", ">" => "&gt;", '"' => "&quot;" }.freeze
1185
+ ESCAPE_XML_MAP = { "&" => "&amp;", "<" => "&lt;", ">" => "&gt;",
1186
+ '"' => "&quot;" }.freeze
1183
1187
  private_constant :ESCAPE_XML_RE, :ESCAPE_XML_MAP
1184
1188
 
1185
1189
  def escape_xml(text)
@@ -1275,7 +1279,13 @@ module Moxml
1275
1279
  # attachment query that otherwise fires for every element under
1276
1280
  # Monitor#synchronize.
1277
1281
  eref_active = doc_eref_active?(elem.doc) if eref_active.nil?
1278
- entity_refs, child_sequence = eref_active ? lookup_entity_ref_serialization(elem) : [nil, nil]
1282
+ entity_refs, child_sequence = if eref_active
1283
+ lookup_entity_ref_serialization(elem)
1284
+ else
1285
+ [
1286
+ nil, nil
1287
+ ]
1288
+ end
1279
1289
 
1280
1290
  # Always use verbose format <tag></tag> for consistency with other adapters
1281
1291
  output << ">"
@@ -1619,8 +1629,14 @@ module Moxml
1619
1629
  # duplicated — callers that need the subtree use deep_duplicate_node.
1620
1630
  def shallow_duplicate_element(native_node)
1621
1631
  new_node = ::LibXML::XML::Node.new(native_node.name)
1622
- copy_element_namespaces(native_node, new_node) if native_node.is_a?(::LibXML::XML::Node)
1623
- copy_element_attributes(native_node, new_node) if native_node.attributes?
1632
+ if native_node.is_a?(::LibXML::XML::Node)
1633
+ copy_element_namespaces(native_node,
1634
+ new_node)
1635
+ end
1636
+ if native_node.attributes?
1637
+ copy_element_attributes(native_node,
1638
+ new_node)
1639
+ end
1624
1640
  new_node
1625
1641
  end
1626
1642
 
@@ -1656,6 +1672,7 @@ module Moxml
1656
1672
  # @private
1657
1673
  class LibXMLSAXBridge
1658
1674
  include ::LibXML::XML::SaxParser::Callbacks
1675
+ include Moxml::SAX::NamespaceSplitter
1659
1676
 
1660
1677
  def initialize(handler)
1661
1678
  @handler = handler
@@ -1672,26 +1689,7 @@ module Moxml
1672
1689
  end
1673
1690
 
1674
1691
  def on_start_element(name, attributes)
1675
- # Convert LibXML attributes hash to separate attrs and namespaces
1676
- attr_hash = {}
1677
- ns_hash = {}
1678
-
1679
- attributes&.each do |attr_name, attr_value|
1680
- if attr_name.to_s.start_with?("xmlns")
1681
- # Namespace declaration
1682
- prefix = if attr_name.to_s == "xmlns"
1683
- nil
1684
- else
1685
- attr_name.to_s.sub(
1686
- "xmlns:", ""
1687
- )
1688
- end
1689
- ns_hash[prefix] = attr_value
1690
- else
1691
- attr_hash[attr_name.to_s] = attr_value
1692
- end
1693
- end
1694
-
1692
+ attr_hash, ns_hash = split_attributes_and_namespaces(attributes)
1695
1693
  @handler.on_start_element(name.to_s, attr_hash, ns_hash)
1696
1694
  end
1697
1695
 
@@ -1,7 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ return if RUBY_ENGINE == "opal"
4
+
3
5
  require_relative "base"
4
6
  require "nokogiri"
7
+ require_relative "../sax/namespace_splitter"
5
8
 
6
9
  module Moxml
7
10
  module Adapter
@@ -242,25 +245,22 @@ module Moxml
242
245
  end
243
246
 
244
247
  def add_child(element, child)
245
- # Special handling for declarations on Nokogiri documents
246
248
  if element.is_a?(::Nokogiri::XML::Document) &&
247
249
  child.is_a?(::Nokogiri::XML::ProcessingInstruction) &&
248
250
  child.name == "xml"
249
- # Set document's xml_decl property
250
251
  version = declaration_attribute(child, "version") || "1.0"
251
252
  encoding = declaration_attribute(child, "encoding")
252
253
  standalone = declaration_attribute(child, "standalone")
253
254
 
254
- # Store declaration state in attachment map
255
255
  attachments.set(element, :xml_decl, {
256
256
  version: version,
257
257
  encoding: encoding,
258
258
  standalone: standalone,
259
259
  }.compact)
260
+ return
260
261
  end
261
262
 
262
263
  if node_type(child) == :doctype
263
- # avoid exceptions: cannot reparent Nokogiri::XML::DTD there
264
264
  element.create_internal_subset(
265
265
  child.name, child.external_id, child.system_id
266
266
  )
@@ -394,23 +394,28 @@ module Moxml
394
394
  save_options |= ::Nokogiri::XML::Node::SaveOptions::FORMAT
395
395
  end
396
396
 
397
- # Handle declaration option
398
- # Priority:
399
- # 1. Explicit no_declaration option
400
- # 2. Check attachment-stored xml_decl (when remove is called, this becomes nil)
401
- if options.key?(:no_declaration)
402
- save_options |= ::Nokogiri::XML::Node::SaveOptions::NO_DECLARATION if options[:no_declaration]
403
- elsif attachments.key?(node, :xml_decl)
404
- # State stored in attachment - if nil, declaration was removed
405
- xml_decl = attachments.get(node, :xml_decl)
406
- save_options |= ::Nokogiri::XML::Node::SaveOptions::NO_DECLARATION if xml_decl.nil?
397
+ custom_decl = nil
398
+ if options[:no_declaration]
399
+ save_options |= ::Nokogiri::XML::Node::SaveOptions::NO_DECLARATION
400
+ elsif attachments.key?(node, :xml_decl) && (xml_decl = attachments.get(node, :xml_decl))
401
+ save_options |= ::Nokogiri::XML::Node::SaveOptions::NO_DECLARATION
402
+ attrs = ["version=\"#{xml_decl[:version]}\""]
403
+ attrs << "encoding=\"#{xml_decl[:encoding]}\"" if xml_decl[:encoding]
404
+ attrs << "standalone=\"#{xml_decl[:standalone]}\"" if xml_decl[:standalone]
405
+ custom_decl = "<?xml #{attrs.join(' ')}?>"
407
406
  end
408
407
 
409
- node.to_xml(
408
+ result = node.to_xml(
410
409
  indent: options[:indent],
411
410
  encoding: options[:encoding],
412
411
  save_with: save_options,
413
412
  )
413
+
414
+ if custom_decl
415
+ result = "#{custom_decl}\n#{result}"
416
+ end
417
+
418
+ result
414
419
  end
415
420
 
416
421
  def has_declaration?(native_doc, wrapper)
@@ -446,6 +451,8 @@ module Moxml
446
451
  #
447
452
  # @private
448
453
  class NokogiriSAXBridge < ::Nokogiri::XML::SAX::Document
454
+ include Moxml::SAX::NamespaceSplitter
455
+
449
456
  def initialize(handler)
450
457
  super()
451
458
  @handler = handler
@@ -462,24 +469,8 @@ module Moxml
462
469
  end
463
470
 
464
471
  def start_element(name, attributes = [])
465
- # Convert Nokogiri attributes array to hash
466
- attr_hash = {}
467
- namespaces_hash = {}
468
-
469
- attributes.each do |attr|
470
- attr_name = attr[0]
471
- attr_value = attr[1]
472
-
473
- if attr_name.start_with?("xmlns")
474
- # Namespace declaration
475
- prefix = attr_name == "xmlns" ? nil : attr_name.sub("xmlns:", "")
476
- namespaces_hash[prefix] = attr_value
477
- else
478
- attr_hash[attr_name] = attr_value
479
- end
480
- end
481
-
482
- @handler.on_start_element(name, attr_hash, namespaces_hash)
472
+ attr_hash, ns_hash = split_attributes_and_namespaces(attributes)
473
+ @handler.on_start_element(name, attr_hash, ns_hash)
483
474
  end
484
475
 
485
476
  def end_element(name)
@@ -3,6 +3,7 @@
3
3
  require_relative "base"
4
4
  require_relative "customized_oga"
5
5
  require "oga"
6
+ require_relative "../sax/namespace_splitter"
6
7
 
7
8
  module Moxml
8
9
  module Adapter
@@ -288,11 +289,25 @@ module Moxml
288
289
  child_or_text
289
290
  end
290
291
 
291
- # Special handling for declarations on Oga documents
292
292
  if element.is_a?(::Oga::XML::Document) &&
293
293
  child.is_a?(::Oga::XML::XmlDeclaration)
294
- # Track declaration state in attachment map
295
294
  attachments.set(element, :xml_declaration, child)
295
+ return
296
+ end
297
+
298
+ # Insert doctype before root element in document
299
+ if element.is_a?(::Oga::XML::Document) && child.is_a?(::Oga::XML::Doctype)
300
+ root_idx = nil
301
+ element.children.each_with_index do |n, i|
302
+ if n.is_a?(::Oga::XML::Element)
303
+ root_idx = i
304
+ break
305
+ end
306
+ end
307
+ if root_idx
308
+ element.children.insert(root_idx, child)
309
+ return
310
+ end
296
311
  end
297
312
 
298
313
  element.children << child
@@ -464,86 +479,53 @@ module Moxml
464
479
 
465
480
  private
466
481
 
482
+ def declaration_to_xml(decl)
483
+ parts = ["<?xml"]
484
+ parts << %( version="#{decl.version}") if decl.version
485
+ parts << %( encoding="#{decl.encoding}") if decl.encoding
486
+ parts << %( standalone="#{decl.standalone}") if decl.standalone
487
+ "#{parts.join}?>"
488
+ end
489
+
467
490
  def serialize_without_entity_processing(node, options = {})
468
- # Oga's XmlGenerator doesn't support options directly
469
- # We need to handle declaration options ourselves for Document nodes
470
491
  if node.is_a?(::Oga::XML::Document)
471
- # Check if we should include declaration
472
- # Priority: explicit option > existence of xml_declaration (native or attachment)
473
- effective_xml_declaration = node.xml_declaration || attachments.get(
474
- node, :xml_declaration
475
- )
492
+ effective_xml_declaration = attachments.get(node, :xml_declaration)
493
+
476
494
  should_include_decl = if options.key?(:no_declaration)
477
495
  !options[:no_declaration]
478
496
  elsif options.key?(:declaration)
479
497
  options[:declaration]
480
498
  else
481
- # Default: include if document has xml_declaration
482
- effective_xml_declaration ? true : false
499
+ effective_xml_declaration || node.xml_declaration ? true : false
483
500
  end
484
501
 
485
- # Fix: Check if declaration already exists in children
486
- # This prevents duplicate declarations when document already has one
487
- has_existing_declaration = node.children.any?(::Oga::XML::XmlDeclaration)
488
-
489
- if should_include_decl && !effective_xml_declaration && !has_existing_declaration
490
- # Need to add declaration - create default one
491
- output = []
492
- output << '<?xml version="1.0" encoding="UTF-8"?>'
493
- output << "\n"
494
-
495
- # Serialize doctype if present
496
- output << node.doctype.to_xml << "\n" if node.doctype
497
-
498
- # Serialize children
499
- node.children.each do |child|
500
- output << ::Moxml::Adapter::CustomizedOga::XmlGenerator.new(child).to_xml
501
- end
502
-
503
- return output.join
504
- elsif !should_include_decl
505
- # Skip xml_declaration
506
- output = []
507
-
508
- # Serialize doctype if present
509
- output << node.doctype.to_xml << "\n" if node.doctype
510
-
511
- # Serialize root and other children
512
- node.children.each do |child|
513
- next if child.is_a?(::Oga::XML::XmlDeclaration)
502
+ output = []
514
503
 
515
- output << ::Moxml::Adapter::CustomizedOga::XmlGenerator.new(child).to_xml
504
+ if should_include_decl
505
+ decl = effective_xml_declaration || node.xml_declaration
506
+ if decl
507
+ output << declaration_to_xml(decl)
508
+ else
509
+ output << '<?xml version="1.0" encoding="UTF-8"?>'
516
510
  end
517
-
518
- return output.join
511
+ output << "\n"
519
512
  end
520
- end
521
513
 
522
- # Default: use XmlGenerator
523
- # But first check if we need to handle declaration specially
524
- effective_xml_declaration = node.is_a?(::Oga::XML::Document) && (node.xml_declaration || attachments.get(
525
- node, :xml_declaration
526
- ))
527
- if node.is_a?(::Oga::XML::Document) && effective_xml_declaration
528
- # Document has declaration - use custom handling to avoid duplicates
529
- output = []
530
- xml_declaration_serialized = false
514
+ if node.doctype
515
+ output << node.doctype.to_xml
516
+ output << "\n"
517
+ end
531
518
 
532
- # Serialize children, but skip XmlDeclaration if it would cause duplication
533
519
  node.children.each do |child|
534
- xml_declaration = child.is_a?(::Oga::XML::XmlDeclaration)
535
- next if xml_declaration && xml_declaration_serialized
536
-
537
- xml_declaration_serialized = true if xml_declaration
520
+ next if child.is_a?(::Oga::XML::XmlDeclaration)
538
521
 
539
522
  output << ::Moxml::Adapter::CustomizedOga::XmlGenerator.new(child).to_xml
540
523
  end
541
524
 
542
- output.join
543
- else
544
- # Normal case - use XmlGenerator directly
545
- ::Moxml::Adapter::CustomizedOga::XmlGenerator.new(node).to_xml
525
+ return output.join
546
526
  end
527
+
528
+ ::Moxml::Adapter::CustomizedOga::XmlGenerator.new(node).to_xml
547
529
  end
548
530
  end
549
531
  end
@@ -555,6 +537,8 @@ module Moxml
555
537
  #
556
538
  # @private
557
539
  class OgaSAXBridge
540
+ include Moxml::SAX::NamespaceSplitter
541
+
558
542
  def initialize(handler)
559
543
  @handler = handler
560
544
  end
@@ -563,29 +547,8 @@ module Moxml
563
547
  # namespace may be nil
564
548
  # attributes is an array of [name, value] pairs
565
549
  def on_element(namespace, name, attributes)
566
- # Build full qualified name if namespace present
567
550
  element_name = namespace ? "#{namespace}:#{name}" : name
568
-
569
- # Convert Oga attributes to hash
570
- attr_hash = {}
571
- ns_hash = {}
572
-
573
- # Oga delivers attributes as array of [name, value] pairs
574
- attributes.each do |attr_name, attr_value|
575
- if attr_name.to_s.start_with?("xmlns")
576
- prefix = if attr_name.to_s == "xmlns"
577
- nil
578
- else
579
- attr_name.to_s.sub(
580
- "xmlns:", ""
581
- )
582
- end
583
- ns_hash[prefix] = attr_value
584
- else
585
- attr_hash[attr_name.to_s] = attr_value
586
- end
587
- end
588
-
551
+ attr_hash, ns_hash = split_attributes_and_namespaces(attributes)
589
552
  @handler.on_start_element(element_name, attr_hash, ns_hash)
590
553
  end
591
554