moxml 0.1.22 → 0.1.24

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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +1 -0
  4. data/.rubocop_todo.yml +680 -110
  5. data/Rakefile +12 -9
  6. data/lib/compat/opal/moxml_boot.rb +53 -0
  7. data/lib/compat/opal/rexml/namespace.rb +8 -5
  8. data/lib/compat/opal/rexml/parsers/baseparser.rb +276 -212
  9. data/lib/compat/opal/rexml/source.rb +28 -27
  10. data/lib/compat/opal/rexml/text.rb +112 -104
  11. data/lib/compat/opal/rexml/xmltokens.rb +8 -8
  12. data/lib/compat/opal/rexml_compat.rb +12 -11
  13. data/lib/moxml/adapter/base.rb +5 -5
  14. data/lib/moxml/adapter/customized_libxml/cdata.rb +0 -2
  15. data/lib/moxml/adapter/customized_libxml/comment.rb +0 -2
  16. data/lib/moxml/adapter/customized_libxml/element.rb +3 -5
  17. data/lib/moxml/adapter/customized_libxml/node.rb +2 -2
  18. data/lib/moxml/adapter/customized_libxml/processing_instruction.rb +0 -2
  19. data/lib/moxml/adapter/customized_libxml/text.rb +0 -2
  20. data/lib/moxml/adapter/customized_oga/xml_declaration.rb +8 -1
  21. data/lib/moxml/adapter/customized_rexml/formatter.rb +7 -8
  22. data/lib/moxml/adapter/headed_ox.rb +1 -5
  23. data/lib/moxml/adapter/libxml/entity_ref_registry.rb +4 -2
  24. data/lib/moxml/adapter/libxml/entity_restorer.rb +3 -1
  25. data/lib/moxml/adapter/libxml.rb +29 -14
  26. data/lib/moxml/adapter/nokogiri.rb +21 -17
  27. data/lib/moxml/adapter/oga.rb +48 -67
  28. data/lib/moxml/adapter/ox.rb +61 -31
  29. data/lib/moxml/adapter/rexml.rb +5 -6
  30. data/lib/moxml/adapter.rb +13 -5
  31. data/lib/moxml/attribute.rb +5 -2
  32. data/lib/moxml/config.rb +16 -2
  33. data/lib/moxml/context.rb +8 -8
  34. data/lib/moxml/declaration.rb +2 -7
  35. data/lib/moxml/document.rb +2 -19
  36. data/lib/moxml/document_builder.rb +12 -8
  37. data/lib/moxml/element.rb +4 -4
  38. data/lib/moxml/entity_registry.rb +8 -4
  39. data/lib/moxml/entity_registry_opal_data.rb +3 -2
  40. data/lib/moxml/node.rb +45 -14
  41. data/lib/moxml/node_set.rb +2 -4
  42. data/lib/moxml/sax/block_handler.rb +0 -2
  43. data/lib/moxml/sax/element_handler.rb +0 -2
  44. data/lib/moxml/sax/namespace_splitter.rb +5 -4
  45. data/lib/moxml/sax.rb +4 -25
  46. data/lib/moxml/version.rb +1 -1
  47. data/lib/moxml/xml_utils.rb +2 -3
  48. data/lib/moxml/xpath/compiler.rb +1 -49
  49. data/lib/moxml/xpath/conversion.rb +7 -6
  50. data/lib/moxml/xpath/ruby/generator.rb +12 -19
  51. data/lib/moxml/xpath/ruby/node.rb +1 -9
  52. data/lib/moxml/xpath.rb +6 -14
  53. data/lib/moxml.rb +74 -20
  54. data/spec/integration/all_adapters_spec.rb +1 -0
  55. data/spec/integration/shared_examples/line_ending_behavior.rb +56 -0
  56. data/spec/moxml/adapter/libxml_internals_spec.rb +4 -2
  57. data/spec/moxml/adapter/platform_spec.rb +2 -1
  58. data/spec/moxml/attribute_spec.rb +16 -0
  59. data/spec/moxml/config_spec.rb +33 -0
  60. data/spec/moxml/context_spec.rb +14 -0
  61. data/spec/moxml/moxml_spec.rb +13 -0
  62. data/spec/moxml/node_spec.rb +58 -0
  63. data/spec/moxml/xpath/ruby/node_spec.rb +3 -3
  64. metadata +4 -2
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../xml_utils"
4
- require_relative "../document_builder"
5
-
6
3
  module Moxml
