moxml 0.1.8 → 0.1.9

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +22 -39
  3. data/docs/_config.yml +3 -3
  4. data/docs/_guides/index.adoc +6 -0
  5. data/docs/_guides/modifying-xml.adoc +0 -1
  6. data/docs/_guides/parsing-xml.adoc +0 -1
  7. data/docs/_guides/xml-declaration.adoc +450 -0
  8. data/docs/_pages/adapter-compatibility.adoc +1 -1
  9. data/docs/_pages/adapters/headed-ox.adoc +9 -9
  10. data/docs/_pages/adapters/index.adoc +0 -1
  11. data/docs/_pages/adapters/libxml.adoc +1 -2
  12. data/docs/_pages/adapters/nokogiri.adoc +1 -2
  13. data/docs/_pages/adapters/oga.adoc +1 -2
  14. data/docs/_pages/adapters/ox.adoc +2 -1
  15. data/docs/_pages/adapters/rexml.adoc +1 -2
  16. data/docs/_pages/best-practices.adoc +0 -1
  17. data/docs/_pages/compatibility.adoc +0 -1
  18. data/docs/_pages/configuration.adoc +0 -1
  19. data/docs/_pages/error-handling.adoc +0 -1
  20. data/docs/_pages/headed-ox-limitations.adoc +16 -0
  21. data/docs/_pages/installation.adoc +0 -1
  22. data/docs/_pages/node-api-reference.adoc +0 -1
  23. data/docs/_pages/performance.adoc +0 -1
  24. data/docs/_pages/quick-start.adoc +0 -1
  25. data/docs/_pages/thread-safety.adoc +0 -1
  26. data/docs/_references/document-api.adoc +0 -1
  27. data/docs/_tutorials/basic-usage.adoc +0 -1
  28. data/docs/_tutorials/builder-pattern.adoc +0 -1
  29. data/docs/_tutorials/namespace-handling.adoc +0 -1
  30. data/docs/_tutorials/xpath-queries.adoc +0 -1
  31. data/lib/moxml/adapter/customized_rexml/formatter.rb +2 -2
  32. data/lib/moxml/adapter/libxml.rb +19 -3
  33. data/lib/moxml/adapter/nokogiri.rb +37 -2
  34. data/lib/moxml/adapter/oga.rb +67 -3
  35. data/lib/moxml/adapter/ox.rb +45 -7
  36. data/lib/moxml/adapter/rexml.rb +32 -10
  37. data/lib/moxml/context.rb +18 -1
  38. data/lib/moxml/declaration.rb +9 -0
  39. data/lib/moxml/document.rb +14 -0
  40. data/lib/moxml/document_builder.rb +7 -0
  41. data/lib/moxml/node.rb +61 -1
  42. data/lib/moxml/version.rb +1 -1
  43. data/lib/moxml/xpath/compiler.rb +2 -0
  44. data/spec/integration/shared_examples/node_wrappers/declaration_behavior.rb +0 -3
  45. data/spec/moxml/declaration_preservation_spec.rb +217 -0
  46. data/spec/performance/memory_usage_spec.rb +3 -2
  47. metadata +3 -1
@@ -1,12 +1,12 @@
1
1
  ---
2
- title: HeadedOx adapter
2
+ title: HeadedOx
3
3
  parent: Adapters
4
4
  nav_order: 6
5
5
  ---
6
6
 
7
- === HeadedOx adapter
7
+ == HeadedOx adapter
8
8
 
9
- ==== General
9
+ === General
10
10
 
11
11
  The HeadedOx adapter combines Ox's fast C-based XML parsing with Moxml's
12
12
  comprehensive pure Ruby XPath 1.0 engine.
@@ -39,7 +39,7 @@ cheap = doc.xpath('//book[@price <= sum(//book/@price) div count(//book)]')
39
39
  IMPORTANT: For complete XPath 1.0 specification with zero limitations today, use
40
40
  Nokogiri or Oga adapters.
41
41
 
42
- ==== Features
42
+ === Features
43
43
 
44
44
  * Fast XML parsing (Ox C extension) - Same speed as standard Ox
45
45
  * 6 of 13 XPath axes (46% - covers 80% of common usage patterns)
