dommy 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/lib/dommy/animation.rb +9 -1
  3. data/lib/dommy/attr.rb +192 -39
  4. data/lib/dommy/backend/nokogiri_adapter.rb +76 -0
  5. data/lib/dommy/backend/nokolexbor_adapter.rb +37 -0
  6. data/lib/dommy/backend.rb +46 -0
  7. data/lib/dommy/blob.rb +28 -9
  8. data/lib/dommy/bridge/constructor_registry.rb +28 -0
  9. data/lib/dommy/bridge/methods.rb +57 -0
  10. data/lib/dommy/bridge.rb +97 -0
  11. data/lib/dommy/callable_invoker.rb +36 -0
  12. data/lib/dommy/cookie_store.rb +3 -1
  13. data/lib/dommy/crypto.rb +7 -1
  14. data/lib/dommy/css.rb +46 -0
  15. data/lib/dommy/custom_elements.rb +27 -3
  16. data/lib/dommy/data_transfer.rb +4 -0
  17. data/lib/dommy/document.rb +615 -48
  18. data/lib/dommy/dom_parser.rb +28 -15
  19. data/lib/dommy/element.rb +999 -471
  20. data/lib/dommy/event.rb +260 -96
  21. data/lib/dommy/event_source.rb +6 -2
  22. data/lib/dommy/fetch.rb +505 -43
  23. data/lib/dommy/file_reader.rb +11 -3
  24. data/lib/dommy/form_data.rb +2 -0
  25. data/lib/dommy/history.rb +43 -8
  26. data/lib/dommy/html_collection.rb +55 -2
  27. data/lib/dommy/html_elements.rb +102 -1519
  28. data/lib/dommy/internal/css_pseudo_handlers.rb +109 -0
  29. data/lib/dommy/internal/global_functions.rb +26 -0
  30. data/lib/dommy/internal/idna.rb +16 -7
  31. data/lib/dommy/internal/ipv4_parser.rb +22 -7
  32. data/lib/dommy/internal/mutation_coordinator.rb +11 -2
  33. data/lib/dommy/internal/namespaces.rb +70 -0
  34. data/lib/dommy/internal/node_equality.rb +86 -0
  35. data/lib/dommy/internal/node_wrapper_cache.rb +62 -27
  36. data/lib/dommy/internal/observable_callback.rb +1 -5
  37. data/lib/dommy/internal/parent_node.rb +126 -0
  38. data/lib/dommy/internal/reflected_attributes.rb +103 -13
  39. data/lib/dommy/internal/selector_parser.rb +664 -0
  40. data/lib/dommy/internal/url_parser.rb +677 -0
  41. data/lib/dommy/intersection_observer.rb +2 -0
  42. data/lib/dommy/location.rb +2 -0
  43. data/lib/dommy/media_query_list.rb +7 -1
  44. data/lib/dommy/message_channel.rb +32 -2
  45. data/lib/dommy/mutation_observer.rb +55 -12
  46. data/lib/dommy/navigator.rb +26 -12
  47. data/lib/dommy/node.rb +158 -28
  48. data/lib/dommy/notification.rb +3 -1
  49. data/lib/dommy/performance.rb +4 -0
  50. data/lib/dommy/performance_observer.rb +2 -0
  51. data/lib/dommy/promise.rb +14 -14
  52. data/lib/dommy/range.rb +74 -5
  53. data/lib/dommy/resize_observer.rb +2 -0
  54. data/lib/dommy/scheduler.rb +34 -13
  55. data/lib/dommy/shadow_root.rb +23 -54
  56. data/lib/dommy/storage.rb +2 -0
  57. data/lib/dommy/streams.rb +18 -27
  58. data/lib/dommy/svg_elements.rb +204 -3606
  59. data/lib/dommy/text_codec.rb +174 -21
  60. data/lib/dommy/tree_walker.rb +255 -66
  61. data/lib/dommy/url.rb +287 -449
  62. data/lib/dommy/url_pattern.rb +2 -0
  63. data/lib/dommy/version.rb +1 -1
  64. data/lib/dommy/web_socket.rb +37 -7
  65. data/lib/dommy/window.rb +202 -213
  66. data/lib/dommy/worker.rb +7 -7
  67. data/lib/dommy/xml_http_request.rb +15 -5
  68. data/lib/dommy.rb +7 -0
  69. metadata +12 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ccbe61a606e042968621f7fa81588634f2cdae566d96cef137bfefdfb6d38dd6