7
4
  module Adapter
8
5
  class Base
@@ -104,8 +101,7 @@ module Moxml
104
101
  #
105
102
  # @return [Boolean] true if SAX parsing is supported
106
103
  def sax_supported?
107
- respond_to?(:sax_parse) &&
108
- method(:sax_parse).owner != Moxml::Adapter::Base.singleton_class
104
+ method(:sax_parse).owner != Moxml::Adapter::Base.singleton_class
109
105
  end
110
106
 
111
107
  def create_document(_native_doc = nil)
@@ -212,6 +208,10 @@ namespace_validation_mode: :strict)
212
208
  wrapper.has_xml_declaration
213
209
  end
214
210
 
211
+ # Clear the declaration state from the native document.
212
+ # Called when a Declaration node is removed from a document.
213
+ def remove_declaration(_native_doc); end
214
+
215
215
  # Return the actual native node after an add_child operation.
216
216
  # Override for adapters where node identity may change (e.g., LibXML doc.root=).
217
217
  def actual_native(child_native, _parent_native)
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "node"
4
-
5
3
  module Moxml
6
4
  module Adapter
7
5
  module CustomizedLibxml
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "node"
4
-
5
3
  module Moxml
6
4
  module Adapter
7
5
  module CustomizedLibxml
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "node"
4
-
5
3
  module Moxml
6
4
  module Adapter
7
5
  module CustomizedLibxml
@@ -12,7 +10,7 @@ module Moxml
12
10
  class Element < Node
13
11
  # Add a child to this element, handling document import automatically
14
12
  def add_child(child)
15
- child_native = child.respond_to?(:native) ? child.native : child
13
+ child_native = child.is_a?(Node) ? child.native : child
16
14
 
17
15
  # Check if child needs to be imported
18
16
  if needs_import?(child_native)
@@ -26,9 +24,9 @@ module Moxml
26
24
  private
27
25
 
28
26
  def needs_import?(child_node)
29
- return false unless @native.respond_to?(:doc)
27
+ return false unless @native.is_a?(::LibXML::XML::Node)
30
28
  return false unless @native.doc
31
- return false unless child_node.respond_to?(:doc)
29
+ return false unless child_node.is_a?(::LibXML::XML::Node)
32
30
  return false unless child_node.doc
33
31
 
34
32
  child_node.doc != @native.doc
@@ -22,7 +22,7 @@ module Moxml
22
22
  def ==(other)
23
23
  return false unless other
24
24
 
25
- other_native = other.respond_to?(:native) ? other.native : other
25
+ other_native = other.is_a?(self.class) ? other.native : other
26
26
  @native == other_native
27
27
  end
28
28
 
@@ -34,7 +34,7 @@ module Moxml
34
34
 
35
35
  # Check if node has a document
36
36
  def document_present?
37
- @native.respond_to?(:doc) && !@native.doc.nil?
37
+ @native.is_a?(::LibXML::XML::Node) && !@native.doc.nil?
38
38
  end
39
39
 
40
40
  # Get the document this node belongs to
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "node"
4
-
5
3
  module Moxml
6
4
  module Adapter
7
5
  module CustomizedLibxml
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "node"
4
-
5
3
  module Moxml
6
4
  module Adapter
7
5
  module CustomizedLibxml
@@ -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
@@ -70,12 +70,12 @@ module Moxml
70
70
  child.to_s.strip.empty? &&
71
71
  !(child.next_sibling.nil? && child.previous_sibling.nil?)
72
72
 
73
- output << "\n" << (' ' * @level) if indent_children
73
+ output << "\n" << (" " * @level) if indent_children
74
74
  write(child, output)
75
75
  end
76
76
  when :eref
77
77
  if eref_idx < entity_refs.size
78
- output << "\n" << (' ' * @level) if indent_children
78
+ output << "\n" << (" " * @level) if indent_children
79
79
  write(entity_refs[eref_idx], output)
80
80
  eref_idx += 1
81
81
  end
@@ -87,14 +87,14 @@ module Moxml
87
87
  child.to_s.strip.empty? &&
88
88
  !(child.next_sibling.nil? && child.previous_sibling.nil?)
89
89
 
90
- output << "\n" << (' ' * @level) if indent_children
90
+ output << "\n" << (" " * @level) if indent_children
91
91
  write(child, output)
92
92
  end
93
93
  end
94
94
 
95
95
  if indent_children
96
96
  @level -= @indentation.length
