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, attribute_order: 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
111
112
# 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,
attribute_order: nil
)
  @name = name
  @namespace_prefix = namespace_prefix
  @namespace_prefix_explicit = !namespace_prefix.nil? && !namespace_prefix.empty?
  @attributes = attributes
  @attribute_order = attribute_order
  @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

#attribute_orderObject (readonly)

Returns the value of attribute attribute_order.



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

def attribute_order
  @attribute_order
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



358
359
360
# File 'lib/lutaml/xml/xml_element.rb', line 358

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

#add_namespace(namespace) ⇒ Object



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

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

#attribute_is_namespace?(name) ⇒ Boolean

Returns:

  • (Boolean)


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

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

#cdataObject



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

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

  cdata_children.map(&:text).join
end

#cdata?Boolean

Check if this is a CDATA section

Returns:

  • (Boolean)


138
139
140
# File 'lib/lutaml/xml/xml_element.rb', line 138

def cdata?
  @node_type == :cdata
end

#cdata_childrenObject



319
320
321
# File 'lib/lutaml/xml/xml_element.rb', line 319

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

#comment?Boolean

Check if this is a comment node

Returns:

  • (Boolean)


143
144
145
# File 'lib/lutaml/xml/xml_element.rb', line 143

def comment?
  @node_type == :comment
end

#default_namespaceObject



256
257
258
# File 'lib/lutaml/xml/xml_element.rb', line 256

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

#documentObject



196
197
198
# File 'lib/lutaml/xml/xml_element.rb', line 196

def document
  Document.new(self)
end

#element?Boolean

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

Returns:

  • (Boolean)


148
149
150
# File 'lib/lutaml/xml/xml_element.rb', line 148

def element?
  @node_type == :element
end

#element_childrenObject

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



329
330
331
332
333
334
335
336
337
# File 'lib/lutaml/xml/xml_element.rb', line 329

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? || child.processing_instruction?))
  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



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

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



378
379
380
381
382
383
384
385
# File 'lib/lutaml/xml/xml_element.rb', line 378

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



389
390
391
392
393
394
395
396
397
398
399
400
# File 'lib/lutaml/xml/xml_element.rb', line 389

def ensure_children_index
  return if @children_index

  @children_index = {}
  @children.each do |child|
    next if child.is_a?(XmlElement) && child.processing_instruction?

    key = child.namespaced_name
    @children_index[key] ||= []
    @children_index[key] << child
  end
end

#find_attribute_value(attribute_name) ⇒ Object



362
363
364
365
366
367
368
369
370
371
372
373
374
375
# File 'lib/lutaml/xml/xml_element.rb', line 362

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



415
416
417
418
419
420
421
422
423
424
425
426
427
# File 'lib/lutaml/xml/xml_element.rb', line 415

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



404
405
406
407
408
409
410
411
412
413
# File 'lib/lutaml/xml/xml_element.rb', line 404

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



157
158
159
160
161
# File 'lib/lutaml/xml/xml_element.rb', line 157

def name
  return @name unless namespace_prefix

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

#namespaceObject



221
222
223
224
225
226
227
228
229
# File 'lib/lutaml/xml/xml_element.rb', line 221

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



237
238
239
240
241
242
243
244
245
# File 'lib/lutaml/xml/xml_element.rb', line 237

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

  @namespace_uri = begin
    ns = namespace
    uri = ns&.uri
    uri ? -uri : nil
  end
end

#namespaced_nameObject



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

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
    raw = 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
    # Deduplicate the string to reduce GC pressure from repeated
    # namespace URIs across thousands of elements.
    -raw
  end
end

#namespacesObject



200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# File 'lib/lutaml/xml/xml_element.rb', line 200

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)


433
434
435
# File 'lib/lutaml/xml/xml_element.rb', line 433

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

#orderObject



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

def order
  return @order_cache if @order_cache

  @order_cache = children.filter_map do |child|
    if child.cdata?
      Lutaml::Xml::Element.new("Text", "#cdata-section",
                               text_content: child.text,
                               node_type: :cdata)
    elsif child.text?
      next if child.text.nil?

      Lutaml::Xml::Element.new("Text", "text",
                               text_content: child.text,
                               node_type: :text)
    elsif child.comment?
      Lutaml::Xml::Element.new("Comment", "comment",
                               text_content: child.text,
                               node_type: :comment)
    elsif child.processing_instruction?
      Lutaml::Xml::Element.new("ProcessingInstruction",
                               child.unprefixed_name,
                               text_content: child.text,
                               node_type: :processing_instruction)
    else
      Lutaml::Xml::Element.new("Element", child.unprefixed_name,
                               node_type: :element,
                               namespace_uri: child.namespace_uri,
                               namespace_prefix: child.namespace_prefix)
    end
  end.each(&:freeze).freeze
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



216
217
218
219
# File 'lib/lutaml/xml/xml_element.rb', line 216

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.



126
127
128
129
# File 'lib/lutaml/xml/xml_element.rb', line 126

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)


153
154
155
# File 'lib/lutaml/xml/xml_element.rb', line 153

def processing_instruction?
  @node_type == :processing_instruction
end

#rootObject



292
293
294
# File 'lib/lutaml/xml/xml_element.rb', line 292

def root
  self
end

#textObject



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

def text
  return @text if children.empty?
  return @computed_text if defined?(@computed_text)

  @computed_text = if content_bearing_children_count > 1
                     text_children.map(&:text)
                   else
                     text_children.map(&:text).join
                   end
end

#text?Boolean

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

Returns:

  • (Boolean)


133
134
135
# File 'lib/lutaml/xml/xml_element.rb', line 133

def text?
  @node_type == :text
end

#text_childrenObject



323
324
325
# File 'lib/lutaml/xml/xml_element.rb', line 323

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

#to_hObject



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

def to_h
  document.to_h
end

#unprefixed_nameObject



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

def unprefixed_name
  @name
end

#xml_declarationObject

Default: no XML declaration. Document wrappers override this.



297
298
299
# File 'lib/lutaml/xml/xml_element.rb', line 297

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