4
- data.tar.gz: 4358d9b68082608b7e78971be2d2818fb35200b17bec1819ce21a520b3552d7c
3
+ metadata.gz: ae1783921d75a534741bfa2de3be6ddd7492f01a6d63d4a3102b85411518e64d
4
+ data.tar.gz: 3022f80e295f06db17782b034d252e8529435ccae9e7ce7283e936da2cea8804
5
5
  SHA512:
6
- metadata.gz: a8c9a3b7bbfacb6f58440cbfec8884cc79c8690beda03225b229ca935f1b6d1ce0778c83a9253f2996198e46c8d8b85f9df53ccedb1ffa4e942a5b9841c051e4
7
- data.tar.gz: f428a160a4c2f0180e7fa01f8cb1e6ec31dc9855f2bd7b43f1be6abd7a752a6b93926c1e57791a1c8b51a22e62631c997c4fbe8b55ea38fed5748f62cc874033
6
+ metadata.gz: c3ccc2658999f785bab29f01e94bca1d7c0505370256fb55687cca8822b0e4dcdd8efb1d64d172a3a7dced5a3a4224612bc38d8df48f943088d56bd81bc85cd9
7
+ data.tar.gz: dc65af8b414579d71d72601e1c1f3ac070ed6e4a67a7385cbc17222ed9b896687a494709f974f75af192d2441f12de3cc6be0f4eaaa263198f5bc868335193ab
@@ -48,6 +48,8 @@ module Dommy
48
48
  end
49
49
  end
50
50
 
51
+ include Bridge::Methods
52
+ js_methods %w[getTiming updateTiming]
51
53
  def __js_call__(method, args)
52
54
  case method
53
55
  when "getTiming"
@@ -217,11 +219,17 @@ module Dommy
217
219
  @playback_rate = value.to_f
218
220
  when "id"
219
221
  @id = value.to_s
222
+ else
223
+ return Bridge::UNHANDLED
220
224
  end
221
225
 
222
226
  nil
223
227
  end
224
228
 
229
+ include Bridge::Methods
230
+ js_methods %w[
231
+ play pause cancel finish reverse addEventListener removeEventListener dispatchEvent
232
+ ]
225
233
  def __js_call__(method, args)
226
234
  case method
227
235
  when "play"
@@ -237,7 +245,7 @@ module Dommy
237
245
  when "addEventListener"
238
246
  add_event_listener(args[0], args[1], args[2])
239
247
  when "removeEventListener"
240
- remove_event_listener(args[0], args[1])
248
+ remove_event_listener(args[0], args[1], args[2])
241
249
  when "dispatchEvent"
242
250
  dispatch_event(args[0])
243
251
  end
data/lib/dommy/attr.rb CHANGED
@@ -13,12 +13,32 @@ module Dommy
13
13
  # not yet attached. Value is stored locally; `setAttributeNode`
14
14
  # transfers it to an element.
15
15
  class Attr
16
- attr_reader :name
16
+ include Node
17
+ include EventTarget
17
18
 
18
- def initialize(name, owner: nil, value: "")
19
- @name = name.to_s.downcase
19
+ attr_reader :name, :namespace_uri, :prefix, :local_name
20
+
21
+ def __internal_event_parent__
22
+ nil
23
+ end
24
+
25
+ def initialize(name, owner: nil, value: "", namespace_uri: nil, prefix: nil, local_name: nil)
26
+ qname = name.to_s
20
27
  @owner = owner
21
28
  @detached_value = value.to_s
29
+ if namespace_uri && !namespace_uri.to_s.empty?
30
+ # Namespaced attributes preserve case and carry prefix / localName.
31
+ @name = qname
32
+ @namespace_uri = namespace_uri.to_s
33
+ @prefix = prefix
34
+ @local_name = (local_name || qname.split(":", 2).last).to_s
35
+ else
36
+ # Null-namespace (HTML) attributes are lower-cased, as before.
37
+ @name = qname.downcase
38
+ @namespace_uri = nil
39
+ @prefix = nil
40
+ @local_name = @name
41
+ end
22
42
  end