97
- output << "\n" << (' ' * @level)
97
+ output << "\n" << (" " * @level)
98
98
  end
99
99
  end
100
100
 
@@ -185,9 +185,8 @@ module Moxml
185
185
  node.attributes.each do |name, attr|
186
186
  next unless name.to_s.start_with?("xmlns:") || name.to_s == "xmlns"
187
187
 
188
- # convert the default namespace
189
188
  name = "xmlns" if name.to_s == "xmlns:"
190
- value = attr.respond_to?(:value) ? attr.value : attr
189
+ value = attr.is_a?(::REXML::Attribute) ? attr.value : attr
191
190
  output << " #{name}=\"#{value}\""
192
191
  end
193
192
 
@@ -196,14 +195,14 @@ module Moxml
196
195
  next if name.to_s.start_with?("xmlns:") || name.to_s == "xmlns"
197
196
 
198
197
  output << " "
199
- output << if attr.respond_to?(:prefix) && attr.prefix
198
+ output << if attr.is_a?(::REXML::Attribute) && attr.prefix
200
199
  "#{attr.prefix}:#{attr.name}"
201
200
  else
202
201
  name.to_s
203
202
  end
204
203
 
205
204
  output << "=\""
206
- value = attr.respond_to?(:value) ? attr.value : attr
205
+ value = attr.is_a?(::REXML::Attribute) ? attr.value : attr
207
206
  output << escape_attribute_value(value.to_s)
208
207
  output << "\""
209
208
  end # rubocop:enable Style/CombinableLoops
@@ -2,11 +2,7 @@
2
2
 
3
3
  return if RUBY_ENGINE == "opal"
4
4
 
5
- require_relative "ox"
6
- require_relative "../xpath"
7
- # Force load XPath modules (autoload doesn't work well with relative requires in examples)
8
- require_relative "../xpath/parser"
9
- require_relative "../xpath/compiler"
5
+ require "moxml/adapter/ox"
10
6
 
11
7
  module Moxml
12
8
  module Adapter