@@ -48,7 +48,7 @@ Nokogiri or Oga adapters.
48
48
  * Expression compilation and caching (1000-entry LRU cache)
49
49
  * Document construction and serialization through Ox
50
50
 
51
- ==== Architecture
51
+ === Architecture
52
52
 
53
53
  HeadedOx is a **hybrid adapter** that layers Moxml's pure Ruby XPath engine on
54
54
  top of Ox's fast C parser:
@@ -78,7 +78,7 @@ top of Ox's fast C parser:
78
78
  └─────────────────────────┘
79
79
  ----
80
80
 
81
- ==== Known limitations
81
+ === Known limitations
82
82
 
83
83
  The following 16 test failures represent architectural boundaries in the Ox gem,
84
84
  not bugs in HeadedOx:
@@ -100,7 +100,7 @@ See link:docs/HEADED_OX_LIMITATIONS.md[HEADED_OX_LIMITATIONS.md] for:
100
100
  * When to use HeadedOx vs other adapters decision guide
101
101
  * Future roadmap if Ox adds namespace introspection API
102
102
 
103
- ==== When to Use HeadedOx
103
+ === When to Use HeadedOx
104
104
 
105
105
  You can use HeadedOx instead of Ox for all XML parsing needs, except when
106
106
  certain advanced XPath features are required.
@@ -126,7 +126,7 @@ link:docs/headed-ox.adoc[HeadedOx Implementation Guide] and
126
126
  link:docs/HEADED_OX_LIMITATIONS.md[HeadedOx Limitations Documentation].
127
127
 
128
128
 
129
- ==== XPath capabilities
129
+ === XPath capabilities
130
130
 
131
131
  [cols="1,1,4"]
132
132
  |===
@@ -169,7 +169,7 @@ operator predicates, complex nested expressions
169
169
  |===
170
170
 
171
171
 
172
- ==== What XPath queries work in HeadedOx
172
+ === What XPath queries work in HeadedOx
173
173
 
174
174
  NOTE: This table is of v0.2.0.
175
175
 
@@ -1,6 +1,5 @@
1
1
  ---
2
2
  title: Adapters
3
- parent: Overview
4
3
  nav_order: 5
5
4
  has_children: true
6
5
  ---
@@ -1,7 +1,6 @@
1
1
  ---
2
- title: LibXML adapter
2
+ title: LibXML
3
3
  parent: Adapters
4
- grand_parent: Overview
5
4
  nav_order: 2
6
5
  ---
7
6
 
@@ -1,7 +1,6 @@
1
1
  ---
2
- title: Nokogiri adapter
2
+ title: Nokogiri
3
3
  parent: Adapters
4
- grand_parent: Overview
5
4
  nav_order: 1
6
5
  ---
7
6
 
@@ -1,7 +1,6 @@
1
1
  ---
2
- title: Oga adapter
2
+ title: Oga
3
3
  parent: Adapters
4
- grand_parent: Overview
5
4
  nav_order: 3
6
5
  ---
7
6
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- title: Ox adapter
2
+ title: Ox
3
3
  parent: Adapters
4
4
  nav_order: 5
5
5
  ---
@@ -11,6 +11,7 @@ nav_order: 5
11
11
  Ox is the fastest XML parser available for Ruby, providing excellent performance for simple to moderately complex XML documents.
12
12
 
13
13
  **Best for:**
14
+
14
15
  * Maximum parsing speed
15
16
  * Simple document structures
16
17
  * Memory-constrained environments
@@ -1,7 +1,6 @@
1
1
  ---
2
- title: REXML adapter
2
+ title: REXML
3
3
  parent: Adapters
4
- grand_parent: Overview
5
4
  nav_order: 4
6
5
  ---
7
6
 
@@ -1,6 +1,5 @@
1
1
  ---
2
2
  title: Best practices
3
- parent: Overview
4
3
  nav_order: 9
5
4
  ---
6
5
 
@@ -1,6 +1,5 @@
1
1
  ---
2
2
  title: Compatibility
3
- parent: Overview
4
3
  nav_order: 6
5
4
  ---
6
5
 
@@ -1,6 +1,5 @@
1
1
  ---
2
2
  title: Configuration
3
- parent: Overview
4
3
  nav_order: 7
