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



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/moxml/attribute_resolver.rb', line 85

def assign(element, name, value)
  name = name.to_s
  adapter = element.context.config.adapter
  if name == "xmlns" || name.start_with?("xmlns:")
    adapter.set_attribute(element.native, name, value)
    element.invalidate_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_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)


138
139
140
141
142
143
144
145
146
147
148
# File 'lib/moxml/attribute_resolver.rb', line 138

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)


155
156
157
158
159
# File 'lib/moxml/attribute_resolver.rb', line 155

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).



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

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).



164
165
166
# File 'lib/moxml/attribute_resolver.rb', line 164

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

.prefix_part(name) ⇒ Object



189
190
191
# File 'lib/moxml/attribute_resolver.rb', line 189

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.



57
58
59
60
61
62
# File 'lib/moxml/attribute_resolver.rb', line 57

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:



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/moxml/attribute_resolver.rb', line 109

def remove(element, name)
  name = name.to_s
  adapter = element.context.config.adapter
  if name == "xmlns" || 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_attribute_cache!
  attr
end

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

Returns the matching attribute wrapper.

Returns:



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/moxml/attribute_resolver.rb', line 32

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