@@ -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(
@@ -2,14 +2,14 @@
2
2
 
3
3
  return if RUBY_ENGINE == "opal"
4
4
 
5
- require_relative "base"
6
5
  require "libxml"
7
- require_relative "customized_libxml"
8
- require_relative "../sax/namespace_splitter"
9
6
 
10
7
  module Moxml
11
8
  module Adapter
12
9
  class Libxml < Base
10
+ autoload :EntityRefRegistry, "moxml/adapter/libxml/entity_ref_registry"
11
+ autoload :EntityRestorer, "moxml/adapter/libxml/entity_restorer"
12
+
13
13
  # Wrapper class to store DOCTYPE information
14
14
  class DoctypeWrapper
15
15
  attr_reader :native_doc
@@ -239,7 +239,7 @@ module Moxml
239
239
  # Duck-typed fallback for libxml types that aren't ::Node
240
240
  # subclasses but still expose node_type (e.g. ::Attr).
241
241
  native = unpatch_node(node)
242
- return :unknown unless native.respond_to?(:node_type)
242
+ return :unknown unless native.is_a?(::LibXML::XML::Node)
243
243
 
244
244
  NATIVE_NODE_TYPE_MAP[native.node_type] || :unknown
245
245
  end
@@ -836,7 +836,7 @@ module Moxml
836
836
  return [] unless namespaces
837
837
 
838
838
  namespace_list =
839
- if namespaces.respond_to?(:definitions)
839
+ if namespaces.is_a?(::LibXML::XML::Namespaces)
840
840
  namespaces.definitions
841
841
  else
842
842
  namespaces
@@ -1076,6 +1076,11 @@ module Moxml
1076
1076
  end
1077
1077
  end
1078
1078
 
1079
+ def remove_declaration(native_doc)
1080
+ decl = attachments.get(native_doc, :declaration)
1081
+ decl&.removed = true
1082
+ end
1083
+
1079
1084
  # LibXML's doc.root= creates a new Ruby wrapper with different object_id.
1080
1085
  # Return the actual root node so attachments are stored on the correct object.
1081
1086
  def actual_native(child_native, parent_native)
@@ -1182,7 +1187,8 @@ module Moxml
1182
1187
  end
1183
1188
 
1184
1189
  ESCAPE_XML_RE = /[&<>"]/
1185
- ESCAPE_XML_MAP = { "&" => "&amp;", "<" => "&lt;", ">" => "&gt;", '"' => "&quot;" }.freeze
1190
+ ESCAPE_XML_MAP = { "&" => "&amp;", "<" => "&lt;", ">" => "&gt;",
1191
+ '"' => "&quot;" }.freeze
1186
1192
  private_constant :ESCAPE_XML_RE, :ESCAPE_XML_MAP
1187
1193
 
1188
1194
  def escape_xml(text)
@@ -1278,7 +1284,13 @@ module Moxml
1278
1284
  # attachment query that otherwise fires for every element under
1279
1285
  # Monitor#synchronize.
1280
1286
  eref_active = doc_eref_active?(elem.doc) if eref_active.nil?
1281
- entity_refs, child_sequence = eref_active ? lookup_entity_ref_serialization(elem) : [nil, nil]
1287
+ entity_refs, child_sequence = if eref_active
1288
+ lookup_entity_ref_serialization(elem)
1289
+ else
1290
+ [
1291
+ nil, nil
1292
+ ]
1293
+ end
1282
1294
 
1283
1295
  # Always use verbose format <tag></tag> for consistency with other adapters
1284
1296
  output << ">"
@@ -1312,7 +1324,7 @@ module Moxml
1312
1324
  return unless elem.is_a?(::LibXML::XML::Node)
1313
1325
 
1314
1326
  ns_list = elem.namespaces
1315
- return unless ns_list.respond_to?(:definitions)
1327
+ return unless ns_list.is_a?(::LibXML::XML::Namespaces)
1316
1328
 
1317
1329
  definitions = ns_list.definitions
1318
1330
  return if definitions.empty?
@@ -1544,7 +1556,7 @@ module Moxml
1544
1556
 
1545
1557
  # Also check if this element has an active namespace (inherited or own)
1546
1558
  # This catches cases where elements inherit namespaces from parents
1547
- if node.is_a?(::LibXML::XML::Node) && node.namespaces.respond_to?(:namespace)
1559
+ if node.is_a?(::LibXML::XML::Node) && node.namespaces.is_a?(::LibXML::XML::Namespaces)
1548
1560
  active_ns = node.namespaces.namespace
1549
1561
  if active_ns
1550
1562
  prefix = active_ns.prefix
@@ -1622,8 +1634,14 @@ module Moxml
1622
1634
  # duplicated — callers that need the subtree use deep_duplicate_node.
1623
1635
  def shallow_duplicate_element(native_node)
1624
1636
  new_node = ::LibXML::XML::Node.new(native_node.name)
1625
- copy_element_namespaces(native_node, new_node) if native_node.is_a?(::LibXML::XML::Node)
1626
- copy_element_attributes(native_node, new_node) if native_node.attributes?
1637
+ if native_node.is_a?(::LibXML::XML::Node)
1638
+ copy_element_namespaces(native_node,
1639
+ new_node)
1640
+ end
1641
+ if native_node.attributes?
1642
+ copy_element_attributes(native_node,
1643
+ new_node)
1644
+ end
1627
1645
  new_node
1628
1646
  end
1629
1647
 
@@ -1707,6 +1725,3 @@ module Moxml
1707
1725
  end
1708
1726
  end
1709
1727
  end
1710
-
1711
- require_relative "libxml/entity_ref_registry"
1712
- require_relative "libxml/entity_restorer"
@@ -2,9 +2,7 @@
2
2
 
3
3
  return if RUBY_ENGINE == "opal"
4
4
 
5
- require_relative "base"
6
5
  require "nokogiri"
7
- require_relative "../sax/namespace_splitter"
8
6
 
9
7
  module Moxml
10
8
  module Adapter
@@ -245,25 +243,22 @@ module Moxml
245
243
  end
246
244
 
247
245
  def add_child(element, child)
248
- # Special handling for declarations on Nokogiri documents
249
246
  if element.is_a?(::Nokogiri::XML::Document) &&
250
247
  child.is_a?(::Nokogiri::XML::ProcessingInstruction) &&
251
248
  child.name == "xml"
252
- # Set document's xml_decl property
253
249
  version = declaration_attribute(child, "version") || "1.0"
254
250
  encoding = declaration_attribute(child, "encoding")
255
251
  standalone = declaration_attribute(child, "standalone")
256
252
 
257
- # Store declaration state in attachment map
258
253
  attachments.set(element, :xml_decl, {
259
254
  version: version,
260
255
  encoding: encoding,
261
256
  standalone: standalone,
262
257
  }.compact)
258
+ return
263
259
  end
264
260
 
265
261
  if node_type(child) == :doctype
266
- # avoid exceptions: cannot reparent Nokogiri::XML::DTD there
267
262
  element.create_internal_subset(
268
263
  child.name, child.external_id, child.system_id
269
264
  )
@@ -397,23 +392,28 @@ module Moxml
397
392
  save_options |= ::Nokogiri::XML::Node::SaveOptions::FORMAT
398
393
  end
399
394
 
400
- # Handle declaration option
401
- # Priority:
402
- # 1. Explicit no_declaration option
403
- # 2. Check attachment-stored xml_decl (when remove is called, this becomes nil)
404
- if options.key?(:no_declaration)
405
- save_options |= ::Nokogiri::XML::Node::SaveOptions::NO_DECLARATION if options[:no_declaration]
406
- elsif attachments.key?(node, :xml_decl)
407
- # State stored in attachment - if nil, declaration was removed
408
- xml_decl = attachments.get(node, :xml_decl)
409
- save_options |= ::Nokogiri::XML::Node::SaveOptions::NO_DECLARATION if xml_decl.nil?
395
+ custom_decl = nil
396
+ if options[:no_declaration]
397
+ save_options |= ::Nokogiri::XML::Node::SaveOptions::NO_DECLARATION
398
+ elsif attachments.key?(node, :xml_decl) && (xml_decl = attachments.get(node, :xml_decl))
399
+ save_options |= ::Nokogiri::XML::Node::SaveOptions::NO_DECLARATION
400
+ attrs = ["version=\"#{xml_decl[:version]}\""]
401
+ attrs << "encoding=\"#{xml_decl[:encoding]}\"" if xml_decl[:encoding]
402
+ attrs << "standalone=\"#{xml_decl[:standalone]}\"" if xml_decl[:standalone]
403
+ custom_decl = "<?xml #{attrs.join(' ')}?>"
410
404
  end
411
405
 
412
- node.to_xml(
406
+ result = node.to_xml(
413
407
  indent: options[:indent],
414
408
  encoding: options[:encoding],
415
409
  save_with: save_options,
416
410
  )
411
+
412
+ if custom_decl
413
+ result = "#{custom_decl}\n#{result}"
414
+ end
415
+
416
+ result
417
417
  end
418
418
 
419
419
  def has_declaration?(native_doc, wrapper)
@@ -424,6 +424,10 @@ module Moxml
424
424
  end
425
425
  end
426
426
 
427
+ def remove_declaration(native_doc)
428
+ attachments.set(native_doc, :xml_decl, nil)
429
+ end
430
+
427
431
  private
428
432
 
429
433
  def build_declaration_attrs(version, encoding, standalone)
@@ -1,9 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "base"
4
- require_relative "customized_oga"
5
3
  require "oga"
6
- require_relative "../sax/namespace_splitter"
7
4
 
8
5
  module Moxml
9
6
  module Adapter
@@ -289,11 +286,25 @@ module Moxml
289
286
  child_or_text
290
287
  end
291
288
 
292
- # Special handling for declarations on Oga documents
293
289
  if element.is_a?(::Oga::XML::Document) &&
294
290
  child.is_a?(::Oga::XML::XmlDeclaration)
295
- # Track declaration state in attachment map
296
291
  attachments.set(element, :xml_declaration, child)
292
+ return
293
+ end
294
+
295
+ # Insert doctype before root element in document
296
+ if element.is_a?(::Oga::XML::Document) && child.is_a?(::Oga::XML::Doctype)
297
+ root_idx = nil
298
+ element.children.each_with_index do |n, i|
299
+ if n.is_a?(::Oga::XML::Element)
300
+ root_idx = i
301
+ break
302
+ end
303
+ end
304
+ if root_idx
305
+ element.children.insert(root_idx, child)
306
+ return
307
+ end
297
308
  end
298
309
 
299
310
  element.children << child
@@ -456,95 +467,65 @@ module Moxml
456
467
  def has_declaration?(native_doc, _wrapper)
457
468
  decl = attachments.get(native_doc, :xml_declaration)
458
469
  if decl.nil? && !attachments.key?(native_doc, :xml_declaration)
459
- # No attachment entry - check native doc (for parsed documents)
460
- native_doc.respond_to?(:xml_declaration) && !native_doc.xml_declaration.nil?
470
+ native_doc.is_a?(::Oga::XML::Document) && !native_doc.xml_declaration.nil?
461
471
  else
462
472
  !decl.nil?
463
473
  end
464
474
  end
465
475
 
476
+ def remove_declaration(native_doc)
477
+ attachments.set(native_doc, :xml_declaration, nil)
478
+ end
479
+
466
480
  private
467
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
+
468
490
  def serialize_without_entity_processing(node, options = {})
469
- # Oga's XmlGenerator doesn't support options directly
470
- # We need to handle declaration options ourselves for Document nodes
471
491
  if node.is_a?(::Oga::XML::Document)
472
- # Check if we should include declaration
473
- # Priority: explicit option > existence of xml_declaration (native or attachment)
474
- effective_xml_declaration = node.xml_declaration || attachments.get(
475
- node, :xml_declaration
476
- )
492
+ effective_xml_declaration = attachments.get(node, :xml_declaration)
493
+
477
494
  should_include_decl = if options.key?(:no_declaration)
478
495
  !options[:no_declaration]
479
496
  elsif options.key?(:declaration)
480
497
  options[:declaration]
481
498
  else
482
- # Default: include if document has xml_declaration
483
- effective_xml_declaration ? true : false
499
+ effective_xml_declaration || node.xml_declaration ? true : false
484
500
  end
485
501
 
486
- # Fix: Check if declaration already exists in children
487
- # This prevents duplicate declarations when document already has one
488
- has_existing_declaration = node.children.any?(::Oga::XML::XmlDeclaration)
502
+ output = []
489
503
 
490
- if should_include_decl && !effective_xml_declaration && !has_existing_declaration
491
- # Need to add declaration - create default one
492
- output = []
493
- output << '<?xml version="1.0" encoding="UTF-8"?>'
504
+ if should_include_decl
505
+ decl = effective_xml_declaration || node.xml_declaration
506
+ output << if decl
507
+ declaration_to_xml(decl)
508
+ else
509
+ '<?xml version="1.0" encoding="UTF-8"?>'
510
+ end
494
511
  output << "\n"
495
-
496
- # Serialize doctype if present
497
- output << node.doctype.to_xml << "\n" if node.doctype
498
-
499
- # Serialize children
500
- node.children.each do |child|
501
- output << ::Moxml::Adapter::CustomizedOga::XmlGenerator.new(child).to_xml
502
- end
503
-
504
- return output.join
505
- elsif !should_include_decl
506
- # Skip xml_declaration
507
- output = []
508
-
509
- # Serialize doctype if present
510
- output << node.doctype.to_xml << "\n" if node.doctype
511
-
512
- # Serialize root and other children
513
- node.children.each do |child|
514
- next if child.is_a?(::Oga::XML::XmlDeclaration)
515
-
516
- output << ::Moxml::Adapter::CustomizedOga::XmlGenerator.new(child).to_xml
517
- end
518
-
519
- return output.join
520
512
  end
521
- end
522
513
 
523
- # Default: use XmlGenerator
524
- # But first check if we need to handle declaration specially
525
- effective_xml_declaration = node.is_a?(::Oga::XML::Document) && (node.xml_declaration || attachments.get(
526
- node, :xml_declaration
527
- ))
528
- if node.is_a?(::Oga::XML::Document) && effective_xml_declaration
529
- # Document has declaration - use custom handling to avoid duplicates
530
- output = []
531
- xml_declaration_serialized = false
514
+ if node.doctype
515
+ output << node.doctype.to_xml
516
+ output << "\n"
517
+ end
532
518
 
533
- # Serialize children, but skip XmlDeclaration if it would cause duplication
534
519
  node.children.each do |child|
535
- xml_declaration = child.is_a?(::Oga::XML::XmlDeclaration)
536
- next if xml_declaration && xml_declaration_serialized
537
-
538
- xml_declaration_serialized = true if xml_declaration
520
+ next if child.is_a?(::Oga::XML::XmlDeclaration)
539
521
 
540
522
  output << ::Moxml::Adapter::CustomizedOga::XmlGenerator.new(child).to_xml
541
523
  end
542
524
 
543
- output.join
544
- else
545
- # Normal case - use XmlGenerator directly
546
- ::Moxml::Adapter::CustomizedOga::XmlGenerator.new(node).to_xml
525
+ return output.join
547
526
  end
527
+
528
+ ::Moxml::Adapter::CustomizedOga::XmlGenerator.new(node).to_xml
548
529
  end
549
530
  end
550
531
  end