5
4
  ---
6
5
 
@@ -1,6 +1,5 @@
1
1
  ---
2
2
  title: Error handling
3
- parent: Overview
4
3
  nav_order: 8
5
4
  ---
6
5
 
@@ -77,6 +77,7 @@ all_attrs = books.flat_map { |book| book.attributes.values }
77
77
  ----
78
78
 
79
79
  **Test failures:**
80
+
80
81
  * `spec/moxml/xpath/compiler_spec.rb:189` - Attribute axis wildcards
81
82
  * `spec/moxml/xpath/axes_spec.rb:220` - Attribute + predicate combinations
82
83
 
@@ -85,6 +86,7 @@ all_attrs = books.flat_map { |book| book.attributes.values }
85
86
  **Status:** Not implemented in HeadedOx adapter
86
87
 
87
88
  **What's missing:**
89
+
88
90
  * `adapter.namespace(node)` - Get primary namespace of element
89
91
  * `adapter.namespace_definitions(node)` - Get all namespace definitions
90
92
  * `node.namespace` - Access element's namespace
@@ -114,6 +116,7 @@ end
114
116
  None. These operations require Ox enhancements.
115
117
 
116
118
  **Test failures:**
119
+
117
120
  * `spec/integration/shared_examples/edge_cases.rb:102` - Default namespace changes
118
121
  * `spec/integration/shared_examples/edge_cases.rb:120` - Recursive namespace definitions
119
122
  * `spec/integration/shared_examples/integration_workflows.rb:98` - Complex namespace scenarios
@@ -148,6 +151,7 @@ value = attr&.value
148
151
  ----
149
152
 
150
153
  **Test failures:**
154
+
151
155
  * `spec/integration/shared_examples/edge_cases.rb:134` - Attributes with same local name
152
156
 
153
157
  === 4. Parent Node Setter
@@ -189,6 +193,7 @@ new_parent.add_child(node) # Add to new parent
189
193
  **Note:** This workaround is used internally where needed, but the getter/setter syntax is not supported.
190
194
 
191
195
  **Test failures:**
196
+
192
197
  * `spec/integration/shared_examples/integration_workflows.rb:122` - Complex modifications
193
198
 
194
199
  === 5. CDATA End Marker Escaping
@@ -224,6 +229,7 @@ doc.create_cdata(safe_content)
224
229
  ----
225
230
 
226
231
  **Test failures:**
232
+
227
233
  * `spec/integration/shared_examples/edge_cases.rb:41` - CDATA nested markers
228
234
  * `spec/integration/shared_examples/node_wrappers/cdata_behavior.rb:44` - CDATA escaping
229
235
 
@@ -263,6 +269,7 @@ second_title = titles[1].text # Works correctly
263
269
  ----
264
270
 
265
271
  **Test failures:**
272
+
266
273
  * `spec/moxml/adapter/headed_ox_spec.rb:77` - String functions in predicates
267
274
  * `spec/moxml/adapter/headed_ox_spec.rb:84` - Position functions
268
275
  * `spec/moxml/adapter/headed_ox_spec.rb:304` - last() function
@@ -277,6 +284,7 @@ second_title = titles[1].text # Works correctly
277
284
  **Why it fails:**
278
285
 
279
286
  When using `//*` to select all elements, HeadedOx returns 6 elements while Nokogiri returns 7+. This is likely due to differences in:
287
+
280
288
  * Document node counting
281
289
  * Text node inclusion/exclusion
282
290
  * Ox's internal DOM structure
@@ -297,6 +305,7 @@ result = doc.xpath("//*")
297
305
  Use specific element names instead of wildcards.
298
306
 
299
307
  **Test failures:**
308
+
300
309
  * `spec/moxml/xpath/compiler_spec.rb:160` - Descendant-or-self wildcards
301
310
 
302
311
  === 8. Namespace-Aware XPath with Predicates
@@ -340,6 +349,7 @@ result = items.select { |item| item['id'] == '123' }
340
349
  ----
341
350
 
342
351
  **Test failures:**
352
+
343
353
  * `spec/integration/shared_examples/integration_workflows.rb:69` - XPath queries
344
354
 
345
355
  == Ox Enhancement Requirements
