Class: Lutaml::Xml::XmlElement

Inherits:
Object
  • Object
show all
Defined in:
lib/lutaml/xml/xml_element.rb

Direct Known Subclasses

AdapterElement

Constant Summary collapse

XML_NAMESPACE_URI =
"http://www.w3.org/XML/1998/namespace"
TEXT_NODE_NAME =

Performance: Frozen string constants for frequently used values

"text"
CDATA_NODE_NAME =
"#cdata-section"
XMLNS_PREFIX =
"xmlns"
EMPTY_NAMESPACES =

Performance: Frozen empty hash to reduce allocations

{}.freeze
EMPTY_CHILDREN_ARRAY =

Performance: Frozen empty array for child lookups

[].freeze
NODE_TYPES =

Node types for XML elements

  • :element - regular XML element

  • :text - text content node

  • :cdata - CDATA section

  • :comment - XML comment

  • :processing_instruction - processing instruction

%i[element text cdata comment processing_instruction].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(node, attributes = {}, children = [], text = nil, name: nil, parent_document: nil, namespace_prefix: nil, default_namespace: nil, explicit_no_namespace: false, node_type: nil) ⇒ XmlElement

Returns a new instance of XmlElement.



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
# File 'lib/lutaml/xml/xml_element.rb', line 84

def initialize(
  node,
attributes = {},
children = [],
text = nil,
name: nil,
parent_document: nil,
namespace_prefix: nil,
default_namespace: nil,
explicit_no_namespace: false,
node_type: nil
)
  @name = name
  @namespace_prefix = namespace_prefix
  @namespace_prefix_explicit = !namespace_prefix.nil? && !namespace_prefix.empty?
  @attributes = attributes
  @children = children
  @text = text
  @parent_document = parent_document
  @default_namespace = default_namespace
  @explicit_no_namespace = explicit_no_namespace
  # Set node_type, defaulting to :element
  # Backward compatibility: infer from name if node_type not provided
  @node_type = node_type || infer_node_type_from_name(name)

  self.adapter_node = node
end

Instance Attribute Details

#adapter_nodeObject

Returns the value of attribute adapter_node.



29
30
31
# File 'lib/lutaml/xml/xml_element.rb', line 29

def adapter_node
  @adapter_node
end

#attributesObject (readonly)

Returns the value of attribute attributes.



27
28
29
# File 'lib/lutaml/xml/xml_element.rb', line 27

def attributes
  @attributes
end

#childrenObject

Returns the value of attribute children.



27
28
29
# File 'lib/lutaml/xml/xml_element.rb', line 27

def children
  @children
end

#namespace_prefixObject (readonly)

Returns the value of attribute namespace_prefix.



27
28
29
# File 'lib/lutaml/xml/xml_element.rb', line 27

def namespace_prefix
  @namespace_prefix
end

#namespace_prefix_explicitObject (readonly)

Returns the value of attribute namespace_prefix_explicit.



27
28
29
# File 'lib/lutaml/xml/xml_element.rb', line 27

def namespace_prefix_explicit
  @namespace_prefix_explicit
end

#node_typeObject (readonly)

Returns the value of attribute node_type.



27
28
29
# File 'lib/lutaml/xml/xml_element.rb', line 27

def node_type
  @node_type
end

#order_cache=(value) ⇒ Object (writeonly)

Cache for order method - invalidated when children change



32
33
34
# File 'lib/lutaml/xml/xml_element.rb', line 32

def order_cache=(value)
  @order_cache = value
end

#parent_documentObject (readonly)

Returns the value of attribute parent_document.



27
28
29
# File 'lib/lutaml/xml/xml_element.rb', line 27

def parent_document
  @parent_document
end

#processing_instructionsObject

Returns the value of attribute processing_instructions.



29
30
31
# File 'lib/lutaml/xml/xml_element.rb', line 29

def processing_instructions
  @processing_instructions
end

Class Method Details

.detect_explicit_no_namespace(has_empty_xmlns:, node_namespace_nil:) ⇒ Boolean

Detect if xmlns=“” is explicitly set (W3C explicit no namespace) This is a helper method for adapters to use during element initialization

Parameters:

  • has_empty_xmlns (Boolean)

    true if xmlns=“” is present

  • node_namespace_nil (Boolean)

    true if the node has no namespace

Returns:

  • (Boolean)

    true if both conditions met (explicit no namespace)



60
61
62
63
# File 'lib/lutaml/xml/xml_element.rb', line 60

def self.detect_explicit_no_namespace(has_empty_xmlns:,
  node_namespace_nil:)
  has_empty_xmlns && node_namespace_nil
