Module: Moxml::AttributeResolver

Defined in:
lib/moxml/attribute_resolver.rb

Overview

Canonical, adapter-independent attribute-name resolution.

XML Namespaces 1.0 semantics, implemented once on the Moxml object model (the unified interface); adapters provide raw access only:

  • An attribute's expanded name is (namespace URI or none) + local name. Unprefixed attributes are never in a namespace — the default namespace declaration does not apply to attributes (Namespaces 1.0 §5.2).
  • "prefix:local" resolves the prefix against the element's in-scope declarations and matches by expanded name: an attribute written as q:type matches a lookup as p:type whenever both prefixes are bound to the same URI.
  • "xml" is prebound to the XML namespace URI; it never needs a declaration (Namespaces 1.0 §4).
  • "xmlns"/"xmlns:*" are declarations, not attributes: never match.
  • An undeclared prefix names nothing: nil. No silent prefix-string or bare-name fallback.

When several attributes match (only possible in namespace-ill-formed documents), the first in document order wins, matching every adapter's attributes() ordering contract.

Constant Summary collapse

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

Class Method Summary collapse

Class Method Details

.assign(element, name, value) ⇒ String

Assign a value to the attribute named by XML-correct resolution, mirroring #resolve:

  • "xmlns"/"xmlns:*" are namespace declarations, not attributes: delegated verbatim to the adapter (declaration creation is adapter territory).
  • A bare name replaces the no-namespace attribute only — a namespaced attribute sharing the local name is left alone.
  • A prefixed name replaces by expanded name: assigning p:x overwrites an attribute spelled q:x when both prefixes are bound to the same URI.

When nothing matches, the attribute is created with the given spelling. A prefix not yet in scope is stored raw — builders legitimately write detached children before attaching them under the declaring ancestor, and reads resolve by expanded name once the prefix is in scope (matching nokogiri/libxml creation behavior; namespace validity is a property of the serialized document).

Returns:

  • (String)

    the assigned value



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/moxml/attribute_resolver.rb', line 104

def assign(element, name, value)
  name = name.to_s
  adapter = element.adapter
  # One probe covers both spellings ("xmlns" and "xmlns:*"); a
  # plain attribute literally starting with "xmlns" would only
  # over-invalidate (global bump instead of local) — a no-op
  # for correctness.
  if name.start_with?("xmlns")
    adapter.set_attribute(element.native, name, value)
    element.invalidate_attribute_cache!
    return value
  end

  # Fast path: every adapter's set_attribute writes by qualified
  # name, and XML cannot namespace an unprefixed attribute — so
  # a bare-name set replaces only the no-namespace attribute and
  # can never touch a namespaced p:<local> sibling. The full
  # expanded-name resolve (wrap every attribute, resolve each
  # prefix against in-scope namespaces) is only needed for
  # prefixed names; it dominated programmatic-builder attribute
  # writes.
  if !name.include?(":") && adapter.bare_set_qname_safe?
    adapter.set_attribute(element.native, name, value)
    element.invalidate_local_attribute_cache!
    return value
  end

  existing = resolve(element, name)
  if existing
    existing.value = value
    return value
  end

  adapter.set_attribute(element.native, name, value)
  element.invalidate_local_attribute_cache!
  value
end

.attribute_test?(element, attr, prefix, local) ⇒ Boolean

XPath 1.0 attribute node test, sharing the resolver's expanded-name semantics:

Parameters:

  • prefix (String, Symbol, nil)
    • String: match by namespace URI resolved against the element's in-scope declarations; the prefix spelling is irrelevant (q:x matches a p:x test when URIs agree)
    • :any: any namespace, including none (*:local)
    • nil: bare name — no-namespace attributes only (Namespaces 1.0 §5.2)
  • local (String, nil)

    local name, or nil for any (p:*)

Returns:

  • (Boolean)


175
176
177
178
179
180
181
182
183
184
185
# File 'lib/moxml/attribute_resolver.rb', line 175

def attribute_test?(element, attr, prefix, local)
  return false if local && local_name(attr.name) != local

  case prefix
  when :any then true
  when nil then attribute_uri(element, attr).nil?
  else
    uri = prefix_uri(element, prefix)
    !uri.nil? && uri == attribute_uri(element, attr)
  end
end

.attribute_test_uri?(element, attr, uri, local) ⇒ Boolean

Attribute node test with a URI resolved at XPath compile time (the static context's prefix mapping wins over the document's own declarations). See #attribute_test? for local semantics.