23
43
 
24
44
  # The Element this attr is on, or nil if detached.
@@ -28,7 +48,11 @@ module Dommy
28
48
 
29
49
  def value
30
50
  if @owner
31
- @owner.__dommy_backend_node__[@name].to_s
51
+ if @namespace_uri
52
+ Backend.get_attribute_ns(@owner.__dommy_backend_node__, @namespace_uri, @local_name).to_s
53
+ else
54
+ @owner.__dommy_backend_node__[@name].to_s
55
+ end
32
56
  else
33
57
  @detached_value
34
58
  end
@@ -36,7 +60,11 @@ module Dommy
36
60
 
37
61
  def value=(new_value)
38
62
  if @owner
39
- @owner.set_attribute(@name, new_value.to_s)
63
+ if @namespace_uri
64
+ @owner.set_attribute_ns(@namespace_uri, @name, new_value.to_s)
65
+ else
66
+ @owner.set_attribute(@name, new_value.to_s)
67
+ end
40
68
  else
41
69
  @detached_value = new_value.to_s
42
70
  end
@@ -52,36 +80,65 @@ module Dommy
52
80
  @name
53
81
  when "nodeValue"
54
82
  value
83
+ when "textContent"
84
+ # Node.textContent for an Attr returns its value (WHATWG DOM).
85
+ value
55
86
  when "ownerElement"
56
87
  @owner
57
88
  when "localName"
58
- @name
89
+ @local_name
59
90
  when "namespaceURI"
60
- nil
91
+ @namespace_uri
92
+ when "prefix"
93
+ @prefix
61
94
  when "nodeType"
62
95
  2
96
+ when "specified"
97
+ # Legacy/useless attribute — always true (WHATWG DOM).
98
+ true
63
99
  end
64
100
  end
65
101
 
66
102
  def __js_set__(key, val)
67
103
  case key
68
- when "value", "nodeValue"
104
+ when "value", "nodeValue", "textContent"
69
105
  self.value = val
106
+ else
107
+ return Bridge::UNHANDLED
70
108
  end
71
109
 
72
110
  nil
73
111
  end
74
112
 
75
- # Methods routed through __js_call__ (keep in sync with its when-arms).
76
- JS_METHOD_NAMES = %w[cloneNode].freeze
77
- def __js_method_names__
78
- JS_METHOD_NAMES
79
- end
80
-
81
- def __js_call__(method, _args)
113
+ include Bridge::Methods
114
+ js_methods %w[cloneNode isSameNode getRootNode hasChildNodes normalize compareDocumentPosition
115
+ appendChild insertBefore removeChild replaceChild
116
+ addEventListener removeEventListener dispatchEvent]
117
+ def __js_call__(method, args)
82
118
  case method
83
119
  when "cloneNode"
84
- Attr.new(@name, owner: nil, value: value)
120
+ Attr.new(@name, owner: nil, value: value,
121
+ namespace_uri: @namespace_uri, prefix: @prefix, local_name: @local_name)
122
+ when "isSameNode"
123
+ is_same_node(args[0])
124
+ when "getRootNode"
125
+ get_root_node(args[0])
126
+ when "compareDocumentPosition"
127
+ compare_document_position(args[0])
128
+ when "appendChild", "insertBefore"
129
+ raise DOMException::HierarchyRequestError, "an Attr may not have children"
130
+ when "removeChild", "replaceChild"
131
+ raise DOMException::NotFoundError, "the node to be removed is not a child of this node"
132
+ when "hasChildNodes"
133
+ false
134
+ when "normalize"
135
+ nil
136
+ when "addEventListener"
137
+ add_event_listener(args[0], args[1], args[2])
138
+ when "removeEventListener"
139
+ remove_event_listener(args[0], args[1], args[2])
140
+ when "dispatchEvent"
141
+ dispatch_event(args[0])
85
142
  end
86
143
  end
87
144
 
@@ -112,49 +169,134 @@ module Dommy
112
169
 
113
170
  def initialize(element)
114
171
  @element = element