end

.fpi?(uri) ⇒ Boolean

Detect if a string is an FPI (Formal Public Identifier), not a valid namespace URI. FPIs start with -// or +// (SGML-style, not a URI scheme).

Returns:

  • (Boolean)


80
81
82
# File 'lib/lutaml/xml/xml_element.rb', line 80

def self.fpi?(uri)
  uri.is_a?(String) && uri.start_with?("-//", "+//")
end

.fpi_to_urn(fpi) ⇒ Object

Convert a Formal Public Identifier (FPI) to a URN per RFC 3151. FPI examples: “-//OASIS//DTD XML Exchange Table Model 19990315//EN” Returns nil if the string is not an FPI.

RFC 3151 format: urn:publicid:prefix:+/-//registrant//description//language// Conversion: replace spaces with +, prepend “urn:publicid:”



71
72
73
74
75
76
# File 'lib/lutaml/xml/xml_element.rb', line 71

def self.fpi_to_urn(fpi)
  return nil unless fpi.is_a?(String) && fpi.start_with?("-//", "+//")

  # Replace spaces with + per RFC 3151
  "urn:publicid:#{fpi.gsub(' ', '+')}"
end

Instance Method Details

#[](name) ⇒ Object



355
356
357
# File 'lib/lutaml/xml/xml_element.rb', line 355

def [](name)
  find_attribute_value(name) || find_children_by_name(name)
end

#add_namespace(namespace) ⇒ Object



245
246
247
248
# File 'lib/lutaml/xml/xml_element.rb', line 245

def add_namespace(namespace)
  @namespaces ||= {}
  @namespaces[namespace.prefix] = namespace
end

#attribute_is_namespace?(name) ⇒ Boolean

Returns:

  • (Boolean)


241
242
243
# File 'lib/lutaml/xml/xml_element.rb', line 241

def attribute_is_namespace?(name)
  name.to_s.start_with?(XMLNS_PREFIX)
end

#cdataObject



310
311
312
313
314
315
# File 'lib/lutaml/xml/xml_element.rb', line 310

def cdata
  return @text if children.empty?
  return cdata_children.map(&:text) if children.count > 1

  cdata_children.map(&:text).join
end

#cdata?Boolean

Check if this is a CDATA section

Returns:

  • (Boolean)


136
137
138
# File 'lib/lutaml/xml/xml_element.rb', line 136

def cdata?
  @node_type == :cdata
end

#cdata_childrenObject



317
318
319
# File 'lib/lutaml/xml/xml_element.rb', line 317

def cdata_children
  children.select(&:cdata?)
end

#comment?Boolean

Check if this is a comment node

Returns:

  • (Boolean)


141
142
143
# File 'lib/lutaml/xml/xml_element.rb', line 141

def comment?
  @node_type == :comment
end

#default_namespaceObject



250
251
252
# File 'lib/lutaml/xml/xml_element.rb', line 250

def default_namespace
  namespaces[nil] || @parent_document&.namespaces&.dig(nil)
end

#documentObject



191
192
193
# File 'lib/lutaml/xml/xml_element.rb', line 191

def document
  Document.new(self)
end

#element?Boolean

Check if this is a regular element (not text/cdata/comment)

Returns:

  • (Boolean)


146
147
148
# File 'lib/lutaml/xml/xml_element.rb', line 146

def element?
  @node_type == :element
end

#element_childrenObject

Get child elements (excluding text, strings, and symbols) Performance: Cached to avoid repeated filtering per rule



327
328
329
330
331
332
333
334
# File 'lib/lutaml/xml/xml_element.rb', line 327

def element_children
  return @element_children if defined?(@element_children)

  @element_children = children.reject do |child|
    child.is_a?(String) || child.is_a?(Symbol) ||
      (child.is_a?(XmlElement) && child.text?)
  end
end

#element_children_indexObject

Index of element children by namespaced_name for O(1) lookup Returns nil for single-child elements (not worth indexing) Performance: Cached to avoid repeated index building



339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
# File 'lib/lutaml/xml/xml_element.rb', line 339

def element_children_index
  return @element_children_index if defined?(
    @element_children_index
  )

  ec = element_children
  @element_children_index = if ec.size > 1
                              index = {}
                              ec.each do |child|
                                key = child.namespaced_name
                                (index[key] ||= []) << child
                              end
                              index
                            end
end

#ensure_attribute_indexObject

Performance: Build index for O(1) attribute lookups by namespaced_name



375
376
377
378
379
380
381
382
# File 'lib/lutaml/xml/xml_element.rb', line 375