Returns:

  • (Boolean)


192
193
194
195
196
# File 'lib/moxml/attribute_resolver.rb', line 192

def attribute_test_uri?(element, attr, uri, local)
  return false if local && local_name(attr.name) != local

  attribute_uri(element, attr) == uri
end

.attribute_uri(element, attr, attr_name = nil) ⇒ Object

URI of an attribute's namespace, nil for the no-namespace case.

Uses the attribute's own namespace when the native binding exposes one with a URI (nokogiri/oga/libxml, and rexml/ox for declared prefixes). Otherwise reconstructs it from the attribute prefix via the element's in-scope declarations — covering adapters that report a prefix without resolving it (rexml/ox for the prebound xml prefix) and adapters that expose neither namespace nor separate prefix (leptris qualified names).



214
215
216
217
218
219
220
221
222
223
224
# File 'lib/moxml/attribute_resolver.rb', line 214

def attribute_uri(element, attr, attr_name = nil)
  ns = attr.namespace
  return ns.uri unless ns.nil? || ns.uri.to_s.empty?

  prefix = ns&.prefix || prefix_part(attr_name || attr.name)
  return nil if prefix.nil? || prefix.empty?
  return XML_NAMESPACE_URI if prefix == "xml"

  element.in_scope_namespaces
    .find { |candidate| candidate.prefix == prefix }&.uri
end

.local_name(name) ⇒ Object

Local part of a wrapper attribute name. Local names cannot contain ':' in namespace-well-formed documents; splitting is for adapters whose natives carry the qualified name (leptris).



201
202
203
# File 'lib/moxml/attribute_resolver.rb', line 201

def local_name(name)
  name.include?(":") ? name.split(":", 2)[1] : name
end

.prefix_part(name) ⇒ Object



226
227
228
# File 'lib/moxml/attribute_resolver.rb', line 226

def prefix_part(name)
  name.include?(":") ? name.split(":", 2)[0] : nil
end

.prefix_uri(element, prefix) ⇒ Object

URI a prefix is bound to for lookups on this element, or nil when the prefix is undeclared.



76
77
78
79
80
81
# File 'lib/moxml/attribute_resolver.rb', line 76

def prefix_uri(element, prefix)
  return XML_NAMESPACE_URI if prefix == "xml"

  element.in_scope_namespaces
    .find { |candidate| candidate.prefix == prefix }&.uri
end

.remove(element, name) ⇒ Moxml::Attribute?

Remove the attribute named by XML-correct resolution (see #assign for the name semantics).

Returns:



146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/moxml/attribute_resolver.rb', line 146

def remove(element, name)
  name = name.to_s
  adapter = element.adapter
  if name.start_with?("xmlns")
    adapter.remove_attribute(element.native, name)
    element.invalidate_attribute_cache!
    return nil
  end

  attr = resolve(element, name)
  return nil unless attr

  adapter.remove_attribute_native(attr.native)
  element.invalidate_local_attribute_cache!
  attr
end

.resolve(element, name) ⇒ Object



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/moxml/attribute_resolver.rb', line 51

def resolve(element, name)
  name = name.to_s
  if name.include?(":")
    prefix, local = name.split(":", 2)
    return nil if prefix == "xmlns"

    uri = prefix_uri(element, prefix)
    return nil if uri.nil?

    element.attributes.find do |attr|
      # attr.name allocates on several adapters (native string,
      # plus leptris' force_encoding dup) — read it once per probe
      attr_name = attr.name
      local_name(attr_name) == local && attribute_uri(element, attr, attr_name) == uri
    end
  else
    element.attributes.find do |attr|
      attr_name = attr.name
      local_name(attr_name) == name && attribute_uri(element, attr, attr_name).nil?
    end
  end
end

.resolve_value(element, name) ⇒ Moxml::Attribute?

Value-only prefixed resolution for Element#[]: prefix resolution is the resolver's real work; the match step goes to the engine's expanded-name lookup where the adapter offers it, skipping the attribute-list materialization and per-attr URI probes. Bare names take Element#'s adapter fast path instead.

Returns:



37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/moxml/attribute_resolver.rb', line 37

def resolve_value(element, name)
  prefix, local = name.split(":", 2)
  return nil if prefix == "xmlns"

  uri = prefix_uri(element, prefix)
  return nil if uri.nil?

  adapter = element.adapter
  return adapter.expanded_attr_value(element.native, uri, local) if adapter.expanded_attr_reads?

  attr = resolve(element, name)
  attr&.value
end