Class: Dommy::Internal::NodeWrapperCache

Inherits:
Object
  • Object
show all
Defined in:
lib/dommy/internal/node_wrapper_cache.rb

Overview

Manages DOM node identity via wrapper caching. Ensures that wrap_node(nokogiri_node) always returns the same Ruby object. Separates identity/caching management from Document’s public DOM API.

Instance Method Summary collapse

Constructor Details

#initialize(document) ⇒ NodeWrapperCache

Returns a new instance of NodeWrapperCache.



9
10
11
12
# File 'lib/dommy/internal/node_wrapper_cache.rb', line 9

def initialize(document)
  @document = document
  @wrappers = {}
end

Instance Method Details

#create_attribute(name) ⇒ Object



68
69
70
71
72
73
74
# File 'lib/dommy/internal/node_wrapper_cache.rb', line 68

def create_attribute(name)
  str = domstring(name)
  raise DOMException::InvalidCharacterError, "name must not be empty" if str.empty?
  raise DOMException::InvalidCharacterError, "invalid attribute name: #{str.inspect}" unless str.match?(Namespaces::NAME)

  Attr.new(str)
end

#create_attribute_ns(namespace_uri, qualified_name) ⇒ Object



76
77
78
79
80
81
# File 'lib/dommy/internal/node_wrapper_cache.rb', line 76

def create_attribute_ns(namespace_uri, qualified_name)
  namespace_uri = nil if namespace_uri.equal?(Bridge::UNDEFINED)
  qualified_name = domstring(qualified_name)
  ns, prefix, local = Namespaces.validate_and_extract(namespace_uri, qualified_name)
  Attr.new(qualified_name, namespace_uri: ns, prefix: prefix, local_name: local)
end

#create_cdata_section(text) ⇒ Object



56
57
58
# File 'lib/dommy/internal/node_wrapper_cache.rb', line 56

def create_cdata_section(text)
  wrap_node(Backend.create_cdata(text.to_s, @document.nokogiri_doc))
end

#create_comment(text) ⇒ Object



60
61
62
# File 'lib/dommy/internal/node_wrapper_cache.rb', line 60

def create_comment(text)
  wrap_node(Backend.create_comment(text.to_s, @document.nokogiri_doc))
end

#create_document_fragmentObject



64
65
66
# File 'lib/dommy/internal/node_wrapper_cache.rb', line 64

def create_document_fragment
  wrap_node(@document.nokogiri_doc.fragment(""))
end

#create_element(name) ⇒ Object

Factory methods



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/dommy/internal/node_wrapper_cache.rb', line 29

def create_element(name)
  str = domstring(name)
  raise DOMException::InvalidCharacterError, "name must not be empty" if str.empty?
  raise DOMException::InvalidCharacterError, "invalid element name: #{str.inspect}" unless str.match?(Namespaces::NAME)

  # WHATWG createElement: lowercase (ASCII) the name only in an HTML
  # document; the namespace is the HTML namespace for HTML/XHTML documents
  # and null for a non-XHTML XML document. Record the metadata so the
  # element's localName/tagName/namespaceURI getters report it faithfully
  # (in particular case preservation for XML/XHTML).
  if @document.html_document?
    local = str.downcase(:ascii)
    namespace = Element::HTML_NAMESPACE
  else
    local = str
    namespace = @document.content_type == "application/xhtml+xml" ? Element::HTML_NAMESPACE : nil
  end

  wrapper = wrap_node(Backend.create_element(local, @document.nokogiri_doc))
  wrapper.__internal_set_namespace__(namespace, nil, local, local)
  wrapper
end

#create_element_ns(namespace_uri, qualified_name) ⇒ Object



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/dommy/internal/node_wrapper_cache.rb', line 83

def create_element_ns(namespace_uri, qualified_name)
  # WHATWG "validate and extract": QName-validate the qualifiedName
  # (InvalidCharacterError) and apply the prefix/namespace rules
  # (NamespaceError), then build the element with its prefix bound.
  # namespace is nullable (undefined → null); qualifiedName is a plain
  # DOMString (undefined → "undefined", null → "null").
  namespace_uri = nil if namespace_uri.equal?(Bridge::UNDEFINED)
  qualified_name = domstring(qualified_name)
  ns, prefix, local = Namespaces.validate_and_extract(namespace_uri, qualified_name)

  el = Backend.create_element(qualified_name, @document.nokogiri_doc)
  Backend.add_namespace_definition(el, prefix, ns) if ns

  wrapper = wrap(el)
  wrapper.__internal_set_namespace__(ns, prefix, local, qualified_name)
  wrapper