def ensure_attribute_index
  return if @attribute_index

  @attribute_index = {}
  attributes.each_value do |attr|
    @attribute_index[attr.namespaced_name] = attr.value
  end
end

#ensure_children_indexObject

Performance: Build index for O(1) child lookups by name Called once per element, then reused for all lookups



386
387
388
389
390
391
392
393
394
395
# File 'lib/lutaml/xml/xml_element.rb', line 386

def ensure_children_index
  return if @children_index

  @children_index = {}
  @children.each do |child|
    key = child.namespaced_name
    @children_index[key] ||= []
    @children_index[key] << child
  end
end

#find_attribute_value(attribute_name) ⇒ Object



359
360
361
362
363
364
365
366
367
368
369
370
371
372
# File 'lib/lutaml/xml/xml_element.rb', line 359

def find_attribute_value(attribute_name)
  # Performance: Use hash index for O(1) lookup instead of linear scan
  ensure_attribute_index

  if attribute_name.is_a?(Array)
    attribute_name.each do |name|
      val = @attribute_index[name]
      return val unless val.nil?
    end
    nil
  else
    @attribute_index[attribute_name]
  end
end

#find_child_by_name(name) ⇒ Object



410
411
412
413
414
415
416
417
418
419
420
421
422
# File 'lib/lutaml/xml/xml_element.rb', line 410

def find_child_by_name(name)
  ensure_children_index

  if name.is_a?(Array)
    name.each do |n|
      found = @children_index[n]&.first
      return found if found
    end
    nil
  else
    @children_index[name]&.first
  end
end

#find_children_by_name(name) ⇒ Object

Find children by namespaced name using indexed lookup Performance: O(1) for single name, O(k) for k names



399
400
401
402
403
404
405
406
407
408
# File 'lib/lutaml/xml/xml_element.rb', line 399

def find_children_by_name(name)
  ensure_children_index

  if name.is_a?(Array)
    # Multiple names: collect from index
    name.flat_map { |n| @children_index[n] || EMPTY_CHILDREN_ARRAY }
  else
    @children_index[name] || EMPTY_CHILDREN_ARRAY
  end
end

#nameObject



155
156
157
158
159
# File 'lib/lutaml/xml/xml_element.rb', line 155

def name
  return @name unless namespace_prefix

  @prefixed_name ||= "#{namespace_prefix}:#{@name}" # rubocop:disable Naming/MemoizedInstanceVariableName
end

#namespaceObject



216
217
218
219
220
221
222
223
224
# File 'lib/lutaml/xml/xml_element.rb', line 216

def namespace
  return @namespace if defined?(@namespace)

  @namespace = if namespace_prefix
                 namespaces[namespace_prefix]
               else
                 default_namespace
               end
end

#namespace_scope_configObject



48
49
50
# File 'lib/lutaml/xml/xml_element.rb', line 48

def namespace_scope_config
  nil
end

#namespace_uriString?

Get the namespace URI of this element.

Returns the URI string for namespace-aware type resolution. Returns nil if the element has no namespace.

Returns:

  • (String, nil)

    The namespace URI or nil



232
233
234
235
236
237
238
239
# File 'lib/lutaml/xml/xml_element.rb', line 232

def namespace_uri
  return @namespace_uri if defined?(@namespace_uri)

  @namespace_uri = begin
    ns = namespace
    ns&.uri
  end
end

#namespaced_nameObject



161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/lutaml/xml/xml_element.rb', line 161

def namespaced_name
  return @namespaced_name if defined?(@namespaced_name)

  @namespaced_name = begin
    return @name if text?
    # If xmlns="" was explicitly set, element has NO namespace
    return @name if @explicit_no_namespace

    # Priority order for namespace resolution:
    # 1. If has explicit prefix, use namespaces[prefix]
    # 2. If has @default_namespace, use it (preferred for default ns)
    # 3. Fall back to namespaces[nil] if exists
    # 4. Return unprefixed name
    ns = namespaces
    if namespace_prefix && ns[namespace_prefix]
      "#{ns[namespace_prefix].uri}:#{@name}"
    elsif @default_namespace
      "#{@default_namespace}:#{@name}"
    elsif ns[nil]
      "#{ns[nil].uri}:#{@name}"
    else
      @name
    end
  end
end

#namespacesObject



195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# File 'lib/lutaml/xml/xml_element.rb', line 195

def namespaces
  # When @namespaces is non-empty, return it directly (element has own declarations)
  # When @namespaces is nil or empty, fall back to parent's in-scope namespaces
  # This supports the new namespace_definitions approach where each element only
  # stores its own declarations, and child elements inherit from parent
  #
  # NOTE: Not cached here because Oga::Element#initialize calls this before
  # super() sets @parent_document, which would cache an incorrect empty result.
  # The hot path (namespaced_name) is cached separately.
  if @namespaces&.any?
    @namespaces
  else
    @parent_document&.namespaces || EMPTY_NAMESPACES
  end