172
+ # Attr-node identity cache, keyed by [namespace_or_nil, localName].
173
+ # Every accessor (item / index / getNamedItem(NS)) returns the SAME
174
+ # Attr object for a given underlying attribute, per the DOM.
175
+ @attrs = {}
115
176
  end
116
177
 
117
178
  def length
118
- @element.__dommy_backend_node__.attribute_nodes.size
179
+ Backend.attribute_nodes(@element.__dommy_backend_node__).size
119
180
  end
120
181
 
121
182
  alias size length
122
183
 
123
184
  def item(index)
124
- name = @element.__dommy_backend_node__.attribute_nodes[index.to_i]&.name
125
- name && Attr.new(name, owner: @element)
185
+ node = Backend.attribute_nodes(@element.__dommy_backend_node__)[index.to_i]
186
+ node && attr_for(node)
187
+ end
188
+
189
+ # Return the cached Attr for a backend attribute node, creating (and
190
+ # caching) one on first access so DOM node identity holds.
191
+ def attr_for(attr_node)
192
+ info = Backend.attribute_ns_info(attr_node)
193
+ key = [info[:namespace_uri], info[:local_name]]
194
+ cached = @attrs[key]
195
+ return cached if cached && cached.owner_element.equal?(@element)
196
+
197
+ attr = Attr.new(info[:qualified_name], owner: @element,
198
+ namespace_uri: info[:namespace_uri],
199
+ prefix: info[:prefix],
200
+ local_name: info[:local_name])
201
+ @attrs[key] = attr
202
+ attr
126
203
  end
127
204
 
128
205
  def get_named_item(name)
129
206
  key = name.to_s.downcase
130
- return nil unless @element.__dommy_backend_node__.key?(key)
131
-
132
- Attr.new(key, owner: @element)
207
+ node = Backend.attribute_nodes(@element.__dommy_backend_node__).find do |a|
208
+ Backend.attribute_ns_info(a)[:qualified_name] == key
209
+ end
210
+ node && attr_for(node)
133
211
  end
134
212
 
135
213
  def set_named_item(attr)
136
- return nil unless attr.is_a?(Attr)
137
-
138
- key = attr.name
139
- val = attr.value
140
- attr.__internal_attach__(@element)
141
- @element.set_attribute(key, val)
142
- attr
214
+ set_attribute_node(attr)
143
215
  end
144
216
 
145
217
  def remove_named_item(name)
146
218
  key = name.to_s.downcase
147
- return nil unless @element.__dommy_backend_node__.key?(key)
219
+ node = Backend.attribute_nodes(@element.__dommy_backend_node__).find do |a|
220
+ Backend.attribute_ns_info(a)[:qualified_name] == key
221
+ end
222
+ return nil unless node
148
223
 
149
- attr = Attr.new(key, owner: nil, value: @element.__dommy_backend_node__[key].to_s)
224
+ removed = attr_for(node)
150
225
  @element.remove_attribute(key)
151
- attr
226
+ removed
152
227
  end
153
228
 
154
- def each(&blk)
155
- @element.__dommy_backend_node__.attribute_nodes.each do |a|
156
- yield Attr.new(a.name, owner: @element)
229
+ def each
230
+ Backend.attribute_nodes(@element.__dommy_backend_node__).each do |a|
231
+ yield attr_for(a)
232
+ end
233
+ end
234
+
235
+ # WHATWG "set an attribute" / "set attribute node". Adopts `attr` (the
236
+ # exact object — identity is preserved), replacing any attribute with the
237
+ # same (namespace, localName) and returning the previous Attr (detached),
238
+ # or nil. Throws InUseAttributeError if `attr` is bound to another element.
239
+ def set_attribute_node(attr)
240
+ return nil unless attr.is_a?(Attr)
241
+
242
+ owner = attr.owner_element
243
+ if owner && !owner.equal?(@element)
244
+ raise DOMException::InUseAttributeError, "attribute is in use by another element"
245
+ end
246
+
247
+ ns = attr.namespace_uri
248
+ local = attr.local_name
249
+ old = get_named_item_ns(ns, local)
250
+ return attr if old && old.equal?(attr)
251
+
252
+ value = attr.value
253
+ key = [ns, local]
254
+ if old
255
+ old.__internal_detach__
256
+ @attrs.delete(key)
257
+ end
258
+ attr.__internal_attach__(@element)
259
+ if ns
260
+ @element.set_attribute_ns(ns, attr.name, value)
261
+ else
262
+ @element.set_attribute(attr.name, value)
157
263
  end