end

#create_text_node(text) ⇒ Object



52
53
54
# File 'lib/dommy/internal/node_wrapper_cache.rb', line 52

def create_text_node(text)
  wrap_node(Backend.create_text(text.to_s, @document.nokogiri_doc))
end

#get_element_by_id(id) ⇒ Object



117
118
119
120
121
# File 'lib/dommy/internal/node_wrapper_cache.rb', line 117

def get_element_by_id(id)
  return nil if id.nil? || id.to_s.empty?

  wrap(@document.nokogiri_doc.at_css("##{id}"))
end

#get_elements_by_class_name(name) ⇒ Object



143
144
145
146
147
148
149
150
151
152
153
# File 'lib/dommy/internal/node_wrapper_cache.rb', line 143

def get_elements_by_class_name(name)
  tokens = name.to_s.split(/\s+/).reject(&:empty?)
  doc = @document.nokogiri_doc
  cache = self
  HTMLCollection.new do
    next [] if tokens.empty?

    selector = tokens.map { |t| ".#{t}" }.join("")
    doc.css(selector).map { |n| cache.wrap(n) }.compact
  end
end

#get_elements_by_name(name) ⇒ Object



134
135
136
137
138
139
140
141
# File 'lib/dommy/internal/node_wrapper_cache.rb', line 134

def get_elements_by_name(name)
  doc = @document.nokogiri_doc
  cache = self
  key = name.to_s
  HTMLCollection.new do
    doc.css("[name='#{key}']").map { |x| cache.wrap(x) }.compact
  end
end

#get_elements_by_tag_name(name) ⇒ Object



123
124
125
126
127
128
129
130
131
132
# File 'lib/dommy/internal/node_wrapper_cache.rb', line 123

def get_elements_by_tag_name(name)
  n = name.to_s.downcase
  doc = @document.nokogiri_doc
  cache = self
  if n == "*"
    HTMLCollection.new { doc.css("*").map { |x| cache.wrap(x) }.compact }
  else
    HTMLCollection.new { doc.css(n).map { |x| cache.wrap(x) }.compact }
  end
end

#query_selector(selector) ⇒ Object

Query methods



103
104
105
106
107
108
# File 'lib/dommy/internal/node_wrapper_cache.rb', line 103

def query_selector(selector)
  return nil if selector.nil?
  Internal.validate_selector!(selector)

  wrap(@document.nokogiri_doc.at_css(Internal.backend_safe_selector(selector.to_s), CSS_PSEUDO_HANDLERS))
end

#query_selector_all(selector) ⇒ Object



110
111
112
113
114
115
# File 'lib/dommy/internal/node_wrapper_cache.rb', line 110

def query_selector_all(selector)
  return NodeList.new if selector.nil?
  Internal.validate_selector!(selector)

  NodeList.new(@document.nokogiri_doc.css(Internal.backend_safe_selector(selector.to_s), CSS_PSEUDO_HANDLERS).map { |node| wrap(node) }.compact)
end

#register(nokogiri_node, wrapper) ⇒ Object

Register an externally-built wrapper. Used by Document#adopt_node when migrating a wrapper from another document so the existing Ruby object survives the move rather than being replaced by a freshly-built one.



164
165
166
# File 'lib/dommy/internal/node_wrapper_cache.rb', line 164

def register(nokogiri_node, wrapper)
  @wrappers[nokogiri_node.object_id] = wrapper
end

#reset_wrapper(nokogiri_node) ⇒ Object

Clear cached wrapper (used by customElements.define for upgrades)



156
157
158
# File 'lib/dommy/internal/node_wrapper_cache.rb', line 156

def reset_wrapper(nokogiri_node)
  @wrappers.delete(nokogiri_node.object_id)
end

#wrap(node) ⇒ Object

Returns the wrapped node, creating and caching if needed. Maintains DOM identity across repeated traversals.



16
17
18
19
20
21
22
23
24
25
# File 'lib/dommy/internal/node_wrapper_cache.rb', line 16

def wrap(node)
  return nil unless node

  cached = @wrappers[node.object_id]
  return cached if cached

  wrapper = build_wrapper_for(node)
  @wrappers[node.object_id] = wrapper if wrapper
  wrapper
end