end

#nil_element?Boolean

Returns:

  • (Boolean)


428
429
430
# File 'lib/lutaml/xml/xml_element.rb', line 428

def nil_element?
  find_attribute_value("xsi:nil") == "true"
end

#orderObject



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
# File 'lib/lutaml/xml/xml_element.rb', line 254

def order
  return @order_cache if @order_cache

  @order_cache = children.filter_map do |child|
    if child.text?
      # Skip whitespace-only text nodes (formatting between elements).
      # Significant text in mixed content will contain non-whitespace.
      next if child.text.nil? || child.text.strip.empty?

      # For text nodes:
      # - name is "text" for backward compatibility with tests
      # - text_content contains the actual text for round-trip serialization
      # - node_type explicitly marks this as a text node
      Lutaml::Xml::Element.new("Text", "text",
                               text_content: child.text,
                               node_type: :text)
    elsif child.cdata?
      # For CDATA sections:
      # - name is "#cdata-section" for backward compatibility
      # - text_content contains the actual CDATA content
      # - node_type explicitly marks this as CDATA
      Lutaml::Xml::Element.new("Text", "#cdata-section",
                               text_content: child.text,
                               node_type: :cdata)
    elsif child.comment?
      # Skip comments - they're not part of schema element order
      nil
    else
      # For regular elements:
      # - name is the actual element name
      # - node_type explicitly marks this as an element
      # - namespace_uri and namespace_prefix preserve namespace info for rule matching
      Lutaml::Xml::Element.new("Element", child.unprefixed_name,
                               node_type: :element,
                               namespace_uri: child.namespace_uri,
                               namespace_prefix: child.namespace_prefix)
    end
  end
end

#original_namespace_uriObject



44
45
46
# File 'lib/lutaml/xml/xml_element.rb', line 44

def original_namespace_uri
  nil
end

#original_xml_elementObject



40
41
42
# File 'lib/lutaml/xml/xml_element.rb', line 40

def original_xml_element
  nil
end

#own_namespacesObject



211
212
213
214
# File 'lib/lutaml/xml/xml_element.rb', line 211

def own_namespaces
  # Return only this element's own namespace declarations (not inherited)
  @namespaces || EMPTY_NAMESPACES
end

#pretty_print_instance_variablesObject

This tells which attributes to pretty print, So we remove the so much repeatative output.



124
125
126
127
# File 'lib/lutaml/xml/xml_element.rb', line 124

def pretty_print_instance_variables
  (instance_variables - %i[@adapter_node @parent_document
                           @children_index]).sort
end

#processing_instruction?Boolean

Check if this is a processing instruction

Returns:

  • (Boolean)


151
152
153
# File 'lib/lutaml/xml/xml_element.rb', line 151

def processing_instruction?
  @node_type == :processing_instruction
end

#rootObject



294
295
296
# File 'lib/lutaml/xml/xml_element.rb', line 294

def root
  self
end

#textObject



303
304
305
306
307
308
# File 'lib/lutaml/xml/xml_element.rb', line 303

def text
  return @text if children.empty?
  return text_children.map(&:text) if children.count > 1

  text_children.map(&:text).join
end

#text?Boolean

Check if this is a text content node Uses explicit node_type instead of name-based detection

Returns:

  • (Boolean)


131
132
133
# File 'lib/lutaml/xml/xml_element.rb', line 131

def text?
  @node_type == :text
end

#text_childrenObject



321
322
323
# File 'lib/lutaml/xml/xml_element.rb', line 321

def text_children
  children.select { |child| child.text? && !child.cdata? }
end

#to_hObject



424
425
426
# File 'lib/lutaml/xml/xml_element.rb', line 424

def to_h
  document.to_h
end

#unprefixed_nameObject



187
188
189
# File 'lib/lutaml/xml/xml_element.rb', line 187

def unprefixed_name
  @name
end

#xml_declarationObject

Default: no XML declaration. Document wrappers override this.



299
300
301
# File 'lib/lutaml/xml/xml_element.rb', line 299

def xml_declaration
  nil
end

#xml_namespace_prefixObject

Stubs for DataModel::XmlElement compatibility. These return nil on adapter XmlElements (the data lives on DataModel::XmlElement).



36
37
38
# File 'lib/lutaml/xml/xml_element.rb', line 36

def xml_namespace_prefix
  nil
end