264
+ @attrs[key] = attr
265
+ old
266
+ end
267
+
268
+ # Detach and evict the cached Attr for (namespace, localName), if any —
269
+ # called by Element after the underlying attribute is removed so a held
270
+ # reference reports `ownerElement === null`.
271
+ def __internal_evict__(namespace, local_name)
272
+ key = [namespace.to_s.empty? ? nil : namespace.to_s, local_name.to_s]
273
+ attr = @attrs.delete(key)
274
+ attr&.__internal_detach__
275
+ nil
276
+ end
277
+
278
+ # ----- Namespaced named-item access (getNamedItemNS etc.) -----
279
+
280
+ def get_named_item_ns(namespace, local_name)
281
+ node = Backend.attribute_nodes(@element.__dommy_backend_node__).find do |a|
282
+ info = Backend.attribute_ns_info(a)
283
+ info[:local_name] == local_name.to_s &&
284
+ (info[:namespace_uri] || nil) == (namespace.to_s.empty? ? nil : namespace.to_s)
285
+ end
286
+ node && attr_for(node)
287
+ end
288
+
289
+ # setNamedItemNS shares the "set an attribute" algorithm with setNamedItem.
290
+ def set_named_item_ns(attr)
291
+ set_attribute_node(attr)
292
+ end
293
+
294
+ def remove_named_item_ns(namespace, local_name)
295
+ existing = get_named_item_ns(namespace, local_name)
296
+ return nil unless existing
297
+
298
+ @element.remove_attribute_ns(namespace, local_name)
299
+ existing
158
300
  end
159
301
 
160
302
  # Property-style access — `el.attributes.id`, `el.attributes["class"]`.
@@ -181,12 +323,17 @@ module Dommy
181
323
  end
182
324
  end
183
325
 
184
- # Methods routed through __js_call__ (keep in sync with its when-arms).
185
- JS_METHOD_NAMES = %w[item getNamedItem setNamedItem removeNamedItem].freeze
186
- def __js_method_names__
187
- JS_METHOD_NAMES
326
+ # WebIDL "supported property names" for NamedNodeMap: the qualified name of
327
+ # each attribute, in order (the indexed names are reflected separately).
328
+ def __js_named_props__
329
+ Backend.attribute_nodes(@element.__dommy_backend_node__).map do |a|
330
+ Backend.attribute_ns_info(a)[:qualified_name]
331
+ end
188
332
  end
189
333
 
334
+ include Bridge::Methods
335
+ js_methods %w[item getNamedItem setNamedItem removeNamedItem
336
+ getNamedItemNS setNamedItemNS removeNamedItemNS]
190
337
  def __js_call__(method, args)
191
338
  case method
192
339
  when "item"
@@ -197,6 +344,12 @@ module Dommy
197
344
  set_named_item(args[0])
198
345
  when "removeNamedItem"
199
346
  remove_named_item(args[0])
347
+ when "getNamedItemNS"
348
+ get_named_item_ns(args[0], args[1])
349
+ when "setNamedItemNS"
350
+ set_named_item_ns(args[0])
351
+ when "removeNamedItemNS"
352
+ remove_named_item_ns(args[0], args[1])
200
353
  end
201
354
  end
202
355
 
@@ -21,6 +21,11 @@ module Dommy
21
21
  ::Nokogiri::HTML5(html.to_s, max_errors: 0)
22
22
  end
23
23
 
24
+ # Parse an XML string into an XML document (DOMParser "text/xml" etc.).
25
+ def parse_xml(xml)
26
+ ::Nokogiri::XML(xml.to_s)
27
+ end
28
+
24
29
  def fragment(html, owner_doc:)
25
30
  # owner_doc is unused by Nokogiri — the fragment carries its
26
31
  # own document. The Parser layer copies nodes into the target.
@@ -39,6 +44,12 @@ module Dommy
39
44
  ::Nokogiri::XML::Comment.new(doc, content)