@@ -477,6 +487,7 @@ Ensure element counting matches other parsers' conventions when using wildcard s
477
487
  === If Ox Adds Namespace API (v1.3)
478
488
 
479
489
  With namespace methods (`namespace()`, `namespace_definitions()`):
490
+
480
491
  * **Target:** 99.5% pass rate
481
492
  * **Adds:** 4 more passing tests
482
493
  * **Still limited:** Parent setter, CDATA escaping, attribute wildcards
@@ -484,6 +495,7 @@ With namespace methods (`namespace()`, `namespace_definitions()`):
484
495
  === If Ox Adds Reparenting API (v1.4)
485
496
 
486
497
  With `reparent(new_parent)` method:
498
+
487
499
  * **Target:** 99.6% pass rate
488
500
  * **Adds:** 1 more passing test
489
501
  * **Still limited:** CDATA escaping, attribute wildcards
@@ -491,6 +503,7 @@ With `reparent(new_parent)` method:
491
503
  === If Ox Fixes CDATA Escaping (v1.5)
492
504
 
493
505
  With proper `]]>` handling:
506
+
494
507
  * **Target:** 99.7% pass rate
495
508
  * **Adds:** 2 more passing tests
496
509
  * **Still limited:** Attribute wildcards
@@ -498,6 +511,7 @@ With proper `]]>` handling:
498
511
  === Full Feature Parity (v2.0)
499
512
 
500
513
  Would require:
514
+
501
515
  * All Ox enhancements above
502
516
  * XPath parser support for `@*` wildcard
503
517
  * Investigation and fixes for text content access
@@ -546,11 +560,13 @@ Total passing: **1,992 / 2,008** (99.20%)
546
560
  HeadedOx v1.2 successfully delivers on its core promise: **fast XML parsing with comprehensive XPath support**. The 99.20% pass rate demonstrates excellent compatibility with Moxml's test suite, with the 0.80% of failures representing clear architectural boundaries in the Ox gem rather than bugs in HeadedOx.
547
561
 
548
562
  **Use HeadedOx when:**
563
+
549
564
  - Speed + XPath coverage matter most
550
565
  - Basic namespace queries are sufficient
551
566
  - DOM is mostly read-only
552
567
 
553
568
  **Use Nokogiri/Oga when:**
569
+
554
570
  - Need full namespace API
555
571
  - Heavy DOM modifications required
556
572
  - 100% feature parity is critical
@@ -1,6 +1,5 @@
1
1
  ---
2
2
  title: Installation
3
- parent: Overview
4
3
  nav_order: 2
5
4
  ---
6
5
 
@@ -1,6 +1,5 @@
1
1
  ---
2
2
  title: Node API reference
3
- parent: Overview
4
3
  nav_order: 5
5
4
  ---
6
5
 
@@ -1,6 +1,5 @@
1
1
  ---
2
2
  title: Performance considerations
3
- parent: Overview
4
3
  nav_order: 11
5
4
  ---
6
5
 
@@ -1,6 +1,5 @@
1
1
  ---
2
2
  title: Quick start
3
- parent: Overview
4
3
  nav_order: 3
5
4
  ---
6
5
 
@@ -1,6 +1,5 @@
1
1
  ---
2
2
  title: Thread safety
3
- parent: Overview
4
3
  nav_order: 10
5
4
  ---
6
5
 
@@ -1,6 +1,5 @@
1
1
  ---
2
2
  title: Document API
3
- parent: Overview
4
3
  nav_order: 2
5
4
  ---
6
5
 
@@ -1,6 +1,5 @@
1
1
  ---
2
2
  title: Basic usage
3
- parent: Overview
4
3
  nav_order: 2
5
4
  ---
6
5
 
@@ -1,6 +1,5 @@
1
1
  ---
2
2
  title: Builder pattern
3
- parent: Overview
4
3
  nav_order: 5
5
4
  ---
6
5
 
@@ -1,6 +1,5 @@
1
1
  ---
2
2
  title: Namespace handling
3
- parent: Overview
4
3
  nav_order: 4
5
4
  ---
6
5
 
@@ -1,6 +1,5 @@
1
1
  ---
2
2
  title: XPath queries
3
- parent: Overview
4
3
  nav_order: 3
