Class: Lutaml::Xml::Adapter::NokogiriAdapter
- Inherits:
-
BaseAdapter
- Object
- Document
- BaseAdapter
- Lutaml::Xml::Adapter::NokogiriAdapter
- Extended by:
- AdapterHelpers, DocTypeExtractor
- Defined in:
- lib/lutaml/xml/adapter/nokogiri_adapter.rb
Constant Summary collapse
- TEXT_CLASSES =
[Moxml::Text, Moxml::Cdata].freeze
Instance Attribute Summary
Attributes inherited from Document
#doctype, #encoding, #parsed_doc, #register, #root, #xml_declaration
Class Method Summary collapse
- .order_of(element) ⇒ Object
- .parse(xml, options = {}) ⇒ Object
-
.text_of(element) ⇒ Object
NOTE: name_of, prefixed_name_of, namespaced_attr_name, namespaced_name_of are provided by AdapterHelpers module via extend.
Instance Method Summary collapse
- #attributes_hash(element) ⇒ Object
-
#build_element_value_with_plan(xml, element_rule, value, attribute_def, plan:, mapping: nil, options: {}) ⇒ Object
Build simple element value with plan.
-
#build_element_with_plan(xml, element, plan, options = {}) ⇒ Object
Build element using prepared namespace declaration plan.
-
#build_nested_element_with_plan(xml, value, element_rule, attribute_def, plan, options, parent_plan: nil) ⇒ Object
NOTE: build_unordered_children_with_plan and build_ordered_element_with_plan are inherited from BaseAdapter - no need to override.
-
#build_xml_element(xml, element, parent_uses_default_ns: false, parent_element_form_default: nil, parent_namespace_class: nil) ⇒ Object
Build XML from XmlDataModel::XmlElement structure.
-
#build_xml_element_with_plan(xml, xml_element, plan, options = {}) ⇒ Object
Build XML from XmlDataModel::XmlElement using DeclarationPlan tree (PARALLEL TRAVERSAL).
- #order ⇒ Object
- #to_xml(options = {}) ⇒ Object
Methods included from DocTypeExtractor
Methods included from AdapterHelpers
build_element_from_child, name_of, namespace_uri_hoisted?, namespaced_attr_name, namespaced_name_of, node_type_of, prefix_for_namespace_uri, prefixed_name_of, text_node?
Methods inherited from BaseAdapter
#add_value, #attribute_definition_for, #attribute_value_for, #build_ordered_element_with_plan, #build_unordered_children_with_plan, #child_plan_for, #determine_encoding, extract_document_processing_instructions, fpi?, fpi_to_urn, name_of, namespaced_attr_name, namespaced_name, namespaced_name_of, #ordered?, prefixed_name_of, #process_content_mapping, #render_element?
Methods included from PolymorphicValueHandler
Methods included from DeclarationHandler
extract_attribute, extract_xml_declaration, #generate_declaration, #generate_doctype_declaration, #should_include_declaration?
Methods inherited from Document
#add_value, #attribute_definition_for, #attribute_value_for, #attributes, #cdata, #children, #declaration, #doctype_declaration, #element_children, #element_children_index, encoding, #initialize, name_of, namespaced_name_of, #ordered?, #parse_element, #process_content_mapping, #render_element?, #text, #to_h, type
Constructor Details
This class inherits a constructor from Lutaml::Xml::Document
Class Method Details
.order_of(element) ⇒ Object
203 204 205 206 207 208 209 210 |
# File 'lib/lutaml/xml/adapter/nokogiri_adapter.rb', line 203 def self.order_of(element) element.children.each do |node| if node.is_a?(Moxml::ProcessingInstruction) return [Element.new("ProcessingInstruction", node.name)] end end super end |
.parse(xml, options = {}) ⇒ Object
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
# File 'lib/lutaml/xml/adapter/nokogiri_adapter.rb', line 14 def self.parse(xml, = {}) enc = encoding(xml, ) parsed = Moxml::Adapter::Nokogiri.parse(xml, encoding: enc) root_element = parsed.root # Validate that we have a root element if root_element.nil? raise Lutaml::Model::InvalidFormatError.new( :xml, "Document has no root element. " \ "The XML may be empty, contain only whitespace, " \ "or consist only of an XML declaration.", ) end # Extract DOCTYPE information from raw XML # (Moxml doesn't directly expose DOCTYPE) doctype_info = extract_doctype_from_xml(xml) # Extract XML declaration for preservation xml_decl_info = DeclarationHandler.extract_xml_declaration(xml) @root = NokogiriElement.new(root_element) @root.processing_instructions = extract_document_processing_instructions(parsed) new(@root, enc, doctype: doctype_info, xml_declaration: xml_decl_info) end |
.text_of(element) ⇒ Object
NOTE: name_of, prefixed_name_of, namespaced_attr_name, namespaced_name_of are provided by AdapterHelpers module via extend
187 188 189 |
# File 'lib/lutaml/xml/adapter/nokogiri_adapter.rb', line 187 def self.text_of(element) element.text end |
Instance Method Details
#attributes_hash(element) ⇒ Object
166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 |
# File 'lib/lutaml/xml/adapter/nokogiri_adapter.rb', line 166 def attributes_hash(element) result = Lutaml::Model::MappingHash.new element.attributes_each_value do |attr| if attr.name == "schemaLocation" result["__schema_location"] = { namespace: attr.namespace, prefix: attr.namespace.prefix, schema_location: attr.value, } else result[self.class.namespaced_attr_name(attr)] = attr.value end end result end |
#build_element_value_with_plan(xml, element_rule, value, attribute_def, plan:, mapping: nil, options: {}) ⇒ Object
Build simple element value with plan
484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 |
# File 'lib/lutaml/xml/adapter/nokogiri_adapter.rb', line 484 def build_element_value_with_plan(xml, element_rule, value, attribute_def, plan:, mapping: nil, options: {}) # Handle array values by creating multiple elements if value.is_a?(Array) value.each do |val| build_element_value_with_plan(xml, element_rule, val, attribute_def, plan: plan, mapping: mapping, options: ) end return end return unless render_element?(element_rule, [:element], value) # Get namespace info for this element mapping_local = mapping || [:mapper_class]&.mappings_for(:xml) ns_info = if mapping_local # Try to resolve namespace using local mapping begin NamespaceResolver.new(@register).resolve_for_element( element_rule, attribute_def, mapping_local, plan, ) rescue StandardError # Fallback to default behavior { prefix: nil, ns_info: nil } end else { prefix: nil, ns_info: nil } end prefix = ns_info[:prefix] # Get child's plan if available child_plan = plan&.child_plan(element_rule.to) if value.is_a?(Lutaml::Model::Serialize) # Nested Serialize object child_mapper_class = value.class child_mapper_class.mappings_for(:xml) # Performance: Use dup with direct assignment to avoid merge allocations if [:mapper_class] == child_mapper_class = else = .dup [:mapper_class] = child_mapper_class end if child_plan build_element_with_plan(xml, value, child_plan, ) else build_element(xml, value, ) end elsif value.nil? && element_rule.render_nil? # Render nil value element_name = element_rule.multiple_mappings? ? element_rule.name.first : element_rule.name xml.create_and_add_element(element_name, prefix: prefix) do |inner_xml| inner_xml.text("") end elsif value # Simple string value element_name = element_rule.multiple_mappings? ? element_rule.name.first : element_rule.name xml.create_and_add_element(element_name, prefix: prefix) do |inner_xml| if element_rule.cdata inner_xml.cdata(value.to_s) else inner_xml.text(value.to_s) end end end end |
#build_element_with_plan(xml, element, plan, options = {}) ⇒ Object
Build element using prepared namespace declaration plan
218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 |
# File 'lib/lutaml/xml/adapter/nokogiri_adapter.rb', line 218 def build_element_with_plan(xml, element, plan, = {}) # Provide default empty plan if nil (e.g., for custom methods) plan ||= DeclarationPlan.empty mapper_class = [:mapper_class] || element.class # New: Handle simple types that don't have mappings unless mapper_class.is_a?(Class) && mapper_class.include?(Lutaml::Model::Serialize) tag_name = [:tag_name] || "element" xml.create_and_add_element(tag_name) do |inner_xml| inner_xml.text(element.to_s) end return xml end mapping = mapper_class.mappings_for(:xml) return xml unless mapping # TYPE-ONLY MODELS: No element wrapper, serialize children directly # BUT if we have a tag_name in options, that means parent wants a wrapper if mapping.namespace_class # Check if this element's namespace is explicitly :blank # This happens when the model uses 'namespace :blank' in its xml block # We can detect this through the plan - but since we're inside build_element_with_plan, # we need to check the mapping directly # Actually, the element itself won't have explicit_blank in its namespace resolution # because it's the element's OWN namespace. We need to skip this for the element itself. # The xmlns="" handling is for CHILD elements, not the parent element. # So this section is actually not needed here - it's needed in add_simple_value # But it reads: # @mapping.namespace_class # element.ns_info_for(repository_name, mapping.xml_namespace) end # Use xmlns declarations from plan attributes = {} attributes.merge!(NamespaceDeclarationBuilder.build_xmlns_attributes(plan)) # Collect attribute custom methods to call after element creation attribute_custom_methods = [] # Add regular attributes (non-xmlns) mapping.attributes.each do |attribute_rule| next if [:except]&.include?(attribute_rule.to) # Collect custom methods for later execution (after element is created) if attribute_rule.custom_methods[:to] attribute_custom_methods << attribute_rule next end mapping_rule_name = if attribute_rule.multiple_mappings? attribute_rule.name.first else attribute_rule.name end attr = attribute_definition_for(element, attribute_rule, mapper_class: mapper_class) value = attribute_rule.to_value_for(element) # Handle as_list and delimiter BEFORE serialization for array values # These features convert arrays to delimited strings before serialization if value.is_a?(Array) if attribute_rule.as_list && attribute_rule.as_list[:export] value = attribute_rule.as_list[:export].call(value) elsif attribute_rule.delimiter value = value.join(attribute_rule.delimiter) end end value = attr.serialize(value, :xml, @register) if attr value = ExportTransformer.call(value, attribute_rule, attr, format: :xml) if render_element?(attribute_rule, element, value) # Resolve attribute namespace using extracted module ns_info = AttributeNamespaceResolver.resolve( rule: attribute_rule, attribute: attr, plan: plan, mapper_class: mapper_class, register: @register, ) # Build qualified attribute name based on W3C semantics attr_name = AttributeNamespaceResolver.build_qualified_name( ns_info, mapping_rule_name, attribute_rule, ) attributes[attr_name] = value ? value.to_s : value # Add local xmlns declaration if needed if ns_info[:needs_local_declaration] attributes[ns_info[:local_xmlns_attr]] = ns_info[:local_xmlns_uri] end end end # Add schema_location attribute from ElementNode if present # This is for the plan-based path where schema_location_attr is computed during planning attributes.merge!(plan.root_node.schema_location_attr) if plan&.root_node&.schema_location_attr # Determine prefix from plan using extracted module prefix_info = ElementPrefixResolver.resolve(mapping: mapping, plan: plan) prefix = prefix_info[:prefix] ns_decl = prefix_info[:ns_decl] # Check if element's own namespace needs local declaration (out of scope) if ns_decl&.local_on_use? # FIX: Handle both default (nil prefix) and prefixed namespaces xmlns_attr = if prefix "xmlns:#{prefix}" else "xmlns" end attributes[xmlns_attr] = ns_decl.uri end # W3C COMPLIANCE: Detect if element needs xmlns="" using extracted module if BlankNamespaceHandler.needs_xmlns_blank?(mapping: mapping, options: ) attributes["xmlns"] = "" end # Native type inheritance fix: handle local_on_use xmlns="" even if parents uses default format xmlns_prefix = nil xmlns_ns = nil if mapping&.namespace_class && plan xmlns_ns = plan.namespace_for_class(mapping.namespace_class) xmlns_prefix = xmlns_ns&.prefix end if xmlns_ns&.local_on_use? && !mapping.namespace_uri attributes["xmlns:#{xmlns_prefix}"] = xmlns_ns&.uri || mapping.namespace_uri end tag_name = [:tag_name] || mapping.root_element return if [:except]&.include?(tag_name) # Track if THIS element uses default namespace format # Children will need this info to know if they should add xmlns="" this_element_uses_default_ns = mapping.namespace_class && plan.namespace_for_class(mapping.namespace_class)&.default_format? # Get element_form_default from this element's namespace for children parent_element_form_default = mapping.namespace_class&.element_form_default xml.create_and_add_element(tag_name, attributes: attributes, prefix: prefix) do |xml| # Call attribute custom methods now that element is created attribute_custom_methods.each do |attribute_rule| mapper_class.new.send(attribute_rule.custom_methods[:to], element, xml.parent, xml) end if ordered?(element, .merge(mapper_class: mapper_class)) build_ordered_element_with_plan(xml, element, plan, .merge( mapper_class: mapper_class, parent_prefix: prefix, parent_uses_default_ns: this_element_uses_default_ns, parent_element_form_default: parent_element_form_default, parent_ns_decl: ns_decl, )) else build_unordered_children_with_plan(xml, element, plan, .merge( mapper_class: mapper_class, parent_prefix: prefix, parent_uses_default_ns: this_element_uses_default_ns, parent_element_form_default: parent_element_form_default, parent_ns_decl: ns_decl, )) end end end |
#build_nested_element_with_plan(xml, value, element_rule, attribute_def, plan, options, parent_plan: nil) ⇒ Object
NOTE: build_unordered_children_with_plan and build_ordered_element_with_plan are inherited from BaseAdapter - no need to override
411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 |
# File 'lib/lutaml/xml/adapter/nokogiri_adapter.rb', line 411 def build_nested_element_with_plan(xml, value, element_rule, attribute_def, plan, , parent_plan: nil) if value.is_a?(Lutaml::Model::Collection) items = value.collection attr_type = attribute_def.type(@register) if attr_type <= Lutaml::Model::Type::Value # Simple types - serialize each item items.each do |val| build_element_value_with_plan(xml, element_rule, val, attribute_def, plan: plan, mapping: nil, options: .merge(element: val)) end else # Model types - build elements items.each do |val| # For polymorphic collections, use each item's actual class item_mapper_class = if polymorphic_value?(attribute_def, val) val.class else attr_type end # Collect and plan for each item item_mapping = item_mapper_class.mappings_for(:xml) if item_mapping collector = NamespaceCollector.new(@register) item_needs = collector.collect(val, item_mapping) planner = DeclarationPlanner.new(@register) item_plan = planner.plan(val, item_mapping, item_needs, parent_plan: plan, options: ) else item_plan = plan end # Performance: Use dup with direct assignment to avoid merge allocations # when mapper_class differs from current if [:mapper_class] == item_mapper_class = else = .dup [:mapper_class] = item_mapper_class end if item_plan build_element_with_plan(xml, val, item_plan, ) else build_element(xml, val, ) end end end else # Single Serialize instance # Performance: Use dup with direct assignment child_mapper = attribute_def.type(@register) if [:mapper_class] == child_mapper = else = .dup [:mapper_class] = child_mapper end build_element_with_plan(xml, value, plan, ) end end |
#build_xml_element(xml, element, parent_uses_default_ns: false, parent_element_form_default: nil, parent_namespace_class: nil) ⇒ Object
Build XML from XmlDataModel::XmlElement structure
564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 |
# File 'lib/lutaml/xml/adapter/nokogiri_adapter.rb', line 564 def build_xml_element(xml, element, parent_uses_default_ns: false, parent_element_form_default: nil, parent_namespace_class: nil) # Prepare attributes hash attributes = {} # Determine if attributes should be qualified based on element's namespace element_ns_class = element.namespace_class attribute_form_default = element_ns_class&.attribute_form_default || :unqualified element_prefix = element_ns_class&.prefix_default # Get element_form_default for children # Only set when explicitly configured, not when defaulted to :unqualified this_element_form_default = if element_ns_class&.element_form_default_set? element_ns_class.element_form_default end # Add regular attributes element.attributes.each do |attr| # Determine attribute name with namespace consideration attr_name = if attr.namespace_class # Check if attribute is in SAME namespace as element if attr.namespace_class == element_ns_class && attribute_form_default == :unqualified # Same namespace + unqualified → NO prefix (W3C rule) attr.name else # Different namespace OR qualified → use prefix attr_prefix = attr.namespace_class.prefix_default attr_prefix ? "#{attr_prefix}:#{attr.name}" : attr.name end elsif attribute_form_default == :qualified && element_prefix # Attribute inherits element's namespace when qualified "#{element_prefix}:#{attr.name}" else # Unqualified attribute attr.name end attributes[attr_name] = attr.value end # Determine element name with namespace prefix tag_name = element.name # CRITICAL FIX: element_form_default: :qualified means child elements inherit parent's namespace PREFIX # even when child has NO explicit namespace_class prefix = if element_ns_class && element_prefix # Element has explicit prefix_default - use prefix format element_prefix elsif !element_ns_class && parent_element_form_default == :qualified && parent_namespace_class&.prefix_default # Child has NO namespace, but parent has :qualified form_default # Child should INHERIT parent's namespace PREFIX parent_namespace_class.prefix_default else # No prefix (default format or no parent namespace) nil end # Track if THIS element uses default namespace format for children this_element_uses_default_ns = false # Add namespace declaration if element has namespace if element.namespace_class ns_uri = element.namespace_class.uri if prefix attributes["xmlns:#{prefix}"] = ns_uri # W3C Compliance: When parent uses default namespace and child declares # a DIFFERENT prefixed namespace, child must also add xmlns="" to prevent # its children from inheriting parent's default namespace if parent_uses_default_ns attributes["xmlns"] = "" end else attributes["xmlns"] = ns_uri this_element_uses_default_ns = true end elsif parent_uses_default_ns # W3C Compliance: Element has no namespace (blank namespace) # Check if should inherit parent's namespace based on element_form_default # Parent uses default namespace format if parent_element_form_default == :qualified # Child should INHERIT parent's namespace - no xmlns="" needed # The child is in parent namespace (qualified) elsif parent_element_form_default == :unqualified # Parent's element_form_default is :unqualified - child should be in blank namespace # WITHOUT xmlns="" (no xmlns attribute at all). The child is simply # not in any namespace, which is the correct W3C behavior for unqualified. else # element_form_default is not set (nil/default :unqualified) # Child needs xmlns="" to explicitly opt out of parent's default namespace attributes["xmlns"] = "" end end # Check if element was created from nil value with render_nil option # Add xsi:nil="true" attribute for W3C compliance if element.respond_to?(:xsi_nil) && element.xsi_nil attributes["xsi:nil"] = true end # Create element xml.create_and_add_element(tag_name, attributes: attributes, prefix: prefix) do |inner_xml| # Add text content if present if element.text_content # Check if content should be wrapped in CDATA if element.cdata inner_xml.cdata(element.text_content) else add_text_with_entities(inner_xml.parent, element.text_content.to_s, inner_xml.doc) end end # Recursively build child elements, passing namespace context element.children.each do |child| if child.is_a?(Lutaml::Xml::DataModel::XmlElement) build_xml_element(inner_xml, child, parent_uses_default_ns: this_element_uses_default_ns, parent_element_form_default: this_element_form_default, parent_namespace_class: element_ns_class) elsif child.is_a?(String) inner_xml.text(child) end end end end |
#build_xml_element_with_plan(xml, xml_element, plan, options = {}) ⇒ Object
Build XML from XmlDataModel::XmlElement using DeclarationPlan tree (PARALLEL TRAVERSAL)
Constructs Moxml node tree for XML serialization.
698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 |
# File 'lib/lutaml/xml/adapter/nokogiri_adapter.rb', line 698 def build_xml_element_with_plan(xml, xml_element, plan, = {}) moxml_doc = xml.doc root_node = build_xml_node(xml_element, plan.root_node, moxml_doc, plan.global_prefix_registry, nil, options: , plan: plan) moxml_doc.root = root_node # Add processing instructions before the root element. # reverse_each + add_previous_sibling maintains original order: # each PI is inserted before the root (and before previously-inserted PIs). xml_element.processing_instructions.reverse_each do |pi| pi_node = moxml_doc.create_processing_instruction(pi.target, pi.content) root_node.add_previous_sibling(pi_node) end end |
#order ⇒ Object
191 192 193 194 195 196 197 198 199 200 201 |
# File 'lib/lutaml/xml/adapter/nokogiri_adapter.rb', line 191 def order children.filter_map do |child| if child.text? next if child.text.nil? || child.text.strip.empty? Element.new("Text", "text", text_content: child.text) else Element.new("Element", child.unprefixed_name) end end end |
#to_xml(options = {}) ⇒ Object
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 |
# File 'lib/lutaml/xml/adapter/nokogiri_adapter.rb', line 42 def to_xml( = {}) # Accept xml_declaration from options if present (for model serialization) @xml_declaration = [:xml_declaration] if [:xml_declaration] = {} encoding = determine_encoding() [:encoding] = encoding if encoding builder = Builder::Nokogiri.build() do |xml| if root.is_a?(Lutaml::Xml::NokogiriElement) # Case A: Old parsed XML (from NokogiriElement) - use build_xml root.build_xml(xml) else # Cases B & C: XmlElement or Model instance # ARCHITECTURE: Normalize to XmlElement, then use single rendering path # Determine the source (XmlElement or model instance) original_model = nil xml_element = if root.is_a?(Lutaml::Xml::DataModel::XmlElement) # Case B: Already an XmlElement root else # Case C: Model instance - transform to XmlElement original_model = root mapper_class = [:mapper_class] || root.class transformation = mapper_class.transformation_for( :xml, @register ) transformation.transform(root, ) end # Collect original namespace URIs for namespace alias support. # This enables round-trip fidelity when XML uses alias URIs. original_ns_uris = {} stored_plan = nil if original_model # Case C: Model instance was transformed to XmlElement mapping_for_original = [:mapper_class]&.mappings_for(:xml) || original_model.class.mappings_for(:xml) original_ns_uris = collect_original_namespace_uris( original_model, mapping_for_original ) # Get stored declaration plan from model for PRESERVATION phase if original_model.is_a?(Lutaml::Model::Serialize) stored_plan = original_model.import_declaration_plan end elsif xml_element.is_a?(Lutaml::Xml::DataModel::XmlElement) # Case B: XmlElement from transformation may have @__xml_original_namespace_uri original_ns_uri = xml_element.original_namespace_uri if original_ns_uri # Get mapping from the mapper_class (model class) not from XmlElement mapper_klass = [:mapper_class] || xml_element.class xml_mapping = begin mapper_klass.mappings_for(:xml) rescue StandardError nil end if xml_mapping&.namespace_class canonical_uri = xml_mapping.namespace_class.uri if canonical_uri != original_ns_uri original_ns_uris[canonical_uri] = original_ns_uri end end end end = .merge(__original_namespace_uris: original_ns_uris) if stored_plan [:stored_xml_declaration_plan] = stored_plan end mapper_class = [:mapper_class] || xml_element.class mapping = mapper_class.mappings_for(:xml) # Phase 1: Collect namespace needs from XmlElement tree collector = NamespaceCollector.new(@register) needs = collector.collect(xml_element, mapping, mapper_class: mapper_class) # Phase 2: Plan namespace declarations (builds ElementNode tree) planner = DeclarationPlanner.new(@register) plan = planner.plan(xml_element, mapping, needs, options: ) # Phase 3: Render using XmlElement + DeclarationPlan # Pass original model for custom method invocation = .merge(is_root_element: true) [:original_model] = original_model if original_model build_xml_element_with_plan(xml, xml_element, plan, ) end end xml_data = builder.to_xml # Transcode to target encoding if needed target_encoding = encoding || [:encoding] if target_encoding && target_encoding.upcase != "UTF-8" xml_data = xml_data.encode(target_encoding) end result = "" # Handle XML declaration based on Issue #1: XML Declaration Preservation # Include declaration when encoding is specified OR when declaration is requested if ([:encoding] && ![:encoding].nil?) || should_include_declaration?() result += generate_declaration() end # Add DOCTYPE if present - use DeclarationHandler method doctype_to_use = [:doctype] || @doctype if doctype_to_use && ![:omit_doctype] result += generate_doctype_declaration(doctype_to_use) end result += xml_data # Post-process: Fix OOXML format issues (opt-in) result = fix_ooxml_format(result) if [:fix_boolean_elements] result end |