40
45
  end
41
46
 
47
+ def create_cdata(content, doc)
48
+ ::Nokogiri::XML::CDATA.new(doc, content.to_s)
49
+ end
50
+
51
+ def cdata_class = ::Nokogiri::XML::CDATA
52
+
42
53
  def namespace_of(node)
43
54
  node.namespace
44
55
  end
@@ -46,6 +57,71 @@ module Dommy
46
57
  def add_namespace_definition(node, prefix, href)
47
58
  node.add_namespace_definition(prefix, href)
48
59
  end
60
+
61
+ # ----- Namespaced attributes -----
62
+
63
+ def get_attribute_ns(node, namespace, local_name)
64
+ find_attr_ns(node, namespace, local_name)&.value
65
+ end
66
+
67
+ def has_attribute_ns?(node, namespace, local_name)
68
+ !find_attr_ns(node, namespace, local_name).nil?
69
+ end
70
+
71
+ def set_attribute_ns(node, namespace, prefix, local_name, qualified_name, value)
72
+ # WHATWG "set an attribute value": when an attribute with this
73
+ # (namespace, localName) already exists, only its value changes —
74
+ # the existing prefix/qualified name is preserved, not replaced by
75
+ # the one in this call.
76
+ existing = find_attr_ns(node, namespace, local_name)
77
+ if existing
78
+ existing.value = value.to_s
79
+ return value.to_s
80
+ end
81
+
82
+ if namespace.nil? || namespace.to_s.empty?
83
+ node[qualified_name] = value.to_s
84
+ return value.to_s
85
+ end
86
+
87
+ # Defining the namespace before the qualified-name assignment lets
88
+ # libxml2 bind the prefix to it (verified behavior). Reuse an existing
89
+ # matching definition so repeated sets don't pile up declarations.
90
+ node.namespace_definitions.find { |d| d.href == namespace.to_s && d.prefix == prefix } ||
91
+ node.add_namespace_definition(prefix, namespace.to_s)
92
+ node[qualified_name] = value.to_s
93
+ value.to_s
94
+ end
95
+
96
+ def remove_attribute_ns(node, namespace, local_name)
97
+ find_attr_ns(node, namespace, local_name)&.remove
98
+ nil
99
+ end
100
+
101
+ def attribute_ns_info(attr_node)
102
+ ns = attr_node.namespace
103
+ {
104
+ namespace_uri: ns&.href,
105
+ prefix: ns&.prefix,
106
+ local_name: attr_node.name,
107
+ qualified_name: ns&.prefix ? "#{ns.prefix}:#{attr_node.name}" : attr_node.name,
108
+ value: attr_node.value,
109
+ }
110
+ end
111
+
112
+ def attribute_nodes(node)
113
+ node.attribute_nodes
114
+ end
115
+
116
+ # Finds the attribute node matching (namespace, localName). A null
117
+ # namespace matches only the un-namespaced attribute of that local name.
118
+ def find_attr_ns(node, namespace, local_name)
119
+ if namespace.nil? || namespace.to_s.empty?
120
+ node.attribute_nodes.find { |a| a.namespace.nil? && a.name == local_name.to_s }
121
+ else
122
+ node.attribute_with_ns(local_name.to_s, namespace.to_s)
123
+ end
124
+ end
49
125
  end
50
126
  end
51
127
  end
@@ -63,6 +63,43 @@ module Dommy
63
63
  # No-op: Nokolexbor doesn't support XML namespaces.
64
64
  end
65
65
 
