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



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

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

#add_namespace(namespace) ⇒ Object



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

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

#attribute_is_namespace?(name) ⇒ Boolean

Returns:

  • (Boolean)


243
244
245
# File 'lib/lutaml/xml/xml_element.rb', line 243

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

#cdataObject



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

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)


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

def cdata?
  @node_type == :cdata
end

#cdata_childrenObject



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

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



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

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

#documentObject



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

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



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

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



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

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



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

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



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

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



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

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



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

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



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

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



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

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



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

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

  @namespace_uri = begin
    ns = namespace
    ns&.uri
  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
# 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
    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



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

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)


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

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

#orderObject



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

def order
  return @order_cache if @order_cache

  @order_cache = children.filter_map do |child|
    if child.text?
      next if child.text.nil?

      # 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?
      Lutaml::Xml::Element.new("Comment", "comment",
                               text_content: child.text,
                               node_type: :comment)
    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



213
214
215
216
# File 'lib/lutaml/xml/xml_element.rb', line 213

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



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

def root
  self
end

#textObject



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

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)


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

def text?
  @node_type == :text
end

#text_childrenObject



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

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

#to_hObject



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

def to_h
  document.to_h
end

#unprefixed_nameObject



189
190
191
# File 'lib/lutaml/xml/xml_element.rb', line 189

def unprefixed_name
  @name
end

#xml_declarationObject

Default: no XML declaration. Document wrappers override this.



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

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