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
-
.assign(element, name, value) ⇒ String
Assign a value to the attribute named by XML-correct resolution, mirroring #resolve:.
-
.attribute_test?(element, attr, prefix, local) ⇒ Boolean
XPath 1.0 attribute node test, sharing the resolver's expanded-name semantics:.
-
.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).
-
.attribute_uri(element, attr, attr_name = nil) ⇒ Object
URI of an attribute's namespace, nil for the no-namespace case.
-
.local_name(name) ⇒ Object
Local part of a wrapper attribute name.
- .prefix_part(name) ⇒ Object
-
.prefix_uri(element, prefix) ⇒ Object
URI a prefix is bound to for lookups on this element, or nil when the prefix is undeclared.
-
.remove(element, name) ⇒ Moxml::Attribute?
Remove the attribute named by XML-correct resolution (see #assign for the name semantics).
- .resolve(element, name) ⇒ Object
-
.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.
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).
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. 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:
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.
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).
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.
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.(element.native, uri, local) if adapter. attr = resolve(element, name) attr&.value end |