66
+ # ----- Namespaced attributes (degraded) -----
67
+ # Nokolexbor has no namespace model, so *AttributeNS collapses to the
68
+ # qualified name in the null namespace. Fine for HTML (all attributes are
69
+ # null-namespace); foreign-content (SVG/MathML) fidelity is lost.
70
+
71
+ def get_attribute_ns(node, _namespace, local_name)
72
+ node[local_name.to_s]
73
+ end
74
+
75
+ def has_attribute_ns?(node, _namespace, local_name)
76
+ node.key?(local_name.to_s)
77
+ end
78
+
79
+ def set_attribute_ns(node, _namespace, _prefix, _local_name, qualified_name, value)
80
+ node[qualified_name] = value.to_s
81
+ value.to_s
82
+ end
83
+
84
+ def remove_attribute_ns(node, _namespace, local_name)
85
+ node.remove_attribute(local_name.to_s) if node.key?(local_name.to_s)
86
+ nil
87
+ end
88
+
89
+ def attribute_ns_info(attr_node)
90
+ {
91
+ namespace_uri: nil,
92
+ prefix: nil,
93
+ local_name: attr_node.name,
94
+ qualified_name: attr_node.name,
95
+ value: attr_node.value,
96
+ }
97
+ end
98
+
99
+ def attribute_nodes(node)
100
+ node.attribute_nodes
101
+ end
102
+
66
103
  # Internal helper — visible to allow testing.
67
104
  def in_svg_subtree?(node)
68
105
  return true if node.name.to_s.downcase == "svg"
data/lib/dommy/backend.rb CHANGED
@@ -49,6 +49,12 @@ module Dommy
49
49
  current.parse(html)
50
50
  end
51
51
 
52
+ # Parse XML input into an XML document. Backends without a real XML parser
53
+ # (HTML-only, e.g. Nokolexbor) fall back to the HTML parser.
54
+ def parse_xml(xml)
55
+ current.respond_to?(:parse_xml) ? current.parse_xml(xml) : current.parse(xml)
56
+ end
57
+
52
58
  def fragment(html, owner_doc:)
53
59
  current.fragment(html, owner_doc: owner_doc)
54
60
  end
@@ -65,6 +71,16 @@ module Dommy
65
71
  current.create_comment(content, doc)
66
72
  end
67
73
 
74
+ # CDATA section node (XML documents). Backends without CDATA fall back to a
75
+ # text node.
76
+ def create_cdata(content, doc)
77
+ current.respond_to?(:create_cdata) ? current.create_cdata(content, doc) : current.create_text(content, doc)
78
+ end
79
+
80
+ def cdata_class
81
+ current.respond_to?(:cdata_class) ? current.cdata_class : nil
82
+ end
83
+
68
84
  def namespace_of(node)
69
85
  current.namespace_of(node)
70
86
  end
@@ -73,6 +89,36 @@ module Dommy
73
89
  current.add_namespace_definition(node, prefix, href)
74
90
  end
75
91
 
92
+ # Namespaced attribute access (DOM *AttributeNS). `namespace` is an href
93
+ # String or nil. Nokolexbor degrades to qualified-name (null-namespace).
94
+ def get_attribute_ns(node, namespace, local_name)
95
+ current.get_attribute_ns(node, namespace, local_name)
96
+ end
97
+
98
+ def set_attribute_ns(node, namespace, prefix, local_name, qualified_name, value)
99
+ current.set_attribute_ns(node, namespace, prefix, local_name, qualified_name, value)
100
+ end
101
+
102
+ def remove_attribute_ns(node, namespace, local_name)
103
+ current.remove_attribute_ns(node, namespace, local_name)
104
+ end
105
+
106
+ def has_attribute_ns?(node, namespace, local_name)
107
+ current.has_attribute_ns?(node, namespace, local_name)
108
+ end
109
+
110
+ # Reads a backend attribute node into {namespace_uri:, prefix:,
111
+ # local_name:, qualified_name:, value:} (namespace-aware).
112
+ def attribute_ns_info(attr_node)
113
+ current.attribute_ns_info(attr_node)
114
+ end
115
+
116
+ # The element's attribute nodes (each readable via attribute_ns_info).
117
+ # The single choke point so DOM code doesn't touch parser internals.
118
+ def attribute_nodes(node)
119
+ current.attribute_nodes(node)
120
+ end
121
+
76
122
  # Type constants — proxy through to the current backend so
77
123
  # `node.is_a?(Backend::Element)` resolves dynamically.
78
124
  def element_class
data/lib/dommy/blob.rb CHANGED
@@ -17,12 +17,16 @@ module Dommy
17
17
  # - anything else: coerced via to_s
18
18
  #
19
19
  # `options["type"]` sets the MIME type (lowercased per spec).
