lutaml-model 0.8.5 → 0.8.6
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.
- checksums.yaml +4 -4
- data/.github/workflows/dependent-tests.yml +4 -1
- data/.rubocop_todo.yml +97 -22
- data/docs/_migrations/0-8-0-namespace-restructuring.adoc +90 -0
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/xml/adapter/adapter_helpers.rb +1 -42
- data/lib/lutaml/xml/adapter/base_adapter.rb +48 -458
- data/lib/lutaml/xml/adapter/namespace_data.rb +0 -17
- data/lib/lutaml/xml/adapter/namespace_uri_collector.rb +71 -0
- data/lib/lutaml/xml/adapter/nokogiri_adapter.rb +5 -1110
- data/lib/lutaml/xml/adapter/oga_adapter.rb +6 -846
- data/lib/lutaml/xml/adapter/ox_adapter.rb +7 -884
- data/lib/lutaml/xml/adapter/plan_based_builder.rb +929 -0
- data/lib/lutaml/xml/adapter/rexml_adapter.rb +10 -864
- data/lib/lutaml/xml/adapter/xml_parser.rb +86 -0
- data/lib/lutaml/xml/adapter/xml_serializer.rb +291 -0
- data/lib/lutaml/xml/adapter.rb +0 -1
- data/lib/lutaml/xml/adapter_element.rb +7 -1
- data/lib/lutaml/xml/builder/base.rb +0 -1
- data/lib/lutaml/xml/data_model.rb +9 -1
- data/lib/lutaml/xml/document.rb +3 -1
- data/lib/lutaml/xml/element.rb +13 -10
- data/lib/lutaml/xml/serialization/format_conversion.rb +19 -42
- data/lib/lutaml/xml/serialization/instance_methods.rb +26 -35
- data/lib/lutaml/xml/transformation/custom_method_wrapper.rb +34 -55
- data/lib/lutaml/xml/transformation/rule_applier.rb +1 -1
- data/lib/lutaml/xml/xml_element.rb +24 -20
- data/spec/lutaml/xml/adapter/base_adapter_regression_spec.rb +151 -0
- data/spec/lutaml/xml/adapter/order_spec.rb +150 -0
- data/spec/lutaml/xml/clear_parse_state_spec.rb +139 -0
- data/spec/lutaml/xml/doubly_defined_namespace_spec.rb +0 -2
- data/spec/lutaml/xml/schema/compiler_spec.rb +75 -69
- data/spec/lutaml/xml/transformation/custom_method_wrapper_spec.rb +213 -14
- metadata +9 -3
- data/lib/lutaml/xml/adapter/xml_serialization.rb +0 -145
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "moxml"
|
|
2
4
|
require "moxml/adapter/nokogiri"
|
|
3
5
|
require_relative "base_adapter"
|
|
@@ -6,1116 +8,9 @@ module Lutaml
|
|
|
6
8
|
module Xml
|
|
7
9
|
module Adapter
|
|
8
10
|
class NokogiriAdapter < BaseAdapter
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
TEXT_CLASSES = [Moxml::Text, Moxml::Cdata].freeze
|
|
13
|
-
|
|
14
|
-
def self.parse(xml, options = {})
|
|
15
|
-
enc = encoding(xml, options)
|
|
16
|
-
parsed = Moxml::Adapter::Nokogiri.parse(xml, encoding: enc)
|
|
17
|
-
root_element = parsed.root
|
|
18
|
-
|
|
19
|
-
# Validate that we have a root element
|
|
20
|
-
if root_element.nil?
|
|
21
|
-
raise Lutaml::Model::InvalidFormatError.new(
|
|
22
|
-
:xml,
|
|
23
|
-
"Document has no root element. " \
|
|
24
|
-
"The XML may be empty, contain only whitespace, " \
|
|
25
|
-
"or consist only of an XML declaration.",
|
|
26
|
-
)
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
# Extract DOCTYPE information from raw XML
|
|
30
|
-
# (Moxml doesn't directly expose DOCTYPE)
|
|
31
|
-
doctype_info = extract_doctype_from_xml(xml)
|
|
32
|
-
|
|
33
|
-
# Extract XML declaration for preservation
|
|
34
|
-
xml_decl_info = DeclarationHandler.extract_xml_declaration(xml)
|
|
35
|
-
|
|
36
|
-
@root = NokogiriElement.new(root_element)
|
|
37
|
-
@root.processing_instructions = extract_document_processing_instructions(parsed)
|
|
38
|
-
new(@root, enc, doctype: doctype_info,
|
|
39
|
-
xml_declaration: xml_decl_info)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def to_xml(options = {})
|
|
43
|
-
# Accept xml_declaration from options if present (for model serialization)
|
|
44
|
-
@xml_declaration = options[:xml_declaration] if options[:xml_declaration]
|
|
45
|
-
|
|
46
|
-
builder_options = {}
|
|
47
|
-
encoding = determine_encoding(options)
|
|
48
|
-
builder_options[:encoding] = encoding if encoding
|
|
49
|
-
|
|
50
|
-
builder = Builder::Nokogiri.build(builder_options) do |xml|
|
|
51
|
-
if root.is_a?(Lutaml::Xml::NokogiriElement)
|
|
52
|
-
# Case A: Old parsed XML (from NokogiriElement) - use build_xml
|
|
53
|
-
root.build_xml(xml)
|
|
54
|
-
else
|
|
55
|
-
# Cases B & C: XmlElement or Model instance
|
|
56
|
-
# ARCHITECTURE: Normalize to XmlElement, then use single rendering path
|
|
57
|
-
|
|
58
|
-
# Determine the source (XmlElement or model instance)
|
|
59
|
-
original_model = nil
|
|
60
|
-
|
|
61
|
-
xml_element = if root.is_a?(Lutaml::Xml::DataModel::XmlElement)
|
|
62
|
-
# Case B: Already an XmlElement
|
|
63
|
-
root
|
|
64
|
-
else
|
|
65
|
-
# Case C: Model instance - transform to XmlElement
|
|
66
|
-
original_model = root
|
|
67
|
-
mapper_class = options[:mapper_class] || root.class
|
|
68
|
-
transformation = mapper_class.transformation_for(
|
|
69
|
-
:xml, @register
|
|
70
|
-
)
|
|
71
|
-
transformation.transform(root, options)
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
# Collect original namespace URIs for namespace alias support.
|
|
75
|
-
# This enables round-trip fidelity when XML uses alias URIs.
|
|
76
|
-
original_ns_uris = {}
|
|
77
|
-
stored_plan = nil
|
|
78
|
-
if original_model
|
|
79
|
-
# Case C: Model instance was transformed to XmlElement
|
|
80
|
-
mapping_for_original = options[:mapper_class]&.mappings_for(:xml) || original_model.class.mappings_for(:xml)
|
|
81
|
-
original_ns_uris = collect_original_namespace_uris(
|
|
82
|
-
original_model, mapping_for_original
|
|
83
|
-
)
|
|
84
|
-
# Get stored declaration plan from model for PRESERVATION phase
|
|
85
|
-
if original_model.is_a?(Lutaml::Model::Serialize)
|
|
86
|
-
stored_plan = original_model.import_declaration_plan
|
|
87
|
-
end
|
|
88
|
-
elsif xml_element.is_a?(Lutaml::Xml::DataModel::XmlElement)
|
|
89
|
-
# Case B: XmlElement from transformation may have @__xml_original_namespace_uri
|
|
90
|
-
original_ns_uri = xml_element.original_namespace_uri
|
|
91
|
-
if original_ns_uri
|
|
92
|
-
# Get mapping from the mapper_class (model class) not from XmlElement
|
|
93
|
-
mapper_klass = options[:mapper_class] || xml_element.class
|
|
94
|
-
xml_mapping = begin
|
|
95
|
-
mapper_klass.mappings_for(:xml)
|
|
96
|
-
rescue StandardError
|
|
97
|
-
nil
|
|
98
|
-
end
|
|
99
|
-
if xml_mapping&.namespace_class
|
|
100
|
-
canonical_uri = xml_mapping.namespace_class.uri
|
|
101
|
-
if canonical_uri != original_ns_uri
|
|
102
|
-
original_ns_uris[canonical_uri] =
|
|
103
|
-
original_ns_uri
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
options_with_original_ns = options.merge(__original_namespace_uris: original_ns_uris)
|
|
109
|
-
if stored_plan
|
|
110
|
-
options_with_original_ns[:stored_xml_declaration_plan] =
|
|
111
|
-
stored_plan
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
mapper_class = options[:mapper_class] || xml_element.class
|
|
115
|
-
mapping = mapper_class.mappings_for(:xml)
|
|
116
|
-
|
|
117
|
-
# Phase 1: Collect namespace needs from XmlElement tree
|
|
118
|
-
collector = NamespaceCollector.new(@register)
|
|
119
|
-
needs = collector.collect(xml_element, mapping,
|
|
120
|
-
mapper_class: mapper_class)
|
|
121
|
-
|
|
122
|
-
# Phase 2: Plan namespace declarations (builds ElementNode tree)
|
|
123
|
-
planner = DeclarationPlanner.new(@register)
|
|
124
|
-
plan = planner.plan(xml_element, mapping, needs,
|
|
125
|
-
options: options_with_original_ns)
|
|
126
|
-
|
|
127
|
-
# Phase 3: Render using XmlElement + DeclarationPlan
|
|
128
|
-
# Pass original model for custom method invocation
|
|
129
|
-
render_options = options.merge(is_root_element: true)
|
|
130
|
-
render_options[:original_model] = original_model if original_model
|
|
131
|
-
build_xml_element_with_plan(xml, xml_element, plan,
|
|
132
|
-
render_options)
|
|
133
|
-
end
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
xml_data = builder.to_xml
|
|
137
|
-
|
|
138
|
-
# Transcode to target encoding if needed
|
|
139
|
-
target_encoding = encoding || options[:encoding]
|
|
140
|
-
if target_encoding && target_encoding.upcase != "UTF-8"
|
|
141
|
-
xml_data = xml_data.encode(target_encoding)
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
result = ""
|
|
145
|
-
|
|
146
|
-
# Handle XML declaration based on Issue #1: XML Declaration Preservation
|
|
147
|
-
# Include declaration when encoding is specified OR when declaration is requested
|
|
148
|
-
if (options[:encoding] && !options[:encoding].nil?) || should_include_declaration?(options)
|
|
149
|
-
result += generate_declaration(options)
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
# Add DOCTYPE if present - use DeclarationHandler method
|
|
153
|
-
doctype_to_use = options[:doctype] || @doctype
|
|
154
|
-
if doctype_to_use && !options[:omit_doctype]
|
|
155
|
-
result += generate_doctype_declaration(doctype_to_use)
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
result += xml_data
|
|
159
|
-
|
|
160
|
-
# Post-process: Fix OOXML format issues (opt-in)
|
|
161
|
-
result = fix_ooxml_format(result) if options[:fix_boolean_elements]
|
|
162
|
-
|
|
163
|
-
result
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
def attributes_hash(element)
|
|
167
|
-
result = Lutaml::Model::MappingHash.new
|
|
168
|
-
|
|
169
|
-
element.attributes_each_value do |attr|
|
|
170
|
-
if attr.name == "schemaLocation"
|
|
171
|
-
result["__schema_location"] = {
|
|
172
|
-
namespace: attr.namespace,
|
|
173
|
-
prefix: attr.namespace.prefix,
|
|
174
|
-
schema_location: attr.value,
|
|
175
|
-
}
|
|
176
|
-
else
|
|
177
|
-
result[self.class.namespaced_attr_name(attr)] = attr.value
|
|
178
|
-
end
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
result
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
# NOTE: name_of, prefixed_name_of, namespaced_attr_name, namespaced_name_of
|
|
185
|
-
# are provided by AdapterHelpers module via extend
|
|
186
|
-
|
|
187
|
-
def self.text_of(element)
|
|
188
|
-
element.text
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
def order
|
|
192
|
-
children.filter_map do |child|
|
|
193
|
-
if child.text?
|
|
194
|
-
next if child.text.nil?
|
|
195
|
-
|
|
196
|
-
Element.new("Text", "text", text_content: child.text)
|
|
197
|
-
elsif child.comment?
|
|
198
|
-
Element.new("Comment", "comment",
|
|
199
|
-
text_content: child.content,
|
|
200
|
-
node_type: :comment)
|
|
201
|
-
else
|
|
202
|
-
Element.new("Element", child.unprefixed_name)
|
|
203
|
-
end
|
|
204
|
-
end
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
def self.order_of(element)
|
|
208
|
-
element.children.each do |node|
|
|
209
|
-
if node.is_a?(Moxml::ProcessingInstruction)
|
|
210
|
-
return [Element.new("ProcessingInstruction", node.name)]
|
|
211
|
-
end
|
|
212
|
-
end
|
|
213
|
-
super
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
# Build element using prepared namespace declaration plan
|
|
217
|
-
#
|
|
218
|
-
# @param xml [Builder] the XML builder
|
|
219
|
-
# @param element [Object] the model instance
|
|
220
|
-
# @param plan [Hash] the declaration plan from DeclarationPlanner
|
|
221
|
-
# @param options [Hash] serialization options
|
|
222
|
-
def build_element_with_plan(xml, element, plan, options = {})
|
|
223
|
-
# Provide default empty plan if nil (e.g., for custom methods)
|
|
224
|
-
plan ||= DeclarationPlan.empty
|
|
225
|
-
|
|
226
|
-
mapper_class = options[:mapper_class] || element.class
|
|
227
|
-
|
|
228
|
-
# New: Handle simple types that don't have mappings
|
|
229
|
-
unless mapper_class.is_a?(Class) && mapper_class.include?(Lutaml::Model::Serialize)
|
|
230
|
-
tag_name = options[:tag_name] || "element"
|
|
231
|
-
xml.create_and_add_element(tag_name) do |inner_xml|
|
|
232
|
-
inner_xml.text(element.to_s)
|
|
233
|
-
end
|
|
234
|
-
return xml
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
mapping = mapper_class.mappings_for(:xml)
|
|
238
|
-
return xml unless mapping
|
|
239
|
-
|
|
240
|
-
# TYPE-ONLY MODELS: No element wrapper, serialize children directly
|
|
241
|
-
# BUT if we have a tag_name in options, that means parent wants a wrapper
|
|
242
|
-
if mapping.namespace_class
|
|
243
|
-
# Check if this element's namespace is explicitly :blank
|
|
244
|
-
# This happens when the model uses 'namespace :blank' in its xml block
|
|
245
|
-
# We can detect this through the plan - but since we're inside build_element_with_plan,
|
|
246
|
-
# we need to check the mapping directly
|
|
247
|
-
# Actually, the element itself won't have explicit_blank in its namespace resolution
|
|
248
|
-
# because it's the element's OWN namespace. We need to skip this for the element itself.
|
|
249
|
-
# The xmlns="" handling is for CHILD elements, not the parent element.
|
|
250
|
-
# So this section is actually not needed here - it's needed in add_simple_value
|
|
251
|
-
# But it reads:
|
|
252
|
-
# @mapping.namespace_class
|
|
253
|
-
# element.ns_info_for(repository_name, mapping.xml_namespace)
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
# Use xmlns declarations from plan
|
|
257
|
-
attributes = {}
|
|
258
|
-
attributes.merge!(NamespaceDeclarationBuilder.build_xmlns_attributes(plan))
|
|
259
|
-
|
|
260
|
-
# Collect attribute custom methods to call after element creation
|
|
261
|
-
attribute_custom_methods = []
|
|
262
|
-
|
|
263
|
-
# Add regular attributes (non-xmlns)
|
|
264
|
-
mapping.attributes.each do |attribute_rule|
|
|
265
|
-
next if options[:except]&.include?(attribute_rule.to)
|
|
266
|
-
|
|
267
|
-
# Collect custom methods for later execution (after element is created)
|
|
268
|
-
if attribute_rule.custom_methods[:to]
|
|
269
|
-
attribute_custom_methods << attribute_rule
|
|
270
|
-
next
|
|
271
|
-
end
|
|
272
|
-
|
|
273
|
-
mapping_rule_name = if attribute_rule.multiple_mappings?
|
|
274
|
-
attribute_rule.name.first
|
|
275
|
-
else
|
|
276
|
-
attribute_rule.name
|
|
277
|
-
end
|
|
278
|
-
|
|
279
|
-
attr = attribute_definition_for(element, attribute_rule,
|
|
280
|
-
mapper_class: mapper_class)
|
|
281
|
-
value = attribute_rule.to_value_for(element)
|
|
282
|
-
|
|
283
|
-
# Handle as_list and delimiter BEFORE serialization for array values
|
|
284
|
-
# These features convert arrays to delimited strings before serialization
|
|
285
|
-
if value.is_a?(Array)
|
|
286
|
-
if attribute_rule.as_list && attribute_rule.as_list[:export]
|
|
287
|
-
value = attribute_rule.as_list[:export].call(value)
|
|
288
|
-
elsif attribute_rule.delimiter
|
|
289
|
-
value = value.join(attribute_rule.delimiter)
|
|
290
|
-
end
|
|
291
|
-
end
|
|
292
|
-
|
|
293
|
-
value = attr.serialize(value, :xml, @register) if attr
|
|
294
|
-
value = ExportTransformer.call(value, attribute_rule, attr,
|
|
295
|
-
format: :xml)
|
|
296
|
-
|
|
297
|
-
if render_element?(attribute_rule, element, value)
|
|
298
|
-
# Resolve attribute namespace using extracted module
|
|
299
|
-
ns_info = AttributeNamespaceResolver.resolve(
|
|
300
|
-
rule: attribute_rule,
|
|
301
|
-
attribute: attr,
|
|
302
|
-
plan: plan,
|
|
303
|
-
mapper_class: mapper_class,
|
|
304
|
-
register: @register,
|
|
305
|
-
)
|
|
306
|
-
|
|
307
|
-
# Build qualified attribute name based on W3C semantics
|
|
308
|
-
attr_name = AttributeNamespaceResolver.build_qualified_name(
|
|
309
|
-
ns_info,
|
|
310
|
-
mapping_rule_name,
|
|
311
|
-
attribute_rule,
|
|
312
|
-
)
|
|
313
|
-
attributes[attr_name] = value ? value.to_s : value
|
|
314
|
-
|
|
315
|
-
# Add local xmlns declaration if needed
|
|
316
|
-
if ns_info[:needs_local_declaration]
|
|
317
|
-
attributes[ns_info[:local_xmlns_attr]] =
|
|
318
|
-
ns_info[:local_xmlns_uri]
|
|
319
|
-
end
|
|
320
|
-
end
|
|
321
|
-
end
|
|
322
|
-
|
|
323
|
-
# Add schema_location attribute from ElementNode if present
|
|
324
|
-
# This is for the plan-based path where schema_location_attr is computed during planning
|
|
325
|
-
attributes.merge!(plan.root_node.schema_location_attr) if plan&.root_node&.schema_location_attr
|
|
326
|
-
|
|
327
|
-
# Determine prefix from plan using extracted module
|
|
328
|
-
prefix_info = ElementPrefixResolver.resolve(mapping: mapping,
|
|
329
|
-
plan: plan)
|
|
330
|
-
prefix = prefix_info[:prefix]
|
|
331
|
-
ns_decl = prefix_info[:ns_decl]
|
|
332
|
-
|
|
333
|
-
# Check if element's own namespace needs local declaration (out of scope)
|
|
334
|
-
if ns_decl&.local_on_use?
|
|
335
|
-
# FIX: Handle both default (nil prefix) and prefixed namespaces
|
|
336
|
-
xmlns_attr = if prefix
|
|
337
|
-
"xmlns:#{prefix}"
|
|
338
|
-
else
|
|
339
|
-
"xmlns"
|
|
340
|
-
end
|
|
341
|
-
attributes[xmlns_attr] = ns_decl.uri
|
|
342
|
-
end
|
|
343
|
-
|
|
344
|
-
# W3C COMPLIANCE: Detect if element needs xmlns="" using extracted module
|
|
345
|
-
if BlankNamespaceHandler.needs_xmlns_blank?(mapping: mapping,
|
|
346
|
-
options: options)
|
|
347
|
-
attributes["xmlns"] = ""
|
|
348
|
-
end
|
|
349
|
-
|
|
350
|
-
# Native type inheritance fix: handle local_on_use xmlns="" even if parents uses default format
|
|
351
|
-
xmlns_prefix = nil
|
|
352
|
-
xmlns_ns = nil
|
|
353
|
-
if mapping&.namespace_class && plan
|
|
354
|
-
xmlns_ns = plan.namespace_for_class(mapping.namespace_class)
|
|
355
|
-
xmlns_prefix = xmlns_ns&.prefix
|
|
356
|
-
end
|
|
357
|
-
if xmlns_ns&.local_on_use? && !mapping.namespace_uri
|
|
358
|
-
attributes["xmlns:#{xmlns_prefix}"] =
|
|
359
|
-
xmlns_ns&.uri || mapping.namespace_uri
|
|
360
|
-
end
|
|
361
|
-
|
|
362
|
-
tag_name = options[:tag_name] || mapping.root_element
|
|
363
|
-
return if options[:except]&.include?(tag_name)
|
|
364
|
-
|
|
365
|
-
# Track if THIS element uses default namespace format
|
|
366
|
-
# Children will need this info to know if they should add xmlns=""
|
|
367
|
-
this_element_uses_default_ns = mapping.namespace_class &&
|
|
368
|
-
plan.namespace_for_class(mapping.namespace_class)&.default_format?
|
|
369
|
-
|
|
370
|
-
# Get element_form_default from this element's namespace for children
|
|
371
|
-
parent_element_form_default = mapping.namespace_class&.element_form_default
|
|
372
|
-
|
|
373
|
-
xml.create_and_add_element(tag_name, attributes: attributes,
|
|
374
|
-
prefix: prefix) do |xml|
|
|
375
|
-
# Call attribute custom methods now that element is created
|
|
376
|
-
attribute_custom_methods.each do |attribute_rule|
|
|
377
|
-
mapper_class.new.send(attribute_rule.custom_methods[:to],
|
|
378
|
-
element, xml.parent, xml)
|
|
379
|
-
end
|
|
380
|
-
|
|
381
|
-
if ordered?(element, options.merge(mapper_class: mapper_class))
|
|
382
|
-
build_ordered_element_with_plan(xml, element, plan,
|
|
383
|
-
options.merge(
|
|
384
|
-
mapper_class: mapper_class,
|
|
385
|
-
parent_prefix: prefix,
|
|
386
|
-
parent_uses_default_ns: this_element_uses_default_ns,
|
|
387
|
-
parent_element_form_default: parent_element_form_default,
|
|
388
|
-
parent_ns_decl: ns_decl,
|
|
389
|
-
))
|
|
390
|
-
else
|
|
391
|
-
build_unordered_children_with_plan(xml, element, plan,
|
|
392
|
-
options.merge(
|
|
393
|
-
mapper_class: mapper_class,
|
|
394
|
-
parent_prefix: prefix,
|
|
395
|
-
parent_uses_default_ns: this_element_uses_default_ns,
|
|
396
|
-
parent_element_form_default: parent_element_form_default,
|
|
397
|
-
parent_ns_decl: ns_decl,
|
|
398
|
-
))
|
|
399
|
-
end
|
|
400
|
-
end
|
|
401
|
-
end
|
|
402
|
-
|
|
403
|
-
# Build element children in the original order (for ordered content)
|
|
404
|
-
#
|
|
405
|
-
# This method preserves the order of elements as they appeared in the
|
|
406
|
-
# original XML, using element.element_order to iterate through elements.
|
|
407
|
-
#
|
|
408
|
-
# @param xml [Builder] XML builder
|
|
409
|
-
# @param element [Object] model instance
|
|
410
|
-
# @param plan [DeclarationPlan] namespace declaration plan
|
|
411
|
-
|
|
412
|
-
# NOTE: build_unordered_children_with_plan and build_ordered_element_with_plan
|
|
413
|
-
# are inherited from BaseAdapter - no need to override
|
|
414
|
-
|
|
415
|
-
def build_nested_element_with_plan(xml, value, element_rule,
|
|
416
|
-
attribute_def, plan, options, parent_plan: nil)
|
|
417
|
-
if value.is_a?(Lutaml::Model::Collection)
|
|
418
|
-
items = value.collection
|
|
419
|
-
attr_type = attribute_def.type(@register)
|
|
420
|
-
|
|
421
|
-
if attr_type <= Lutaml::Model::Type::Value
|
|
422
|
-
# Simple types - serialize each item
|
|
423
|
-
items.each do |val|
|
|
424
|
-
build_element_value_with_plan(xml, element_rule, val, attribute_def,
|
|
425
|
-
plan: plan, mapping: nil, options: options.merge(element: val))
|
|
426
|
-
end
|
|
427
|
-
else
|
|
428
|
-
# Model types - build elements
|
|
429
|
-
items.each do |val|
|
|
430
|
-
# For polymorphic collections, use each item's actual class
|
|
431
|
-
item_mapper_class = if polymorphic_value?(attribute_def, val)
|
|
432
|
-
val.class
|
|
433
|
-
else
|
|
434
|
-
attr_type
|
|
435
|
-
end
|
|
436
|
-
|
|
437
|
-
# Collect and plan for each item
|
|
438
|
-
item_mapping = item_mapper_class.mappings_for(:xml)
|
|
439
|
-
if item_mapping
|
|
440
|
-
collector = NamespaceCollector.new(@register)
|
|
441
|
-
item_needs = collector.collect(val, item_mapping)
|
|
442
|
-
|
|
443
|
-
planner = DeclarationPlanner.new(@register)
|
|
444
|
-
item_plan = planner.plan(val, item_mapping, item_needs,
|
|
445
|
-
parent_plan: plan, options: options)
|
|
446
|
-
else
|
|
447
|
-
item_plan = plan
|
|
448
|
-
end
|
|
449
|
-
|
|
450
|
-
# Performance: Use dup with direct assignment to avoid merge allocations
|
|
451
|
-
# when mapper_class differs from current
|
|
452
|
-
if options[:mapper_class] == item_mapper_class
|
|
453
|
-
item_options = options
|
|
454
|
-
else
|
|
455
|
-
item_options = options.dup
|
|
456
|
-
item_options[:mapper_class] = item_mapper_class
|
|
457
|
-
end
|
|
458
|
-
if item_plan
|
|
459
|
-
build_element_with_plan(xml, val, item_plan, item_options)
|
|
460
|
-
else
|
|
461
|
-
build_element(xml, val, item_options)
|
|
462
|
-
end
|
|
463
|
-
end
|
|
464
|
-
end
|
|
465
|
-
else
|
|
466
|
-
# Single Serialize instance
|
|
467
|
-
# Performance: Use dup with direct assignment
|
|
468
|
-
child_mapper = attribute_def.type(@register)
|
|
469
|
-
if options[:mapper_class] == child_mapper
|
|
470
|
-
child_options = options
|
|
471
|
-
else
|
|
472
|
-
child_options = options.dup
|
|
473
|
-
child_options[:mapper_class] = child_mapper
|
|
474
|
-
end
|
|
475
|
-
build_element_with_plan(xml, value, plan, child_options)
|
|
476
|
-
end
|
|
477
|
-
end
|
|
478
|
-
|
|
479
|
-
# Build simple element value with plan
|
|
480
|
-
#
|
|
481
|
-
# @param xml [Builder] XML builder
|
|
482
|
-
# @param element_rule [MappingRule] element mapping rule
|
|
483
|
-
# @param value [Object] value to serialize
|
|
484
|
-
# @param attribute_def [Attribute] attribute definition
|
|
485
|
-
# @param plan [DeclarationPlan] namespace plan
|
|
486
|
-
# @param mapping [Xml::Mapping] optional mapping
|
|
487
|
-
# @param options [Hash] serialization options
|
|
488
|
-
def build_element_value_with_plan(xml, element_rule, value,
|
|
489
|
-
attribute_def, plan:, mapping: nil, options: {})
|
|
490
|
-
# Handle array values by creating multiple elements
|
|
491
|
-
if value.is_a?(Array)
|
|
492
|
-
value.each do |val|
|
|
493
|
-
build_element_value_with_plan(xml, element_rule, val, attribute_def,
|
|
494
|
-
plan: plan, mapping: mapping, options: options)
|
|
495
|
-
end
|
|
496
|
-
return
|
|
497
|
-
end
|
|
498
|
-
|
|
499
|
-
return unless render_element?(element_rule, options[:element], value)
|
|
500
|
-
|
|
501
|
-
# Get namespace info for this element
|
|
502
|
-
mapping_local = mapping || options[:mapper_class]&.mappings_for(:xml)
|
|
503
|
-
ns_info = if mapping_local
|
|
504
|
-
# Try to resolve namespace using local mapping
|
|
505
|
-
begin
|
|
506
|
-
NamespaceResolver.new(@register).resolve_for_element(
|
|
507
|
-
element_rule, attribute_def, mapping_local, plan, options
|
|
508
|
-
)
|
|
509
|
-
rescue StandardError
|
|
510
|
-
# Fallback to default behavior
|
|
511
|
-
{ prefix: nil, ns_info: nil }
|
|
512
|
-
end
|
|
513
|
-
else
|
|
514
|
-
{ prefix: nil, ns_info: nil }
|
|
515
|
-
end
|
|
516
|
-
|
|
517
|
-
prefix = ns_info[:prefix]
|
|
518
|
-
|
|
519
|
-
# Get child's plan if available
|
|
520
|
-
child_plan = plan&.child_plan(element_rule.to)
|
|
521
|
-
|
|
522
|
-
if value.is_a?(Lutaml::Model::Serialize)
|
|
523
|
-
# Nested Serialize object
|
|
524
|
-
child_mapper_class = value.class
|
|
525
|
-
child_mapper_class.mappings_for(:xml)
|
|
526
|
-
|
|
527
|
-
# Performance: Use dup with direct assignment to avoid merge allocations
|
|
528
|
-
if options[:mapper_class] == child_mapper_class
|
|
529
|
-
child_options = options
|
|
530
|
-
else
|
|
531
|
-
child_options = options.dup
|
|
532
|
-
child_options[:mapper_class] = child_mapper_class
|
|
533
|
-
end
|
|
534
|
-
|
|
535
|
-
if child_plan
|
|
536
|
-
build_element_with_plan(xml, value, child_plan, child_options)
|
|
537
|
-
else
|
|
538
|
-
build_element(xml, value, child_options)
|
|
539
|
-
end
|
|
540
|
-
elsif value.nil? && element_rule.render_nil?
|
|
541
|
-
# Render nil value
|
|
542
|
-
element_name = element_rule.multiple_mappings? ? element_rule.name.first : element_rule.name
|
|
543
|
-
xml.create_and_add_element(element_name,
|
|
544
|
-
prefix: prefix) do |inner_xml|
|
|
545
|
-
inner_xml.text("")
|
|
546
|
-
end
|
|
547
|
-
elsif value
|
|
548
|
-
# Simple string value
|
|
549
|
-
element_name = element_rule.multiple_mappings? ? element_rule.name.first : element_rule.name
|
|
550
|
-
xml.create_and_add_element(element_name,
|
|
551
|
-
prefix: prefix) do |inner_xml|
|
|
552
|
-
if element_rule.cdata
|
|
553
|
-
inner_xml.cdata(value.to_s)
|
|
554
|
-
else
|
|
555
|
-
inner_xml.text(value.to_s)
|
|
556
|
-
end
|
|
557
|
-
end
|
|
558
|
-
end
|
|
559
|
-
end
|
|
560
|
-
|
|
561
|
-
# Build XML from XmlDataModel::XmlElement structure
|
|
562
|
-
#
|
|
563
|
-
# @param xml [Builder] XML builder
|
|
564
|
-
# @param element [XmlDataModel::XmlElement] element to build
|
|
565
|
-
# @param parent_uses_default_ns [Boolean] parent uses default namespace format
|
|
566
|
-
# @param parent_element_form_default [Symbol] parent's element_form_default
|
|
567
|
-
# @param parent_namespace_class [Class] parent's namespace class
|
|
568
|
-
def build_xml_element(xml, element, parent_uses_default_ns: false,
|
|
569
|
-
parent_element_form_default: nil, parent_namespace_class: nil)
|
|
570
|
-
# Prepare attributes hash
|
|
571
|
-
attributes = {}
|
|
572
|
-
|
|
573
|
-
# Determine if attributes should be qualified based on element's namespace
|
|
574
|
-
element_ns_class = element.namespace_class
|
|
575
|
-
attribute_form_default = element_ns_class&.attribute_form_default || :unqualified
|
|
576
|
-
element_prefix = element_ns_class&.prefix_default
|
|
577
|
-
|
|
578
|
-
# Get element_form_default for children
|
|
579
|
-
# Only set when explicitly configured, not when defaulted to :unqualified
|
|
580
|
-
this_element_form_default = if element_ns_class&.element_form_default_set?
|
|
581
|
-
element_ns_class.element_form_default
|
|
582
|
-
end
|
|
583
|
-
|
|
584
|
-
# Add regular attributes
|
|
585
|
-
element.attributes.each do |attr|
|
|
586
|
-
# Determine attribute name with namespace consideration
|
|
587
|
-
attr_name = if attr.namespace_class
|
|
588
|
-
# Check if attribute is in SAME namespace as element
|
|
589
|
-
if attr.namespace_class == element_ns_class && attribute_form_default == :unqualified
|
|
590
|
-
# Same namespace + unqualified → NO prefix (W3C rule)
|
|
591
|
-
attr.name
|
|
592
|
-
else
|
|
593
|
-
# Different namespace OR qualified → use prefix
|
|
594
|
-
attr_prefix = attr.namespace_class.prefix_default
|
|
595
|
-
attr_prefix ? "#{attr_prefix}:#{attr.name}" : attr.name
|
|
596
|
-
end
|
|
597
|
-
elsif attribute_form_default == :qualified && element_prefix
|
|
598
|
-
# Attribute inherits element's namespace when qualified
|
|
599
|
-
"#{element_prefix}:#{attr.name}"
|
|
600
|
-
else
|
|
601
|
-
# Unqualified attribute
|
|
602
|
-
attr.name
|
|
603
|
-
end
|
|
604
|
-
attributes[attr_name] = attr.value
|
|
605
|
-
end
|
|
606
|
-
|
|
607
|
-
# Determine element name with namespace prefix
|
|
608
|
-
tag_name = element.name
|
|
609
|
-
# CRITICAL FIX: element_form_default: :qualified means child elements inherit parent's namespace PREFIX
|
|
610
|
-
# even when child has NO explicit namespace_class
|
|
611
|
-
prefix = if element_ns_class && element_prefix
|
|
612
|
-
# Element has explicit prefix_default - use prefix format
|
|
613
|
-
element_prefix
|
|
614
|
-
elsif !element_ns_class && parent_element_form_default == :qualified && parent_namespace_class&.prefix_default
|
|
615
|
-
# Child has NO namespace, but parent has :qualified form_default
|
|
616
|
-
# Child should INHERIT parent's namespace PREFIX
|
|
617
|
-
parent_namespace_class.prefix_default
|
|
618
|
-
else
|
|
619
|
-
# No prefix (default format or no parent namespace)
|
|
620
|
-
nil
|
|
621
|
-
end
|
|
622
|
-
|
|
623
|
-
# Track if THIS element uses default namespace format for children
|
|
624
|
-
this_element_uses_default_ns = false
|
|
625
|
-
|
|
626
|
-
# Add namespace declaration if element has namespace
|
|
627
|
-
if element.namespace_class
|
|
628
|
-
ns_uri = element.namespace_class.uri
|
|
629
|
-
|
|
630
|
-
if prefix
|
|
631
|
-
attributes["xmlns:#{prefix}"] = ns_uri
|
|
632
|
-
# W3C Compliance: When parent uses default namespace and child declares
|
|
633
|
-
# a DIFFERENT prefixed namespace, child must also add xmlns="" to prevent
|
|
634
|
-
# its children from inheriting parent's default namespace
|
|
635
|
-
if parent_uses_default_ns
|
|
636
|
-
attributes["xmlns"] = ""
|
|
637
|
-
end
|
|
638
|
-
else
|
|
639
|
-
attributes["xmlns"] = ns_uri
|
|
640
|
-
this_element_uses_default_ns = true
|
|
641
|
-
end
|
|
642
|
-
elsif parent_uses_default_ns
|
|
643
|
-
# W3C Compliance: Element has no namespace (blank namespace)
|
|
644
|
-
# Check if should inherit parent's namespace based on element_form_default
|
|
645
|
-
# Parent uses default namespace format
|
|
646
|
-
if parent_element_form_default == :qualified
|
|
647
|
-
# Child should INHERIT parent's namespace - no xmlns="" needed
|
|
648
|
-
# The child is in parent namespace (qualified)
|
|
649
|
-
elsif parent_element_form_default == :unqualified
|
|
650
|
-
# Parent's element_form_default is :unqualified - child should be in blank namespace
|
|
651
|
-
# WITHOUT xmlns="" (no xmlns attribute at all). The child is simply
|
|
652
|
-
# not in any namespace, which is the correct W3C behavior for unqualified.
|
|
653
|
-
else
|
|
654
|
-
# element_form_default is not set (nil/default :unqualified)
|
|
655
|
-
# Child needs xmlns="" to explicitly opt out of parent's default namespace
|
|
656
|
-
attributes["xmlns"] = ""
|
|
657
|
-
end
|
|
658
|
-
end
|
|
659
|
-
|
|
660
|
-
# Check if element was created from nil value with render_nil option
|
|
661
|
-
# Add xsi:nil="true" attribute for W3C compliance
|
|
662
|
-
if element.respond_to?(:xsi_nil) && element.xsi_nil
|
|
663
|
-
attributes["xsi:nil"] = true
|
|
664
|
-
end
|
|
665
|
-
|
|
666
|
-
# Create element
|
|
667
|
-
xml.create_and_add_element(tag_name, attributes: attributes,
|
|
668
|
-
prefix: prefix) do |inner_xml|
|
|
669
|
-
# Add text content if present
|
|
670
|
-
if element.text_content
|
|
671
|
-
# Check if content should be wrapped in CDATA
|
|
672
|
-
if element.cdata
|
|
673
|
-
inner_xml.cdata(element.text_content)
|
|
674
|
-
else
|
|
675
|
-
add_text_with_entities(inner_xml.parent,
|
|
676
|
-
element.text_content.to_s, inner_xml.doc)
|
|
677
|
-
end
|
|
678
|
-
end
|
|
679
|
-
|
|
680
|
-
# Recursively build child elements, passing namespace context
|
|
681
|
-
element.children.each do |child|
|
|
682
|
-
if child.is_a?(Lutaml::Xml::DataModel::XmlElement)
|
|
683
|
-
build_xml_element(inner_xml, child,
|
|
684
|
-
parent_uses_default_ns: this_element_uses_default_ns,
|
|
685
|
-
parent_element_form_default: this_element_form_default,
|
|
686
|
-
parent_namespace_class: element_ns_class)
|
|
687
|
-
elsif child.is_a?(String)
|
|
688
|
-
inner_xml.text(child)
|
|
689
|
-
end
|
|
690
|
-
end
|
|
691
|
-
end
|
|
692
|
-
end
|
|
693
|
-
|
|
694
|
-
# Build XML from XmlDataModel::XmlElement using DeclarationPlan tree (PARALLEL TRAVERSAL)
|
|
695
|
-
#
|
|
696
|
-
# Constructs Moxml node tree for XML serialization.
|
|
697
|
-
#
|
|
698
|
-
# @param xml [Builder] XML builder (provides doc access)
|
|
699
|
-
# @param xml_element [XmlDataModel::XmlElement] Element content
|
|
700
|
-
# @param plan [DeclarationPlan] Declaration plan with tree structure
|
|
701
|
-
# @param options [Hash] Serialization options
|
|
702
|
-
def build_xml_element_with_plan(xml, xml_element, plan, options = {})
|
|
703
|
-
moxml_doc = xml.doc
|
|
704
|
-
|
|
705
|
-
root_node = build_xml_node(xml_element, plan.root_node, moxml_doc,
|
|
706
|
-
plan.global_prefix_registry, nil, options: options, plan: plan)
|
|
707
|
-
moxml_doc.root = root_node
|
|
708
|
-
|
|
709
|
-
# Add processing instructions before the root element.
|
|
710
|
-
# reverse_each + add_previous_sibling maintains original order:
|
|
711
|
-
# each PI is inserted before the root (and before previously-inserted PIs).
|
|
712
|
-
xml_element.processing_instructions.reverse_each do |pi|
|
|
713
|
-
pi_node = moxml_doc.create_processing_instruction(pi.target,
|
|
714
|
-
pi.content)
|
|
715
|
-
root_node.add_previous_sibling(pi_node)
|
|
716
|
-
end
|
|
717
|
-
end
|
|
718
|
-
|
|
719
|
-
private
|
|
720
|
-
|
|
721
|
-
# Override BaseAdapter hook to preserve entity references.
|
|
722
|
-
def add_text_nodes(element, text, doc)
|
|
723
|
-
add_text_with_entities(element, text, doc)
|
|
724
|
-
end
|
|
725
|
-
|
|
726
|
-
# Builds Moxml node tree from XmlDataModel::XmlElement content and
|
|
727
|
-
# DeclarationPlan decisions (PARALLEL TRAVERSAL).
|
|
728
|
-
#
|
|
729
|
-
# Uses Moxml APIs for all DOM operations:
|
|
730
|
-
# - doc.create_element for document-aware node creation
|
|
731
|
-
# - element.in_scope_namespaces for in-scope namespace query
|
|
732
|
-
# - element.namespace= for namespace assignment
|
|
733
|
-
# - doc.create_text/create_cdata/create_entity_reference for child nodes
|
|
734
|
-
#
|
|
735
|
-
# @param xml_element [XmlDataModel::XmlElement] Content
|
|
736
|
-
# @param element_node [ElementNode] Decisions
|
|
737
|
-
# @param doc [Moxml::Document] Document
|
|
738
|
-
# @param global_registry [Hash] Global prefix registry (URI => prefix)
|
|
739
|
-
# @param parent [Moxml::Element, nil] Parent element for namespace inheritance
|
|
740
|
-
# @param options [Hash] Serialization options
|
|
741
|
-
# @param plan [DeclarationPlan] Declaration plan with original namespace URIs
|
|
742
|
-
# @param previous_sibling_had_xmlns_blank [Boolean] Previous sibling had xmlns="" for W3C optimization
|
|
743
|
-
# @return [Moxml::Element] Created node
|
|
744
|
-
def build_xml_node(xml_element, element_node, doc,
|
|
745
|
-
global_registry, parent = nil, options: {}, plan: nil, previous_sibling_had_xmlns_blank: false)
|
|
746
|
-
qualified_name = element_node.qualified_name
|
|
747
|
-
|
|
748
|
-
# Split qualified_name to get prefix and local_name
|
|
749
|
-
# IMPORTANT: Only split on SINGLE colon (namespace prefix separator),
|
|
750
|
-
# not on double colon (::) which is a Ruby module path separator.
|
|
751
|
-
# e.g., "prefix:name" should split to ["prefix", "name"]
|
|
752
|
-
# but "Module::Class" should NOT be split (use whole name as local_name)
|
|
753
|
-
if qualified_name.include?(":") && !qualified_name.include?("::")
|
|
754
|
-
_, local_name = qualified_name.split(":", 2)
|
|
755
|
-
else
|
|
756
|
-
local_name = qualified_name
|
|
757
|
-
end
|
|
758
|
-
|
|
759
|
-
element = doc.create_element(local_name)
|
|
760
|
-
|
|
761
|
-
# Add xmlns declarations FIRST (before adding to parent!)
|
|
762
|
-
# This ensures the element's own namespace is declared before it can inherit parent's
|
|
763
|
-
# Keys: nil = default namespace, "prefix" = prefixed namespace
|
|
764
|
-
original_ns_uris = plan&.original_namespace_uris || {}
|
|
765
|
-
use_prefix_option = options[:use_prefix]
|
|
766
|
-
element_node.hoisted_declarations.each do |key, uri|
|
|
767
|
-
next if uri == "http://www.w3.org/XML/1998/namespace"
|
|
768
|
-
|
|
769
|
-
# Convert FPI to URN if necessary (Nokogiri requires valid URI)
|
|
770
|
-
# Only apply original_ns_uris conversion when preserving original format.
|
|
771
|
-
# When use_prefix is explicitly set, we're using system's format preferences.
|
|
772
|
-
effective_uri = if self.class.fpi?(uri)
|
|
773
|
-
self.class.fpi_to_urn(uri)
|
|
774
|
-
elsif use_prefix_option.nil?
|
|
775
|
-
# Preserving original format - use alias URIs from original
|
|
776
|
-
original_ns_uris[uri] || uri
|
|
777
|
-
else
|
|
778
|
-
# Using explicit format preference - use canonical URIs
|
|
779
|
-
uri
|
|
780
|
-
end
|
|
781
|
-
|
|
782
|
-
if key.nil?
|
|
783
|
-
# Default namespace (xmlns="uri")
|
|
784
|
-
element.add_namespace(nil, effective_uri)
|
|
785
|
-
else
|
|
786
|
-
# Prefixed namespace (xmlns:prefix="uri")
|
|
787
|
-
element.add_namespace(key, effective_uri)
|
|
788
|
-
end
|
|
789
|
-
end
|
|
790
|
-
|
|
791
|
-
# NOW set element's namespace (before adding to parent)
|
|
792
|
-
# This ensures the element uses its own namespace, not inherited from parent
|
|
793
|
-
# CRITICAL: Use the decision's namespace_class from element_node, not the element's namespace_class
|
|
794
|
-
# The decision's namespace_class may be nil (blank namespace) even if the element has a namespace_class
|
|
795
|
-
# set during transformation (e.g., when form: :unqualified is set)
|
|
796
|
-
#
|
|
797
|
-
# IMPORTANT: Use xml_element's namespace_class for the namespace decision
|
|
798
|
-
# The element_node (DeclarationPlan::ElementNode) doesn't have namespace_class
|
|
799
|
-
# because namespace decisions are stored in the xml_element during transformation
|
|
800
|
-
#
|
|
801
|
-
# IMPORTANT: When xml_element.namespace_class is nil, check if this is an explicit decision
|
|
802
|
-
# (blank namespace) or if no decision was made. We can tell by checking if the element has
|
|
803
|
-
# a form attribute set to :unqualified.
|
|
804
|
-
target_namespace_class = xml_element.namespace_class
|
|
805
|
-
# Check if this is an explicit "blank namespace" decision (form: :unqualified)
|
|
806
|
-
# If so, don't fall back to any namespace_class
|
|
807
|
-
if target_namespace_class.nil? && xml_element.respond_to?(:form) && xml_element.form == :unqualified
|
|
808
|
-
# Explicit blank namespace decision - don't set any namespace
|
|
809
|
-
target_namespace_class = nil
|
|
810
|
-
end
|
|
811
|
-
# Note: If no explicit decision, we keep target_namespace_class as nil
|
|
812
|
-
# and don't fall back to anything (no default namespace_class)
|
|
813
|
-
|
|
814
|
-
if target_namespace_class && target_namespace_class != :blank
|
|
815
|
-
# Use the prefix to find the namespace when available.
|
|
816
|
-
# This is more reliable than matching by URI because hoisted_declarations
|
|
817
|
-
# may contain canonical URIs while the actual namespace was added using
|
|
818
|
-
# alias URIs (via original_ns_uris conversion).
|
|
819
|
-
target_prefix = element_node.use_prefix
|
|
820
|
-
target_uri = target_namespace_class.uri
|
|
821
|
-
ns = if target_prefix
|
|
822
|
-
# Find namespace by prefix (most reliable - prefix is unique per element)
|
|
823
|
-
element.in_scope_namespaces.find do |n|
|
|
824
|
-
n.prefix == target_prefix
|
|
825
|
-
end
|
|
826
|
-
else
|
|
827
|
-
# Fall back to URI-based lookup for default namespace
|
|
828
|
-
element.in_scope_namespaces.find do |n|
|
|
829
|
-
n.uri == target_uri && n.prefix.nil?
|
|
830
|
-
end
|
|
831
|
-
end
|
|
832
|
-
if ns
|
|
833
|
-
element.namespace = ns
|
|
834
|
-
elsif target_prefix
|
|
835
|
-
# CRITICAL FIX: Check if namespace is declared on parent before adding locally
|
|
836
|
-
# When parent declares the namespace with the SAME format (prefix or default),
|
|
837
|
-
# child should use parent's namespace declaration without re-declaring it.
|
|
838
|
-
# Also check namespace aliases: if parent declared alias URI and child uses
|
|
839
|
-
# canonical URI (or vice versa), the namespace is already established on parent.
|
|
840
|
-
target_prefix = element_node.use_prefix
|
|
841
|
-
parent_has_namespace = parent_has_matching_namespace?(parent, target_uri,
|
|
842
|
-
target_namespace_class)
|
|
843
|
-
|
|
844
|
-
if parent_has_namespace
|
|
845
|
-
# Parent has the namespace declared - find the matching namespace object
|
|
846
|
-
# Must check all URIs (canonical + aliases) since parent may have declared
|
|
847
|
-
# with an alias URI while child uses canonical (or vice versa)
|
|
848
|
-
matching_uris = if target_namespace_class.respond_to?(:all_uris)
|
|
849
|
-
target_namespace_class.all_uris
|
|
850
|
-
else
|
|
851
|
-
[target_uri]
|
|
852
|
-
end
|
|
853
|
-
parent_ns = if target_prefix
|
|
854
|
-
parent.in_scope_namespaces.find do |n|
|
|
855
|
-
matching_uris.include?(n.uri) && n.prefix == target_prefix
|
|
856
|
-
end
|
|
857
|
-
else
|
|
858
|
-
parent.in_scope_namespaces.find do |n|
|
|
859
|
-
matching_uris.include?(n.uri) && n.prefix.nil?
|
|
860
|
-
end
|
|
861
|
-
end
|
|
862
|
-
|
|
863
|
-
if parent_ns
|
|
864
|
-
# Parent has the SAME format declaration - use parent's namespace
|
|
865
|
-
# Defer setting until after add_child so element is in the tree.
|
|
866
|
-
@deferred_namespace = parent_ns
|
|
867
|
-
nil
|
|
868
|
-
else
|
|
869
|
-
# Parent has different format - add namespace declaration locally
|
|
870
|
-
if target_prefix.nil?
|
|
871
|
-
# Default format: add xmlns="uri" declaration
|
|
872
|
-
element.add_namespace(nil, target_uri)
|
|
873
|
-
# Find the newly added namespace and set it
|
|
874
|
-
ns = element.in_scope_namespaces.find do |n|
|
|
875
|
-
n.uri == target_uri
|
|
876
|
-
end
|
|
877
|
-
else
|
|
878
|
-
# Prefix format: add xmlns:prefix="uri" declaration
|
|
879
|
-
element.add_namespace(target_prefix, target_uri)
|
|
880
|
-
# Find the newly added namespace and set it
|
|
881
|
-
ns = element.in_scope_namespaces.find do |n|
|
|
882
|
-
n.uri == target_uri && n.prefix == target_prefix
|
|
883
|
-
end
|
|
884
|
-
end
|
|
885
|
-
element.namespace = ns if ns
|
|
886
|
-
end
|
|
887
|
-
elsif target_prefix.nil?
|
|
888
|
-
# Default format: add xmlns="uri" declaration
|
|
889
|
-
element.add_namespace(nil, target_uri)
|
|
890
|
-
# Find the newly added namespace and set it
|
|
891
|
-
ns = element.in_scope_namespaces.find do |n|
|
|
892
|
-
n.uri == target_uri
|
|
893
|
-
end
|
|
894
|
-
element.namespace = ns if ns
|
|
895
|
-
else
|
|
896
|
-
# Prefix format: add xmlns:prefix="uri" declaration
|
|
897
|
-
element.add_namespace(target_prefix, target_uri)
|
|
898
|
-
# Find the newly added namespace and set it
|
|
899
|
-
ns = element.in_scope_namespaces.find do |n|
|
|
900
|
-
n.uri == target_uri && n.prefix == target_prefix
|
|
901
|
-
end
|
|
902
|
-
element.namespace = ns if ns
|
|
903
|
-
end
|
|
904
|
-
end
|
|
905
|
-
end
|
|
906
|
-
|
|
907
|
-
# Add to parent AFTER namespace is set
|
|
908
|
-
# This prevents the element from inheriting parent's namespace before declaring its own
|
|
909
|
-
parent&.add_child(element)
|
|
910
|
-
|
|
911
|
-
# CRITICAL FIX: Set deferred namespace after adding to parent
|
|
912
|
-
# This allows the element to use parent's namespace declaration without re-declaring it
|
|
913
|
-
if @deferred_namespace
|
|
914
|
-
element.namespace = @deferred_namespace
|
|
915
|
-
@deferred_namespace = nil
|
|
916
|
-
end
|
|
917
|
-
|
|
918
|
-
# CRITICAL FIX: Handle blank namespace elements
|
|
919
|
-
# When element has no namespace_class, it should remain in blank namespace
|
|
920
|
-
# Even if parent uses prefix format, the child should NOT inherit parent's namespace
|
|
921
|
-
# Also applies when form: :unqualified is set (element should be in blank namespace)
|
|
922
|
-
if !xml_element.namespace_class || xml_element.namespace_class == :blank ||
|
|
923
|
-
(xml_element.respond_to?(:form) && xml_element.form == :unqualified)
|
|
924
|
-
# Explicitly set element to blank namespace (no namespace)
|
|
925
|
-
# This prevents the child from inheriting parent's namespace
|
|
926
|
-
element.namespace = nil
|
|
927
|
-
end
|
|
928
|
-
|
|
929
|
-
# W3C Compliance: Add xmlns="" if element is in blank namespace
|
|
930
|
-
# and needs to opt out of parent's default namespace
|
|
931
|
-
# W3C Optimization: Only first sibling needs xmlns="", subsequent inherit
|
|
932
|
-
# Only apply optimization when pretty: true is set
|
|
933
|
-
if element_node.needs_xmlns_blank && (options[:pretty] ? !previous_sibling_had_xmlns_blank : true)
|
|
934
|
-
# Add xmlns="" as an attribute (Nokogiri-specific)
|
|
935
|
-
element["xmlns"] = ""
|
|
936
|
-
end
|
|
937
|
-
|
|
938
|
-
# Add regular attributes (PARALLEL TRAVERSAL by index)
|
|
939
|
-
apply_plan_attributes(xml_element, element_node, element)
|
|
940
|
-
|
|
941
|
-
# Check if element was created from nil value with render_nil option
|
|
942
|
-
# Add xsi:nil="true" attribute for W3C compliance
|
|
943
|
-
if xml_element.respond_to?(:xsi_nil) && xml_element.xsi_nil
|
|
944
|
-
element["xsi:nil"] = true
|
|
945
|
-
end
|
|
946
|
-
|
|
947
|
-
# Add schema_location attribute from ElementNode if present
|
|
948
|
-
element_node.schema_location_attr&.each do |attr_name, attr_value|
|
|
949
|
-
element[attr_name] = attr_value
|
|
950
|
-
end
|
|
951
|
-
|
|
952
|
-
# Handle raw content (map_all directive)
|
|
953
|
-
# If @raw_content exists, parse and add as XML fragment
|
|
954
|
-
# NOTE: We do NOT return early here because the element may have
|
|
955
|
-
# children that also need to be processed. Raw content should be
|
|
956
|
-
# added alongside children, not replace them.
|
|
957
|
-
if xml_element.respond_to?(:raw_content)
|
|
958
|
-
raw_content = xml_element.raw_content
|
|
959
|
-
if raw_content && !raw_content.to_s.empty?
|
|
960
|
-
# Parse raw XML content and add as children using moxml
|
|
961
|
-
# Use inner_xml= approach: parse wrapper, then move children to element
|
|
962
|
-
parsed_fragment = doc.context.parse("<__root__>#{raw_content}</__root__>")
|
|
963
|
-
parsed_fragment.root&.children&.each do |child_node|
|
|
964
|
-
element.add_child(child_node)
|
|
965
|
-
end
|
|
966
|
-
# Do NOT return early - continue to process element's children
|
|
967
|
-
end
|
|
968
|
-
end
|
|
969
|
-
|
|
970
|
-
# Recursively build children (PARALLEL TRAVERSAL by index)
|
|
971
|
-
# Pass THIS element as parent so children can inherit namespaces
|
|
972
|
-
child_element_index = 0
|
|
973
|
-
previous_sibling_had_xmlns_blank = false
|
|
974
|
-
xml_element.children.each do |xml_child| # rubocop:disable Metrics/BlockLength
|
|
975
|
-
# Entity reference nodes are preserved via the marker
|
|
976
|
-
# preprocessing approach in NokogiriAdapter.parse.
|
|
977
|
-
if xml_child.is_a?(Lutaml::Xml::NokogiriElement) &&
|
|
978
|
-
xml_child.adapter_node.respond_to?(:entity_reference?) &&
|
|
979
|
-
xml_child.adapter_node.entity_reference?
|
|
980
|
-
entity_node = doc.create_entity_reference(xml_child.adapter_node.name)
|
|
981
|
-
element.add_child(entity_node)
|
|
982
|
-
next
|
|
983
|
-
elsif xml_child.is_a?(Lutaml::Xml::DataModel::XmlElement)
|
|
984
|
-
child_node = element_node.element_nodes[child_element_index]
|
|
985
|
-
child_element_index += 1
|
|
986
|
-
|
|
987
|
-
# Recurse - child auto-adds itself to element (parent)
|
|
988
|
-
# Pass previous_sibling_had_xmlns_blank for W3C optimization
|
|
989
|
-
build_xml_node(xml_child, child_node, doc, global_registry, element,
|
|
990
|
-
options: options, plan: plan,
|
|
991
|
-
previous_sibling_had_xmlns_blank: previous_sibling_had_xmlns_blank)
|
|
992
|
-
# Track if this child had xmlns="" for next sibling
|
|
993
|
-
# Blank namespace children get xmlns="" to opt out of parent's default namespace
|
|
994
|
-
if !xml_child.namespace_class && xml_element.namespace_class
|
|
995
|
-
previous_sibling_had_xmlns_blank = true
|
|
996
|
-
end
|
|
997
|
-
elsif xml_child.is_a?(String)
|
|
998
|
-
add_content_node(element, xml_child, doc,
|
|
999
|
-
cdata: xml_element.cdata && !xml_child.strip.empty?)
|
|
1000
|
-
elsif xml_child.is_a?(::Lutaml::Xml::DataModel::XmlComment)
|
|
1001
|
-
comment_node = doc.create_comment(xml_child.content)
|
|
1002
|
-
element.add_child(comment_node)
|
|
1003
|
-
end
|
|
1004
|
-
end
|
|
1005
|
-
|
|
1006
|
-
# Add text content AFTER child elements
|
|
1007
|
-
if xml_element.text_content
|
|
1008
|
-
add_content_node(element, xml_element.text_content.to_s,
|
|
1009
|
-
doc, cdata: xml_element.cdata)
|
|
1010
|
-
end
|
|
1011
|
-
|
|
1012
|
-
element
|
|
1013
|
-
end
|
|
1014
|
-
|
|
1015
|
-
# Standard XML predefined entities — these are always resolved by the
|
|
1016
|
-
# XML parser and must NOT be turned into EntityReference nodes during
|
|
1017
|
-
# serialization. If we created an EntityReference for e.g. "lt", the
|
|
1018
|
-
# output would render as `<` instead of preserving the literal text
|
|
1019
|
-
# `<`, which corrupts double-encoded content like `&lt;`.
|
|
1020
|
-
STANDARD_XML_ENTITIES = %w[lt gt amp apos quot].freeze
|
|
1021
|
-
|
|
1022
|
-
# Add text content to an element, preserving entity reference patterns.
|
|
1023
|
-
# Only non-standard named entities (e.g. ©, , —) are
|
|
1024
|
-
# promoted to EntityReference nodes. Standard XML entities, numeric
|
|
1025
|
-
# character references, and all other text are added as plain text nodes
|
|
1026
|
-
# so the XML serializer handles proper escaping.
|
|
1027
|
-
#
|
|
1028
|
-
# Uses Moxml's doc.create_text/create_entity_reference for node creation.
|
|
1029
|
-
#
|
|
1030
|
-
# @param element [Moxml::Element] Target element
|
|
1031
|
-
# @param text [String] Text content possibly containing entity references
|
|
1032
|
-
# @param doc [Moxml::Document] Document for node creation
|
|
1033
|
-
def add_text_with_entities(element, text, doc)
|
|
1034
|
-
entity_pattern = /(&(?:\w+|#\d+|#x[\da-fA-F]+);)/
|
|
1035
|
-
parts = text.to_s.split(entity_pattern, -1)
|
|
1036
|
-
parts.each do |part|
|
|
1037
|
-
next if part.empty?
|
|
1038
|
-
|
|
1039
|
-
# Only non-standard named entities become EntityReference nodes.
|
|
1040
|
-
# Standard XML entities (lt, gt, amp, apos, quot) and numeric
|
|
1041
|
-
# character references (&#NNN;, &#xHHH;) must remain as text so
|
|
1042
|
-
# the serializer escapes them correctly (e.g. < → &lt;).
|
|
1043
|
-
# Entity names must start with a letter per the XML specification,
|
|
1044
|
-
# so patterns like &1; are NOT entity references.
|
|
1045
|
-
# NOTE: use #match (not #match?) because match? does not set $1.
|
|
1046
|
-
if (m = part.match(/\A&([a-zA-Z]\w*);\z/)) && !STANDARD_XML_ENTITIES.include?(m[1])
|
|
1047
|
-
entity_name = m[1]
|
|
1048
|
-
entity_node = doc.create_entity_reference(entity_name)
|
|
1049
|
-
element.add_child(entity_node)
|
|
1050
|
-
else
|
|
1051
|
-
text_node = doc.create_text(part)
|
|
1052
|
-
element.add_child(text_node)
|
|
1053
|
-
end
|
|
1054
|
-
end
|
|
1055
|
-
end
|
|
1056
|
-
|
|
1057
|
-
# Check if parent element has a matching namespace declaration in scope.
|
|
1058
|
-
# Uses Moxml's in_scope_namespaces for namespace query.
|
|
1059
|
-
def parent_has_matching_namespace?(parent, target_uri,
|
|
1060
|
-
target_namespace_class)
|
|
1061
|
-
return false unless parent
|
|
1062
|
-
|
|
1063
|
-
parent_uris = parent.in_scope_namespaces.map(&:uri)
|
|
1064
|
-
|
|
1065
|
-
# Check exact match first
|
|
1066
|
-
return true if parent_uris.include?(target_uri)
|
|
1067
|
-
|
|
1068
|
-
# Check if parent declared an alias URI for the same namespace
|
|
1069
|
-
if target_namespace_class.respond_to?(:all_uris)
|
|
1070
|
-
all_ns_uris = target_namespace_class.all_uris
|
|
1071
|
-
return parent_uris.any? { |href| all_ns_uris.include?(href) }
|
|
1072
|
-
end
|
|
1073
|
-
|
|
1074
|
-
false
|
|
1075
|
-
end
|
|
1076
|
-
|
|
1077
|
-
# Post-process XML string to fix OOXML format issues.
|
|
1078
|
-
# Handles two normalization rules:
|
|
1079
|
-
# 1. Boolean elements: <w:elem w:val="true"/> -> <w:elem/>
|
|
1080
|
-
# 2. XML namespace attribute: <w:t w:xml:space=...> -> <w:t xml:space=...>
|
|
1081
|
-
#
|
|
1082
|
-
# @param xml [String] The XML string to process
|
|
1083
|
-
# @return [String] The processed XML string
|
|
1084
|
-
# OOXML boolean element names: self-closing elements where presence = true.
|
|
1085
|
-
# This is a whitelist of known boolean element names to avoid incorrectly
|
|
1086
|
-
# transforming non-boolean elements like numId, colSpan, etc.
|
|
1087
|
-
OOXML_BOOLEAN_ELEMENTS = %w[
|
|
1088
|
-
b i strike bCs iCs smallCaps caps vanish noProof
|
|
1089
|
-
shadow emboss imprint keepNext keepLines outline
|
|
1090
|
-
tblHeader cantSplit contextualSpacing highlight
|
|
1091
|
-
rPr pPr trPr tcPr
|
|
1092
|
-
].freeze
|
|
1093
|
-
|
|
1094
|
-
def fix_ooxml_format(xml)
|
|
1095
|
-
# Build regex pattern that only matches known boolean element names
|
|
1096
|
-
bool_elem_pattern = OOXML_BOOLEAN_ELEMENTS.join("|")
|
|
1097
|
-
|
|
1098
|
-
# Fix self-closing: <ns:elem w:val="true"/> or <ns:elem w:val="1"/>
|
|
1099
|
-
# Only for known boolean elements
|
|
1100
|
-
xml = xml.gsub(
|
|
1101
|
-
/<([a-zA-Z][a-zA-Z0-9]*):(#{bool_elem_pattern})(\s+w:val=")(true|1)("\s*\/?>)/,
|
|
1102
|
-
) { "<#{$1}:#{$2}/>" }
|
|
1103
|
-
|
|
1104
|
-
# Fix with content: <ns:elem w:val="true">true</ns:elem> or <ns:elem w:val="1">1</ns:elem>
|
|
1105
|
-
xml = xml.gsub(
|
|
1106
|
-
/<([a-zA-Z][a-zA-Z0-9]*):(#{bool_elem_pattern})(\s+w:val=")(true|1)(">)true<\/\1:\2>/,
|
|
1107
|
-
) { "<#{$1}:#{$2}>" }
|
|
1108
|
-
|
|
1109
|
-
# Fix content-only: <ns:elem>true</ns:elem> -> <ns:elem/>
|
|
1110
|
-
# For elements that serialize boolean value as text content
|
|
1111
|
-
xml = xml.gsub(
|
|
1112
|
-
/<([a-zA-Z][a-zA-Z0-9]*):(#{bool_elem_pattern})>true<\/\1:\2>/,
|
|
1113
|
-
) { "<#{$1}:#{$2}/>" }
|
|
1114
|
-
|
|
1115
|
-
# Fix xml:space attribute: <w:t w:xml:space=...> -> <w:t xml:space=...>
|
|
1116
|
-
# The xml: attribute belongs to the xml: namespace, not w:
|
|
1117
|
-
xml.gsub(/\bw:xml:space=/, "xml:space=")
|
|
1118
|
-
end
|
|
11
|
+
MOXML_ADAPTER = Moxml::Adapter::Nokogiri
|
|
12
|
+
BUILDER_CLASS = Builder::Nokogiri
|
|
13
|
+
PARSED_ELEMENT_CLASS = Lutaml::Xml::NokogiriElement
|
|
1119
14
|
end
|
|
1120
15
|
end
|
|
1121
16
|
end
|