5
4
  ---
6
5
 
@@ -166,7 +166,7 @@ module Moxml
166
166
  end
167
167
 
168
168
  # Then write regular attributes
169
- node.attributes.each do |name, attr|
169
+ node.attributes.each do |name, attr| # rubocop:disable Style/CombinableLoops
170
170
  next if name.to_s.start_with?("xmlns:") || name.to_s == "xmlns"
171
171
 
172
172
  output << " "
@@ -180,7 +180,7 @@ module Moxml
180
180
  value = attr.respond_to?(:value) ? attr.value : attr
181
181
  output << escape_attribute_value(value.to_s)
182
182
  output << "\""
183
- end
183
+ end # rubocop:enable Style/CombinableLoops
184
184
  end
185
185
 
186
186
  def escape_attribute_value(value)
@@ -332,7 +332,13 @@ module Moxml
332
332
 
333
333
  def document(node)
334
334
  native_node = unpatch_node(node)
335
- native_node&.doc
335
+ return nil unless native_node
336
+
337
+ # Handle documents themselves
338
+ return native_node if native_node.is_a?(::LibXML::XML::Document)
339
+
340
+ # For other nodes, return their document
341
+ native_node.doc
336
342
  end
337
343
 
338
344
  def root(document)
@@ -831,7 +837,16 @@ module Moxml
831
837
  if native_node.is_a?(::LibXML::XML::Document)
832
838
  output = +""
833
839
 
834
- unless options[:no_declaration]
840
+ # Check if we should include declaration
841
+ # Priority: explicit no_declaration option > default (include)
842
+ should_include_decl = if options.key?(:no_declaration)
843
+ !options[:no_declaration]
844
+ else
845
+ # Default: include declaration
846
+ true
847
+ end
848
+
849
+ if should_include_decl
835
850
  # Check if declaration was explicitly managed
836
851
  if native_node.instance_variable_defined?(:@moxml_declaration)
837
852
  decl = native_node.instance_variable_get(:@moxml_declaration)
@@ -1301,7 +1316,7 @@ module Moxml
1301
1316
  # - On child elements, output namespace definitions that override parent namespaces
1302
1317
  if elem.respond_to?(:namespaces) && elem.namespaces.respond_to?(:definitions)
1303
1318
  # Get parent's namespace definitions to detect overrides
1304
- parent_ns_defs = if !include_ns && elem.respond_to?(:parent) && elem.parent
1319
+ parent_ns_defs = if !include_ns && elem.respond_to?(:parent) && elem.parent && !elem.parent.is_a?(::LibXML::XML::Document)
1305
1320
  parent_namespaces = {}
1306
1321
  if elem.parent.respond_to?(:namespaces)
1307
1322
  elem.parent.namespaces.each do |ns|
@@ -1444,6 +1459,7 @@ module Moxml
1444
1459
  node.each_child do |child|
1445
1460
  collect_ns_from_subtree(child, ns_defs) if child.element?
1446
1461
  end
1462
+ ns_defs
1447
1463
  end
1448
1464
 
1449
1465
  def build_xpath_namespaces(node, user_namespaces)
@@ -221,6 +221,23 @@ module Moxml
221
221
  end
222
222
 
223
223
  def add_child(element, child)
224
+ # Special handling for declarations on Nokogiri documents
225
+ if element.is_a?(::Nokogiri::XML::Document) &&
226
+ child.is_a?(::Nokogiri::XML::ProcessingInstruction) &&
227
+ child.name == "xml"
228
+ # Set document's xml_decl property
229
+ version = declaration_attribute(child, "version") || "1.0"
230
+ encoding = declaration_attribute(child, "encoding")
231
+ standalone = declaration_attribute(child, "standalone")
232
+
233
+ # Nokogiri's xml_decl can only be set via instance variable
234
+ element.instance_variable_set(:@xml_decl, {
235
+ version: version,
236
+ encoding: encoding,
237
+ standalone: standalone,
238
+ }.compact)
239
+ end
240
+
224
241
  if node_type(child) == :doctype
225
242
  # avoid exceptions: cannot reparent Nokogiri::XML::DTD there