20
- def initialize(parts = [], options = {})
20
+ # `window` (optional) lets the JS-facing `text()`/`arrayBuffer()` return real
21
+ # Promises (they need a scheduler). A window-less Blob falls back to a
22
+ # synchronous result, which `await` still handles.
23
+ def initialize(parts = [], options = {}, window = nil)
21
24
  parts = [parts] unless parts.is_a?(Array)
22
25
  @data = collect_bytes(parts)
23
26
  @size = @data.bytesize
24
27
  raw_type = options["type"] || options[:type] || ""
25
28
  @type = raw_type.to_s.downcase
29
+ @window = window
26
30
  end
27
31
 
28
32
  # Return a new Blob over a byte range of this one.
@@ -31,7 +35,7 @@ module Dommy
31
35
  s = clamp_index(start.to_i, @size)
32
36
  e = clamp_index(last.to_i, @size)
33
37
  e = s if e < s
34
- Blob.new([@data.byteslice(s, e - s) || ""], "type" => content_type.to_s)
38
+ Blob.new([@data.byteslice(s, e - s) || ""], {"type" => content_type.to_s}, @window)
35
39
  end
36
40
 
37
41
  # Read the bytes as UTF-8 text. The DOM spec returns a Promise,
@@ -40,10 +44,11 @@ module Dommy
40
44
  @data.dup.force_encoding(Encoding::UTF_8)
41
45
  end
42
46
 
43
- # Read the bytes as an Array<Integer>. The DOM spec returns a
44
- # Promise<ArrayBuffer>; Dommy is synchronous.
47
+ # Read the bytes as a real ArrayBuffer (the spec return type, wrapped so it
48
+ # crosses the JS boundary as a bare ArrayBuffer rather than an Array/typed
49
+ # array). The DOM spec returns a Promise<ArrayBuffer>; Dommy is synchronous.
45
50
  def array_buffer
46
- @data.bytes
51
+ Bridge::ArrayBuffer.new(@data.bytes)
47
52
  end
48
53
 
49
54
  # Raw binary bytes (Ruby ASCII-8BIT string). Used by FormData /
@@ -61,19 +66,31 @@ module Dommy
61
66
  end
62
67
  end
63
68
 
69
+ # Methods routed through __js_call__ (keep in sync with its when-arms).
70
+ # File < Blob inherits these (it adds only properties).
71
+ include Bridge::Methods
72
+ js_methods %w[slice text arrayBuffer]
64
73
  def __js_call__(method, args)
65
74
  case method
66
75
  when "slice"
67
76
  slice(args[0] || 0, args[1] || @size, args[2] || "")
68
77
  when "text"
69
- text
78
+ # WHATWG: Blob.text() returns a Promise<string>.
79
+ promise_or_value(text)
70
80
  when "arrayBuffer"
71
- array_buffer
81
+ # WHATWG: Blob.arrayBuffer() returns a Promise<ArrayBuffer>.
82
+ promise_or_value(array_buffer)
72
83
  end
73
84
  end
74
85
 
75
86
  private
76
87
 
88
+ # Wrap a consumed value in a resolved Promise when a window is available;
89
+ # otherwise return it directly (a window-less Blob — `await` copes either way).
90
+ def promise_or_value(value)
91
+ @window ? PromiseValue.resolve(@window, value) : value
92
+ end
93
+
77
94
  def collect_bytes(parts)
78
95
  buf = String.new(encoding: Encoding::ASCII_8BIT)
79
96
  parts.each do |part|
@@ -106,8 +123,8 @@ module Dommy
106
123
  class File < Blob
107
124
  attr_reader :name, :last_modified
108
125
 
109
- def initialize(parts, name, options = {})
110
- super(parts, options)
126
+ def initialize(parts, name, options = {}, window = nil)
127
+ super(parts, options, window)
111
128
  @name = name.to_s
112
129
  raw_lm = options["lastModified"] || options[:lastModified]
113
130
  @last_modified = (raw_lm || (Time.now.to_f * 1000)).to_i
@@ -172,6 +189,8 @@ module Dommy
172
189
  end
173
190
  end
174
191
 
192
+ include Bridge::Methods
193
+ js_methods %w[item]
175
194
  def __js_call__(method, args)
176
195
  case method
177
196
  when "item"