Module: Moxml::ElementBehavior

Included in:
Element
Defined in:
lib/moxml/element.rb

Overview

Instance behavior for Moxml::Element, extracted so the leptris adapter can extend natives with it in place (issue #230); the class remains the consumer-facing contract.

Instance Method Summary collapse

Instance Method Details

#[](name) ⇒ Object

Unified XML-correct name resolution (see AttributeResolver): bare names match only no-namespace attributes; prefixed names resolve through in-scope declarations to expanded names.



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/moxml/element.rb', line 61

def [](name)
  key = name.to_s
  # Bare names on qualified-name engines: one adapter call with
  # the adapter's own marker restoration (bare_attr_value). The
  # resolver's expanded-name semantics only pay for prefixed
  # names, where resolution is real work.
  adapter = self.adapter
  # Capability memo: the adapter predicate is a per-read
  # dispatch for a lifetime constant.
  if !key.include?(":") &&
      ((@bare_qname_safe ||= adapter.bare_get_qname_safe?) == true)
    # The entity-restore decision rides the wrapper's generation
    # memo (as Element#text) — the adapter-level probe walks to
    # the document and the attachment store on every read, which
    # dominated the fast path.
    value = adapter.bare_attr_value(@native, key)
    return adapter.restore_entities(value) if value.is_a?(String) && entity_bearing?

    return value
  end

  cache = attribute_read_cache
  unless cache.key?(key)
    cache[key] = if key.include?(":")
                   Moxml::AttributeResolver.resolve_value(self, key)
                 else
                   Moxml::AttributeResolver.resolve(self, key)&.value
                 end
  end
  cache[key]
end

#[]=(name, value) ⇒ Object

Write path shares the resolver's expanded-name semantics (see AttributeResolver.assign): bare names target no-namespace attributes only; prefixed names replace by namespace URI and require a declared prefix. Cache coherence is the resolver's.



51
52
53
54
55
56
# File 'lib/moxml/element.rb', line 51

def []=(name, value)
  # Every assign path (native set or value=) ends in
  # invalidate_attribute_cache!, which bumps the generation —
  # a leading bump here was one per write.
  Moxml::AttributeResolver.assign(self, name, normalize_xml_value(value))
end

#add_element(name, attrs = {}) ⇒ Object

Create an element with attributes and attach it under this element — ONE construction on adapters with the bulk face (leptris, #312/#1344 consumer face): create + attach + all attributes in a single Ruby->C crossing. Returns the child wrapper. Equivalent to create_element + attribute writes + add_child, minus two crossings and the intermediate churn.



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/moxml/element.rb', line 135

def add_element(name, attrs = {})
  native_child = adapter.create_element_with_attrs(
    @native, name, attrs
  )
  if native_child
    adapter.node_type(native_child)
    Moxml::Node.wrap_with(native_child, context, adapter)
  else
    fallback = Moxml::Node.wrap_with(
      adapter.create_element(name), context, adapter
    )
    attrs.each { |k, v| fallback[k] = v.to_s }
    add_child(fallback)
    fallback
  end
end

#add_namespace(prefix, uri) ⇒ Object Also known as: add_namespace_definition



199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/moxml/element.rb', line 199

def add_namespace(prefix, uri)
  adapter.create_namespace(@native, prefix, uri,
                           namespace_validation_mode: context.config.namespace_validation_mode)
  invalidate_namespace_cache!
  self
rescue ValidationError => e
  # Re-raise as NamespaceError, provide attributes for error context
  # but the to_s will only add details if provided
  raise Moxml::NamespaceError.new(
    e.message,
    prefix: prefix,
    uri: uri,
    element: self,
  )
end

#append_xml(fragment) ⇒ Moxml::Element

Append a raw XML fragment's top-level nodes as children.

Bulk construction rides the engine's parser instead of per-node create/attach calls — measured 2.0x faster than per-node building through the same adapter, and faster than per-node building on Nokogiri (the parse is C; the per-node path pays an FFI crossing per node).

The fragment must be namespace-self-contained (declarations inside it ride along with the moved subtrees; prefixes relying on THIS element's scope must be spelled out in the fragment) and well-formed as the content of a single wrapper element — declarations and doctypes raise ParseError.

Returns:



185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/moxml/element.rb', line 185

def append_xml(fragment)
  if context.config.adapter_name == :ox
    raise Moxml::AdapterError.new(
      "append_xml is not supported by the Ox adapter (its customized node wrappers do not survive cross-document attachment)",
      adapter: "Ox", operation: "append_xml",
    )
  end

  wrapper = context.parse("<m>#{fragment}</m>")
  children = wrapper.root.children.to_a
  children.each { |child| add_child(child) }
  self
end

#attribute(name) ⇒ Object



93
94
95
# File 'lib/moxml/element.rb', line 93

def attribute(name)
  Moxml::AttributeResolver.resolve(self, name)
end

#attribute_pairs ⇒ Object

Read-only [name, value] pairs in document order, duplicates included — no Attribute node wrappers. The walk-hot shape for consumers that only read name/value (leptris-ruby#278): on leptris >= 1.9.208.1 this answers in one C crossing with zero per-attribute objects. Names may be shared frozen strings — dup before mutating. Mutation and per-attribute namespace resolution stay on #attributes.



125
126
127
# File 'lib/moxml/element.rb', line 125

def attribute_pairs
  adapter.attribute_pairs(@native)
end

#attribute_read_cache ⇒ Object

Resolution reads the element.s in-scope namespaces, so the cache rides the context.s namespace-scope generation clock — every attribute or namespace mutation anywhere bumps it.



100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/moxml/element.rb', line 100

def attribute_read_cache
  generation = context.namespace_scope_generation
  # @attribute_cache.nil? covers the local invalidation clears —
  # a value or list change without a scope bump (see
  # invalidate_local_attribute_cache!) — so writes never allocate
  # a replacement hash; the next read materializes one lazily.
  if @attribute_cache.nil? || @attribute_cache_generation != generation
    @attribute_cache = {}
    @attribute_cache_generation = generation
  end
  @attribute_cache
end

#attributes ⇒ Object



152
153
154
155
156
157
158
159
160
161
# File 'lib/moxml/element.rb', line 152

def attributes
  # Primed like Node.wrap: the adapter and type are known at
  # mint, so the first name/value/attribute? access skips the
  # context hop and the adapter's type probe.
  @attributes ||= adapter.attributes(@native).map do |attr|
    a = Wrappers::Attribute.new(attr, context, adapter, :attribute)
    a.parent_node = self
    a
  end
end

#declared_namespaces ⇒ Object

The element's OWN namespace declarations as [prefix, uri] pairs (nil prefix = default namespace) — not the inherited scope. The shape materialize records carry (issue #138).



260
261
262
263
264
265
# File 'lib/moxml/element.rb', line 260

def declared_namespaces
  adapter.namespace_definitions(@native).map do |ns|
    wrapper = Wrappers::Namespace.new(ns, context)
    [wrapper.prefix, wrapper.uri]
  end
end

#expanded_name ⇒ Object

Returns the expanded name including namespace prefix



27
28
29
30
31
32
33
# File 'lib/moxml/element.rb', line 27

def expanded_name
  if namespace_prefix && !namespace_prefix.empty?
    "#{namespace_prefix}:#{name}"
  else
    name
  end
end

#find_all(xpath) ⇒ Object



355
356
357
# File 'lib/moxml/element.rb', line 355

def find_all(xpath)
  xpath(xpath).to_a
end

#find_element(xpath) ⇒ Object

Convenience find methods



351
352
353
# File 'lib/moxml/element.rb', line 351

def find_element(xpath)
  at_xpath(xpath)
end

#get(attr_name) ⇒ Object

Returns attribute value by name (used by XPath engine)



114
115
116
# File 'lib/moxml/element.rb', line 114

def get(attr_name)
  self[attr_name]
end

#identifier ⇒ String

Returns the primary identifier for this element (its tag name)

Returns:

  • (String) —

    the element name



22
23
24
# File 'lib/moxml/element.rb', line 22

def identifier
  name
end

#in_scope_namespaces ⇒ Object

Returns all namespaces in scope for this element, including those inherited from ancestor elements.



269
270
271
272
273
274
275
276
277
278
# File 'lib/moxml/element.rb', line 269

def in_scope_namespaces
  generation = context.namespace_scope_generation
  if @in_scope_namespaces.nil? || @in_scope_generation != generation
    @in_scope_namespaces = adapter.in_scope_namespaces(@native).map do |ns|
      Wrappers::Namespace.new(ns, context)
    end
    @in_scope_generation = generation
  end
  @in_scope_namespaces
end

#inner_text ⇒ Object



300
301
302
303
# File 'lib/moxml/element.rb', line 300

def inner_text
  text = raw_inner_text
  entity_bearing? ? adapter.restore_entities(text) : text
end

#inner_xml ⇒ Object



311
312
313
# File 'lib/moxml/element.rb', line 311

def inner_xml
  adapter.inner_xml(@native)
end

#inner_xml=(xml) ⇒ Object



315
316
317
318
319
320
# File 'lib/moxml/element.rb', line 315

def inner_xml=(xml)
  wrapper = "_moxml_inner_#{Process.pid}_#{object_id}"
  doc = context.parse("<#{wrapper}>#{xml}</#{wrapper}>")
  adapter.replace_children(@native, doc.root.children.map(&:native))
  invalidate_children_cache!
end

#invalidate_attribute_cache! ⇒ Object

Called by the namespace-scoped attribute paths (xmlns writes and removals) and Attribute#name=. Clears the attribute list and the resolved-read cache, and bumps the context generation so cross-wrapper reads recompute.



383
384
385
386
387
# File 'lib/moxml/element.rb', line 383

def invalidate_attribute_cache!
  @attributes = nil
  @attribute_cache = nil
  context.bump_namespace_scope_generation
end

#invalidate_attribute_value_cache! ⇒ Object

Attribute mutations that cannot change namespace scope (bare and non-xmlns prefixed names) invalidate locally: the resolved-read cache alone for value writes, the wrapper list too when the attribute set changes. No context generation bump — that would evict every wrapper's caches document-wide on each write of a bulk build.



370
371
372
# File 'lib/moxml/element.rb', line 370

def invalidate_attribute_value_cache!
  @attribute_cache = nil
end

#invalidate_local_attribute_cache! ⇒ Object



374
375
376
377
# File 'lib/moxml/element.rb', line 374

def invalidate_local_attribute_cache!
  @attributes = nil
  @attribute_cache = nil
end

#invalidate_namespace_cache! ⇒ Object

Clear the namespace caches and bump the context's scope generation so every wrapper — including ones not reachable from any children cache — recomputes on next read.



392
393
394
395
396
397
# File 'lib/moxml/element.rb', line 392

def invalidate_namespace_cache!
  @namespaces = nil
  @namespace_definitions = nil
  @in_scope_namespaces = nil
  context.bump_namespace_scope_generation
end

#name ⇒ Object



8
9
10
11
12
13
# File 'lib/moxml/element.rb', line 8

def name
  # The adapter read is an FFI call plus (on several adapters) a
  # defensive string copy; names only change through name= and
  # native adoption, both of which clear this.
  @name ||= adapter.node_name(@native)
end

#name=(value) ⇒ Object



15
16
17
18
# File 'lib/moxml/element.rb', line 15

def name=(value)
  @name = nil
  adapter.set_node_name(@native, value)
end

#namespace ⇒ Object

it's NOT the same as namespaces.first



217
218
219
220
# File 'lib/moxml/element.rb', line 217

def namespace
  ns = adapter.namespace(@native)
  ns && Wrappers::Namespace.new(ns, context)
end

#namespace=(ns_or_hash) ⇒ Object

add the prefix to the element name and add the namespace to the list of namespace definitions



224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/moxml/element.rb', line 224

def namespace=(ns_or_hash)
  if ns_or_hash.is_a?(Hash)
    adapter.set_namespace(
      @native,
      adapter.create_namespace(@native, *ns_or_hash.to_a.first,
                               namespace_validation_mode: context.config.namespace_validation_mode),
    )
  else
    adapter.set_namespace(@native, ns_or_hash&.native)
  end
  invalidate_namespace_cache!
end

#namespace_definitions ⇒ Object

The element's OWN namespace declarations only (not inherited). C14n's visibly-utilized calculation and the materializer's declaration pairs want exactly this shape.



249
250
251
252
253
254
255
# File 'lib/moxml/element.rb', line 249

def namespace_definitions
  return @namespace_definitions unless @namespace_definitions.nil?

  @namespace_definitions = adapter.namespace_definitions(@native).map do |ns|
    Wrappers::Namespace.new(ns, context)
  end
end

#namespace_name ⇒ Object

Returns the namespace URI of this element (alias for namespace_uri)



281
282
283
# File 'lib/moxml/element.rb', line 281

def namespace_name
  namespace_uri
end

#namespace_prefix ⇒ Object

Returns the namespace prefix of this element



36
37
38
39
# File 'lib/moxml/element.rb', line 36

def namespace_prefix
  ns = namespace
  ns&.prefix
end

#namespace_uri ⇒ Object

Returns the namespace URI of this element



42
43
44
45
# File 'lib/moxml/element.rb', line 42

def namespace_uri
  ns = namespace
  ns&.uri
end

#namespaces ⇒ Object

All namespaces IN SCOPE for this element — its own declarations plus everything inherited from ancestors — matching the Nokogiri #namespaces contract consumers port against (issue #198: this returned only own declarations, losing ancestor scope under every backend).



242
243
244
# File 'lib/moxml/element.rb', line 242

def namespaces
  in_scope_namespaces
end

#nodes ⇒ Object

Alias for children (used by XPath engine)



360
361
362
# File 'lib/moxml/element.rb', line 360

def nodes
  children
end

#raw_inner_text ⇒ Object

Returns inner text without entity marker restoration. Used internally when raw content with markers is needed (e.g., for DOM construction).



307
308
309
# File 'lib/moxml/element.rb', line 307

def raw_inner_text
  adapter.inner_text(@native)
end

#remove_attribute(name) ⇒ Object



163
164
165
166
167
168
# File 'lib/moxml/element.rb', line 163

def remove_attribute(name)
  # Both remove paths end in invalidate_attribute_cache!, which
  # bumps the generation.
  Moxml::AttributeResolver.remove(self, name)
  self
end

#set_attributes(attributes_hash) ⇒ Object

Bulk attribute setting



339
340
341
342
# File 'lib/moxml/element.rb', line 339

def set_attributes(attributes_hash)
  attributes_hash.each { |name, value| self[name] = value }
  self
end

#text ⇒ Object Also known as: content



285
286
287
288
289
290
291
# File 'lib/moxml/element.rb', line 285

def text
  val = adapter.text_content(@native)
  # Entity-free documents (the common case) skip the marker
  # restore scans entirely — the per-wrapper entity_bearing?
  # memo rides the adapter's serialize generation.
  entity_bearing? ? adapter.restore_entities(val) : val
end

#text=(content) ⇒ Object



295
296
297
298
# File 'lib/moxml/element.rb', line 295

def text=(content)
  adapter.set_text_content(@native, normalize_xml_value(content))
  invalidate_children_cache!
end

#with_attribute(name, value) ⇒ Object

Fluent interface methods



323
324
325
326
# File 'lib/moxml/element.rb', line 323

def with_attribute(name, value)
  self[name] = value
  self
end

#with_child(child) ⇒ Object

Chainable child addition



345
346
347
348
# File 'lib/moxml/element.rb', line 345

def with_child(child)
  add_child(child)
  self
end

#with_namespace(prefix, uri) ⇒ Object



328
329
330
331
# File 'lib/moxml/element.rb', line 328

def with_namespace(prefix, uri)
  add_namespace(prefix, uri)
  self
end

#with_text(content) ⇒ Object



333
334
335
336
# File 'lib/moxml/element.rb', line 333

def with_text(content)
  self.text = content
  self
end