226
243
  element.create_internal_subset(
@@ -240,6 +257,14 @@ module Moxml
240
257
  end
241
258
 
242
259
  def remove(node)
260
+ # Special handling for declarations on Nokogiri documents
261
+ if node.is_a?(::Nokogiri::XML::ProcessingInstruction) &&
262
+ node.name == "xml" &&
263
+ node.parent.is_a?(::Nokogiri::XML::Document)
264
+ # Clear document's xml_decl when removing declaration
265
+ node.parent.instance_variable_set(:@xml_decl, nil)
266
+ end
267
+
243
268
  node.remove
244
269
  end
245
270
 
@@ -328,8 +353,18 @@ module Moxml
328
353
  if options[:indent].to_i.positive?
329
354
  save_options |= ::Nokogiri::XML::Node::SaveOptions::FORMAT
330
355
  end
331
- if options[:no_declaration]
332
- save_options |= ::Nokogiri::XML::Node::SaveOptions::NO_DECLARATION
356
+
357
+ # Handle declaration option
358
+ # Priority:
359
+ # 1. Explicit no_declaration option
360
+ # 2. Check Nokogiri's internal @xml_decl (when remove is called, this becomes nil)
361
+ if options.key?(:no_declaration)
362
+ save_options |= ::Nokogiri::XML::Node::SaveOptions::NO_DECLARATION if options[:no_declaration]
363
+ elsif node.respond_to?(:instance_variable_get) &&
364
+ node.instance_variable_defined?(:@xml_decl)
365
+ # Nokogiri's internal state - if nil, declaration was removed
366
+ xml_decl = node.instance_variable_get(:@xml_decl)
367
+ save_options |= ::Nokogiri::XML::Node::SaveOptions::NO_DECLARATION if xml_decl.nil?
333
368
  end
334
369
 
335
370
  node.to_xml(
@@ -10,7 +10,10 @@ module Moxml
10
10
  class Oga < Base
11
11
  class << self
12
12
  def set_root(doc, element)
13
- doc.children.clear # Clear any existing children
13
+ # Clear existing root element if any - Oga's NodeSet needs special handling
14
+ # We need to manually remove elements since NodeSet doesn't support clear or delete_if
15
+ elements_to_remove = doc.children.select { |child| child.is_a?(::Oga::XML::Element) }
16
+ elements_to_remove.each { |elem| doc.children.delete(elem) }
14
17
  doc.children << element
15
18
  end
16
19
 
@@ -247,6 +250,13 @@ module Moxml
247
250
  child_or_text
248
251
  end
249
252
 
253
+ # Special handling for declarations on Oga documents
254
+ if element.is_a?(::Oga::XML::Document) &&
255
+ child.is_a?(::Oga::XML::XmlDeclaration)
256
+ # Set as document's xml_declaration
257
+ element.instance_variable_set(:@xml_declaration, child)
258
+ end
259
+
250
260
  element.children << child
251
261
  end
252
262
 
@@ -273,6 +283,13 @@ module Moxml
273
283
  end
274
284
 
275
285
  def remove(node)
286
+ # Special handling for declarations on Oga documents
287
+ if node.is_a?(::Oga::XML::XmlDeclaration) &&
288
+ node.parent.is_a?(::Oga::XML::Document)
289
+ # Clear document's xml_declaration when removing declaration
290
+ node.parent.instance_variable_set(:@xml_declaration, nil)
291
+ end
292
+
276
293
  node.remove
277
294
  end
278
295
 
@@ -371,8 +388,55 @@ module Moxml
371
388
  )
372
389
  end
373
390
 
374
- def serialize(node, _options = {})
375
- # Expand empty tags, encode attributes, etc
391
+ def serialize(node, options = {})
392
+ # Oga's XmlGenerator doesn't support options directly
393
+ # We need to handle declaration options ourselves for Document nodes
394
+ if node.is_a?(::Oga::XML::Document)
395
+ # Check if we should include declaration
396
+ # Priority: explicit option > existence of xml_declaration node
397
+ should_include_decl = if options.key?(:no_declaration)
398
+ !options[:no_declaration]
399
+ elsif options.key?(:declaration)
400
+ options[:declaration]
401
+ else
402
+ # Default: include if document has xml_declaration node
403
+ node.xml_declaration ? true : false
404
+ end
405
+
406
+ if should_include_decl && !node.xml_declaration
407
+ # Need to add declaration - create default one
408
+ output = +""
409
+ output << '<?xml version="1.0" encoding="UTF-8"?>'
410
+ output << "\n"
411
+
412
+ # Serialize doctype if present
413
+ output << node.doctype.to_xml << "\n" if node.doctype
414
+
415
+ # Serialize children
416
+ node.children.each do |child|
417
+ output << ::Moxml::Adapter::CustomizedOga::XmlGenerator.new(child).to_xml
418
+ end
419
+
420
+ return output
421
+ elsif !should_include_decl
422
+ # Skip xml_declaration
423
+ output = +""
424
+
425
+ # Serialize doctype if present
426
+ output << node.doctype.to_xml << "\n" if node.doctype
427
+
428
+ # Serialize root and other children
429
+ node.children.each do |child|
430
+ next if child.is_a?(::Oga::XML::XmlDeclaration)
431
+
432
+ output << ::Moxml::Adapter::CustomizedOga::XmlGenerator.new(child).to_xml
433
+ end
434
+
435
+ return output
436
+ end
437
+ end
438
+
439
+ # Default: use XmlGenerator
376
440
  ::Moxml::Adapter::CustomizedOga::XmlGenerator.new(node).to_xml
377
441
  end
378
442
  end
@@ -348,6 +348,24 @@ module Moxml
348
348
  end
349
349
 
350
350
  def add_child(element, child)
351
+ # Special handling for declarations on Ox documents
352
+ if element.is_a?(::Ox::Document) && child.is_a?(::Ox::Instruct) && child.target == "xml"
353
+ # Transfer declaration attributes to document
354
+ element.attributes ||= {}
355
+ if child.attributes["version"]
356
+ element.attributes[:version] =
357
+ child.attributes["version"]
358
+ end
359
+ if child.attributes["encoding"]
360
+ element.attributes[:encoding] =
361
+ child.attributes["encoding"]
362
+ end
363
+ if child.attributes["standalone"]
364
+ element.attributes[:standalone] =
365
+ child.attributes["standalone"]
366
+ end
367
+ end
368
+
351
369
  child.parent = element if child.respond_to?(:parent)
352
370
  element.nodes ||= []
353
371
  element.nodes << child
@@ -380,6 +398,15 @@ module Moxml
380
398
 
381
399
  return unless parent(node)
382
400
 
401
+ # Special handling for declarations on Ox documents
402
+ if parent(node).is_a?(::Ox::Document) && node.is_a?(::Ox::Instruct) && node.target == "xml"
403
+ # Clear declaration attributes from document
404
+ doc = parent(node)
405
+ doc.attributes&.delete(:version)
406
+ doc.attributes&.delete(:encoding)
407
+ doc.attributes&.delete(:standalone)
408
+ end
409
+
383
410
  parent(node).nodes.delete(unpatch_node(node))
384
411
  end
385
412
 
@@ -524,13 +551,24 @@ module Moxml
524
551
  def serialize(node, options = {})
525
552
  output = ""
526
553
  if node.is_a?(::Ox::Document)
527
- # add declaration
528
- version = node[:version] || "1.0"
529
- encoding = options[:encoding] || node[:encoding]
530
- standalone = node[:standalone]
531
-
532
- decl = create_native_declaration(version, encoding, standalone)
533
- output = ::Ox.dump(::Ox::Document.new << decl).strip
554
+ # Check if we should include declaration
555
+ # Priority: explicit option > document attributes
556
+ should_include_decl = if options.key?(:no_declaration)
557
+ !options[:no_declaration]
558
+ else
559
+ # Check if document has declaration attributes
560
+ node[:version] || node[:encoding] || node[:standalone]
561
+ end
562
+
563
+ # Only add declaration if should_include_decl is true
564
+ if should_include_decl
565
+ version = node[:version] || "1.0"
566
+ encoding = options[:encoding] || node[:encoding]
567
+ standalone = node[:standalone]
568
+
569
+ decl = create_native_declaration(version, encoding, standalone)
570
+ output = ::Ox.dump(::Ox::Document.new << decl).strip
571
+ end
534
572
  end
535
573
 
536
574
  ox_options = {