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
@@ -67,11 +67,17 @@ module Dommy
67
67
  remove_event_listener("change", @onchange) if @onchange
68
68
  @onchange = value
69
69
  add_event_listener("change", value) if value
70
+ else
71
+ return Bridge::UNHANDLED
70
72
  end
71
73
 
72
74
  nil
73
75
  end
74
76
 
77
+ include Bridge::Methods
78
+ js_methods %w[
79
+ matches addListener removeListener addEventListener removeEventListener dispatchEvent
80
+ ]
75
81
  def __js_call__(method, args)
76
82
  case method
77
83
  when "matches"
@@ -83,7 +89,7 @@ module Dommy
83
89
  when "addEventListener"
84
90
  add_event_listener(args[0], args[1], args[2])
85
91
  when "removeEventListener"
86
- remove_event_listener(args[0], args[1])
92
+ remove_event_listener(args[0], args[1], args[2])
87
93
  when "dispatchEvent"
88
94
  dispatch_event(args[0])
89
95
  end
@@ -100,11 +100,15 @@ module Dommy
100
100
  @inline_message_handler = value
101
101
  add_event_listener("message", value) if value
102
102
  start if value
103
+ else
104
+ return Bridge::UNHANDLED
103
105
  end
104
106
 
105
107
  nil
106
108
  end
107
109
 
110
+ include Bridge::Methods
111
+ js_methods %w[postMessage start close addEventListener removeEventListener dispatchEvent]
108
112
  def __js_call__(method, args)
109
113
  case method
110
114
  when "postMessage"
@@ -116,7 +120,7 @@ module Dommy
116
120
  when "addEventListener"
117
121
  add_event_listener(args[0], args[1], args[2])
118
122
  when "removeEventListener"
119
- remove_event_listener(args[0], args[1])
123
+ remove_event_listener(args[0], args[1], args[2])
120
124
  when "dispatchEvent"
121
125
  dispatch_event(args[0])
122
126
  end
@@ -157,6 +161,28 @@ module Dommy
157
161
  super
158
162
  end
159
163
  end
164
+
165
+ js_methods %w[initMessageEvent]
166
+ def __js_call__(method, args)
167
+ case method
168
+ when "initMessageEvent"
169
+ # Deprecated initMessageEvent(type, bubbles, cancelable, data, origin,
170
+ # lastEventId, source, ports); a no-op while the event is dispatching.
171
+ raise Bridge::TypeError, "initMessageEvent requires a type argument" if args.empty?
172
+
173
+ unless @dispatch_flag
174
+ init_event(args[0], args[1], args[2])
175
+ @data = args[3]
176
+ @origin = (args[4] || "").to_s
177
+ @last_event_id = (args[5] || "").to_s
178
+ @source = args[6]
179
+ @ports = args[7] || []
180
+ end
181
+ nil
182
+ else
183
+ super
184
+ end
185
+ end
160
186
  end
161
187
 
162
188
  # `BroadcastChannel` — same-origin pub/sub. Dommy keeps a per-window
@@ -222,11 +248,15 @@ module Dommy
222
248
  remove_event_listener("message", @onmessage) if @onmessage
223
249
  @onmessage = value
224
250
  add_event_listener("message", value) if value
251
+ else
252
+ return Bridge::UNHANDLED
225
253
  end
226
254
 
227
255
  nil
228
256
  end
229
257
 
258
+ include Bridge::Methods
259
+ js_methods %w[postMessage close addEventListener removeEventListener dispatchEvent]
230
260
  def __js_call__(method, args)
231
261
  case method
232
262
  when "postMessage"
@@ -236,7 +266,7 @@ module Dommy
236
266
  when "addEventListener"
237
267
  add_event_listener(args[0], args[1], args[2])
238
268
  when "removeEventListener"
239
- remove_event_listener(args[0], args[1])
269
+ remove_event_listener(args[0], args[1], args[2])
240
270
  when "dispatchEvent"
241
271
  dispatch_event(args[0])
242
272
  end
@@ -75,8 +75,11 @@ module Dommy
75
75
  @observed = []
76
76
  @records = []
77
77
  @scheduled = false
78
+ @registered_docs = []
78
79
  end
79
80
 
81
+ include Bridge::Methods
82
+ js_methods %w[observe disconnect takeRecords]
80
83
  def __js_call__(method, args)
81
84
  case method
82
85
  when "observe"
@@ -136,16 +139,29 @@ module Dommy
136
139
  # is supplied; `characterData: true` is implied if
137
140
  # characterDataOldValue is supplied. Matches the spec's option
138
141
  # normalization in MutationObserverInit.
139
- attrs_implied = !attribute_filter.nil? || truthy_option(opts, "attributeOldValue")
140
- char_implied = truthy_option(opts, "characterDataOldValue")
141
- attributes_on = truthy_option(opts, "attributes") || attrs_implied
142
+ # `attributes`/`characterData` are *implied* true only when their
143
+ # old-value/filter companion is present AND the member itself is omitted.
144
+ # If the member is present but false, that companion is a TypeError.
145
+ attr_present = opts.key?("attributes") || opts.key?(:attributes)
146
+ char_present = opts.key?("characterData") || opts.key?(:characterData)
147
+ attrs_extras = !attribute_filter.nil? || truthy_option(opts, "attributeOldValue")
148
+ char_extras = truthy_option(opts, "characterDataOldValue")
149
+
150
+ if attrs_extras && attr_present && !truthy_option(opts, "attributes")
151
+ raise Bridge::TypeError, "attributeOldValue/attributeFilter requires attributes to be true"
152
+ end
153
+ if char_extras && char_present && !truthy_option(opts, "characterData")
154
+ raise Bridge::TypeError, "characterDataOldValue requires characterData to be true"
155
+ end
156
+
157
+ attributes_on = truthy_option(opts, "attributes") || (attrs_extras && !attr_present)
142
158
  child_list_on = truthy_option(opts, "childList")
143
- character_data_on = truthy_option(opts, "characterData") || char_implied
159
+ character_data_on = truthy_option(opts, "characterData") || (char_extras && !char_present)
144
160
 
145
161
  # Per spec, observe() must request at least one of childList,
146
162
  # attributes, or characterData; otherwise TypeError.
147
163
  unless child_list_on || attributes_on || character_data_on
148
- raise TypeError, "MutationObserver.observe: at least one of childList, attributes, characterData must be true"
164
+ raise Bridge::TypeError, "MutationObserver.observe: at least one of childList, attributes, characterData must be true"
149
165
  end
150
166
 
151
167
  entry = {
@@ -169,7 +185,14 @@ module Dommy
169
185
  @observed << entry
170
186
  end
171
187
 
172
- @document.register_observer(self)
188
+ # Register on the TARGET's node-document — not just the observer's own
189
+ # document — so an observer watching a node in another document (e.g. a
190
+ # DOMParser-created XML document) is reached by that document's mutations.
191
+ doc = document_for_target(target)
192
+ unless @registered_docs.include?(doc)
193
+ doc.register_observer(self)
194
+ @registered_docs << doc
195
+ end
173
196
  nil
174
197
  end
175
198
 
@@ -177,10 +200,19 @@ module Dommy
177
200
  @records.clear
178
201
  @scheduled = false
179
202
  @observed.clear
180
- @document.unregister_observer(self)
203
+ @registered_docs.each { |doc| doc.unregister_observer(self) }
204
+ @registered_docs.clear
181
205
  nil
182
206
  end
183
207
 
208
+ # The node-document a target belongs to (the target itself when it is a
209
+ # Document).
210
+ def document_for_target(target)
211
+ return target if target.is_a?(Dommy::Document)
212
+
213
+ target.instance_variable_get(:@document) || @document
214
+ end
215
+
184
216
  def take_records
185
217
  out = @records.dup
186
218
  @records.clear
@@ -194,16 +226,27 @@ module Dommy
194
226
 
195
227
  records = @records.dup
196
228
  @records.clear
197
- if @callback.respond_to?(:__js_call__)
198
- @callback.__js_call__("call", [records])
229
+ # Per spec the callback receives (mutationRecords, observer) and is invoked
230
+ # with `this` set to the observer.
231
+ if @callback.respond_to?(:__js_call_with_this__)
232
+ @callback.__js_call_with_this__([records, self], self)
233
+ elsif @callback.respond_to?(:__js_call__)
234
+ @callback.__js_call__("call", [records, self])
199
235
  elsif @callback.respond_to?(:call)
200
- @callback.call(records)
236
+ @callback.call(records, self)
201
237
  end
202
238
  end
203
239
 
240
+ # A MutationObserverInit member is a WebIDL `boolean`, so its value is
241
+ # converted with JS ToBoolean — any object (e.g. `attributes: ["abc"]`) is
242
+ # truthy; only false / 0 / "" / null / undefined / NaN are falsy.
204
243
  def truthy_option(hash, key)
205
- value = hash[key] || hash[key.to_sym]
206
- value == true || value.to_s == "true"
244
+ value = hash.key?(key) ? hash[key] : hash[key.to_sym]
245
+ return false if value.nil? || value == false || value == 0 || value == ""
246
+ return false if defined?(Bridge::UNDEFINED) && value.equal?(Bridge::UNDEFINED)
247
+ return false if value.is_a?(Float) && value.nan?
248
+
249
+ true
207
250
  end
208
251
  end
209
252
  end
@@ -106,6 +106,8 @@ module Dommy
106
106
  end
107
107
  end
108
108
 
109
+ include Bridge::Methods
110
+ js_methods %w[share canShare vibrate getBattery]
109
111
  def __js_call__(method, args)
110
112
  case method
111
113
  when "share"
@@ -120,7 +122,7 @@ module Dommy
120
122
  end
121
123
 
122
124
  def __js_set__(_key, _value)
123
- nil
125
+ Bridge::UNHANDLED
124
126
  end
125
127
  end
126
128
 
@@ -171,9 +173,11 @@ module Dommy
171
173
  end
172
174
 
173
175
  def __js_set__(_key, _value)
174
- nil
176
+ Bridge::UNHANDLED
175
177
  end
176
178
 
179
+ include Bridge::Methods
180
+ js_methods %w[readText writeText read write addEventListener removeEventListener dispatchEvent]
177
181
  def __js_call__(method, args)
178
182
  case method
179
183
  when "readText"
@@ -187,7 +191,7 @@ module Dommy
187
191
  when "addEventListener"
188
192
  add_event_listener(args[0], args[1], args[2])
189
193
  when "removeEventListener"
190
- remove_event_listener(args[0], args[1])
194
+ remove_event_listener(args[0], args[1], args[2])
191
195
  when "dispatchEvent"
192
196
  dispatch_event(args[0])
193
197
  end
@@ -259,9 +263,11 @@ module Dommy
259
263
  end
260
264
 
261
265
  def __js_set__(_key, _value)
262
- nil
266
+ Bridge::UNHANDLED
263
267
  end
264
268
 
269
+ include Bridge::Methods
270
+ js_methods %w[query]
265
271
  def __js_call__(method, args)
266
272
  case method
267
273
  when "query"
@@ -311,17 +317,21 @@ module Dommy
311
317
  remove_event_listener("change", @onchange) if @onchange
312
318
  @onchange = value
313
319
  add_event_listener("change", value) if value
320
+ else
321
+ return Bridge::UNHANDLED
314
322
  end
315
323
 
316
324
  nil
317
325
  end
318
326
 
327
+ include Bridge::Methods
328
+ js_methods %w[addEventListener removeEventListener dispatchEvent]
319
329
  def __js_call__(method, args)
320
330
  case method
321
331
  when "addEventListener"
322
332
  add_event_listener(args[0], args[1], args[2])
323
333
  when "removeEventListener"
324
- remove_event_listener(args[0], args[1])
334
+ remove_event_listener(args[0], args[1], args[2])
325
335
  when "dispatchEvent"
326
336
  dispatch_event(args[0])
327
337
  end
@@ -393,6 +403,8 @@ module Dommy
393
403
 
394
404
  alias clearWatch clear_watch
395
405
 
406
+ include Bridge::Methods
407
+ js_methods %w[getCurrentPosition watchPosition clearWatch]
396
408
  def __js_call__(method, args)
397
409
  case method
398
410
  when "getCurrentPosition"
@@ -415,13 +427,7 @@ module Dommy
415
427
  end
416
428
 
417
429
  def invoke(callback, payload)
418
- return if callback.nil?
419
-
420
- if callback.respond_to?(:__js_call__)
421
- callback.__js_call__("call", [payload])
422
- elsif callback.respond_to?(:call)
423
- callback.call(payload)
424
- end
430
+ CallableInvoker.invoke(callback, payload)
425
431
  end
426
432
  end
427
433
 
@@ -439,6 +445,8 @@ module Dommy
439
445
  PromiseValue.resolve(@window, WakeLockSentinel.new(@window, type.to_s))
440
446
  end
441
447
 
448
+ include Bridge::Methods
449
+ js_methods %w[request]
442
450
  def __js_call__(method, args)
443
451
  case method
444
452
  when "request"
@@ -479,6 +487,8 @@ module Dommy
479
487
  end
480
488
  end
481
489
 
490
+ include Bridge::Methods
491
+ js_methods %w[release]
482
492
  def __js_call__(method, _args)
483
493
  case method
484
494
  when "release"
@@ -561,6 +571,8 @@ module Dommy
561
571
  PromiseValue.resolve(@window, {"held" => held, "pending" => []})
562
572
  end
563
573
 
574
+ include Bridge::Methods
575
+ js_methods %w[request query]
564
576
  def __js_call__(method, args)
565
577
  case method
566
578
  when "request"
@@ -617,6 +629,8 @@ module Dommy
617
629
  PromiseValue.resolve(@window, @persisted)
618
630
  end
619
631
 
632
+ include Bridge::Methods
633
+ js_methods %w[estimate persist persisted]
620
634
  def __js_call__(method, _args)
621
635
  case method
622
636
  when "estimate"
data/lib/dommy/node.rb CHANGED
@@ -10,12 +10,6 @@ module Dommy
10
10
  # snapshots tree state at the time of the query, matching what
11
11
  # most happy-dom test patterns expect.
12
12
  class NodeList < Array
13
- # Methods routed through __js_call__ (keep in sync with its when-arms).
14
- JS_METHOD_NAMES = %w[item forEach entries keys values].freeze
15
- def __js_method_names__
16
- JS_METHOD_NAMES
17
- end
18
-
19
13
  # Spec-compliant: out-of-range returns nil, not raise (Array#[] is
20
14
  # close but we make negative indices fail too — DOM `item(-1)` is
21
15
  # nil, not Array#[-1]'s last element).
@@ -60,24 +54,24 @@ module Dommy
60
54
  when "length"
61
55
  length
62
56
  else
63
- if key.is_a?(Integer) || key.to_s.match?(/\A\d+\z/)
64
- item(key.to_i)
57
+ # Indexed getter: out-of-range yields JS `undefined` (item() returns null).
58
+ if key.is_a?(Integer) || key.to_s.match?(/\A-?\d+\z/)
59
+ token = item(key.to_i)
60
+ token.nil? ? Bridge::UNDEFINED : token
65
61
  end
66
62
  end
67
63
  end
68
64
 
65
+ include Bridge::Methods
66
+ # keys/values/entries/Symbol.iterator are provided JS-side (the array-like
67
+ # prototype) so they return real iterators, not arrays — see host_runtime.js.
68
+ js_methods %w[item forEach]
69
69
  def __js_call__(method, args)
70
70
  case method
71
71
  when "item"
72
72
  item(args[0])
73
73
  when "forEach"
74
74
  for_each(&args[0])
75
- when "entries"
76
- entries
77
- when "keys"
78
- keys
79
- when "values"
80
- values
81
75
  end
82
76
  end
83
77
  end
@@ -94,12 +88,6 @@ module Dommy
94
88
  class LiveNodeList
95
89
  include Enumerable
96
90
 
97
- # Methods routed through __js_call__ (keep in sync with its when-arms).
98
- JS_METHOD_NAMES = %w[item forEach entries keys values].freeze
99
- def __js_method_names__
100
- JS_METHOD_NAMES
101
- end
102
-
103
91
  def initialize(&block)
104
92
  @compute = block
105
93
  end
@@ -175,24 +163,24 @@ module Dommy
175
163
  when "length"
176
164
  length
177
165
  else
178
- if key.is_a?(Integer) || key.to_s.match?(/\A\d+\z/)
179
- item(key.to_i)
166
+ # Indexed getter: out-of-range yields JS `undefined` (item() returns null).
167
+ if key.is_a?(Integer) || key.to_s.match?(/\A-?\d+\z/)
168
+ token = item(key.to_i)
169
+ token.nil? ? Bridge::UNDEFINED : token
180
170
  end
181
171
  end
182
172
  end
183
173
 
174
+ include Bridge::Methods
175
+ # keys/values/entries/Symbol.iterator are provided JS-side (the array-like
176
+ # prototype) so they return real iterators, not arrays — see host_runtime.js.
177
+ js_methods %w[item forEach]
184
178
  def __js_call__(method, args)
185
179
  case method
186
180
  when "item"
187
181
  item(args[0])
188
182
  when "forEach"
189
183
  for_each(&args[0])
190
- when "entries"
191
- entries
192
- when "keys"
193
- keys
194
- when "values"
195
- values
196
184
  end
197
185
  end
198
186
  end
@@ -226,5 +214,147 @@ module Dommy
226
214
  DOCUMENT_POSITION_CONTAINS = 0x08
227
215
  DOCUMENT_POSITION_CONTAINED_BY = 0x10
228
216
  DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = 0x20
217
+
218
+ # WHATWG Node.isEqualNode — deep structural equality (type-specific data
219
+ # plus equal, in-order, recursively-equal children). Available on every node
220
+ # class that includes Node; the bridge routes "isEqualNode" here.
221
+ def is_equal_node(other)
222
+ Internal::NodeEquality.equal?(self, other)
223
+ end
224
+
225
+ # Node.isSameNode — strict reference identity (deprecated alias for `===`).
226
+ def is_same_node(other)
227
+ equal?(other)
228
+ end
229
+
230
+ # Node.compareDocumentPosition(other) — a bitmask describing where `other`
231
+ # sits relative to this node: 0 for the same node, CONTAINS/CONTAINED_BY for
232
+ # ancestor/descendant, PRECEDING/FOLLOWING for tree order, or DISCONNECTED
233
+ # (with a stable IMPLEMENTATION_SPECIFIC|PRECEDING) for unrelated nodes.
234
+ # Generic over any node with a backing Nokogiri node.
235
+ def compare_document_position(other)
236
+ return 0 if equal?(other)
237
+ unless respond_to?(:__dommy_backend_node__) && other.respond_to?(:__dommy_backend_node__)
238
+ return DOCUMENT_POSITION_DISCONNECTED | DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC | DOCUMENT_POSITION_PRECEDING
239
+ end
240
+
241
+ self_node = __dommy_backend_node__
242
+ other_node = other.__dommy_backend_node__
243
+ self_ancestors = node_ancestor_chain(self_node)
244
+ other_ancestors = node_ancestor_chain(other_node)
245
+
246
+ common = self_ancestors.find { |a| other_ancestors.include?(a) }
247
+ unless common
248
+ return DOCUMENT_POSITION_DISCONNECTED | DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC | DOCUMENT_POSITION_PRECEDING
249
+ end
250
+ return DOCUMENT_POSITION_CONTAINED_BY | DOCUMENT_POSITION_FOLLOWING if common == self_node
251
+ return DOCUMENT_POSITION_CONTAINS | DOCUMENT_POSITION_PRECEDING if common == other_node
252
+
253
+ self_branch = node_branch_under(common, self_ancestors)
254
+ other_branch = node_branch_under(common, other_ancestors)
255
+ common.children.each do |child|
256
+ return DOCUMENT_POSITION_FOLLOWING if child == self_branch
257
+ return DOCUMENT_POSITION_PRECEDING if child == other_branch
258
+ end
259
+ DOCUMENT_POSITION_DISCONNECTED
260
+ end
261
+
262
+ # Node.getRootNode — the topmost ancestor of this node (the document, a
263
+ # ShadowRoot, a detached subtree root, or the node itself). Generic default
264
+ # for any node backed by a Nokogiri node; classes with special roots
265
+ # (Element's shadow handling) override it.
266
+ def get_root_node(_options = nil)
267
+ return self unless respond_to?(:__dommy_backend_node__) && instance_variable_defined?(:@document)
268
+
269
+ node = __dommy_backend_node__
270
+ node = node.parent while node.respond_to?(:parent) && node.parent
271
+ # The topmost node of an attached subtree is the Nokogiri document, which
272
+ # has no element wrapper — map it to the Document. A detached node's root is
273
+ # itself.
274
+ return @document if @document && node.equal?(@document.nokogiri_doc)
275
+
276
+ (@document && @document.wrap_node(node)) || self
277
+ end
278
+
279
+ HTML_NAMESPACE = "http://www.w3.org/1999/xhtml"
280
+
281
+ # Node.lookupNamespaceURI(prefix) — the namespace bound to `prefix` (or the
282
+ # default namespace for a null/empty prefix) in this node's scope, walking up
283
+ # the element ancestors' namespace declarations. HTML elements default to the
284
+ # XHTML namespace.
285
+ def lookup_namespace_uri(prefix)
286
+ wanted = namespace_prefix_arg(prefix)
287
+ nk = nearest_namespaceable_node
288
+ while nk.respond_to?(:element?) && nk.element?
289
+ nk.namespace_definitions.each do |d|
290
+ return d.href if normalize_ns_prefix(d.prefix) == wanted
291
+ end
292
+ return nk.namespace ? nk.namespace.href : HTML_NAMESPACE if wanted.nil?
293
+
294
+ nk = nk.parent
295
+ end
296
+ nil
297
+ end
298
+
299
+ # Node.lookupPrefix(namespace) — a prefix bound to `namespace`, or null for
300
+ # the default namespace.
301
+ def lookup_prefix(namespace)
302
+ ns = namespace.to_s
303
+ return nil if ns.empty?
304
+
305
+ nk = nearest_namespaceable_node
306
+ while nk.respond_to?(:element?) && nk.element?
307
+ nk.namespace_definitions.each do |d|
308
+ return d.prefix if d.href == ns && d.prefix
309
+ end
310
+ nk = nk.parent
311
+ end
312
+ nil
313
+ end
314
+
315
+ # Node.isDefaultNamespace(namespace) — true if `namespace` (null/"" → null) is
316
+ # the default namespace in this node's scope.
317
+ def is_default_namespace(namespace)
318
+ ns = namespace.nil? ? nil : namespace.to_s
319
+ ns = nil if ns == ""
320
+ lookup_namespace_uri(nil) == ns
321
+ end
322
+
323
+ private
324
+
325
+ def node_ancestor_chain(node)
326
+ chain = [node]
327
+ Internal::NodeTraversal.each_ancestor(node) { |n| chain << n }
328
+ chain
329
+ end
330
+
331
+ def node_branch_under(common, chain)
332
+ chain.each_with_index do |node, i|
333
+ return node if i.zero? && node == common
334
+ return node if node.respond_to?(:parent) && node.parent == common
335
+ end
336
+ nil
337
+ end
338
+
339
+ def namespace_prefix_arg(prefix)
340
+ return nil if prefix.nil? || prefix.to_s.empty?
341
+ return nil if defined?(Bridge::UNDEFINED) && prefix.equal?(Bridge::UNDEFINED)
342
+
343
+ prefix.to_s
344
+ end
345
+
346
+ def normalize_ns_prefix(prefix)
347
+ prefix.nil? || prefix.to_s.empty? ? nil : prefix.to_s
348
+ end
349
+
350
+ def nearest_namespaceable_node
351
+ return nil unless respond_to?(:__dommy_backend_node__)
352
+
353
+ nk = __dommy_backend_node__
354
+ nk = nk.parent while nk.respond_to?(:element?) && !nk.element? && nk.respond_to?(:parent) && nk.parent
355
+ nk
356
+ end
229
357
  end
230
358
  end
359
+
360
+ require_relative "internal/node_equality"
@@ -69,6 +69,8 @@ module Dommy
69
69
  end
70
70
  end
71
71
 
72
+ include Bridge::Methods
73
+ js_methods %w[close addEventListener removeEventListener dispatchEvent]
72
74
  def __js_call__(method, args)
73
75
  case method
74
76
  when "close"
@@ -76,7 +78,7 @@ module Dommy
76
78
  when "addEventListener"
77
79
  add_event_listener(args[0], args[1], args[2])
78
80
  when "removeEventListener"
79
- remove_event_listener(args[0], args[1])
81
+ remove_event_listener(args[0], args[1], args[2])
80
82
  when "dispatchEvent"
81
83
  dispatch_event(args[0])
82
84
  end
@@ -93,6 +93,10 @@ module Dommy
93
93
  end
94
94
  end
95
95
 
96
+ include Bridge::Methods
97
+ js_methods %w[
98
+ now mark measure clearMarks clearMeasures getEntries getEntriesByName getEntriesByType
99
+ ]
96
100
  def __js_call__(method, args)
97
101
  case method
98
102
  when "now"
@@ -41,6 +41,8 @@ module Dommy
41
41
  invoke_callback(entries)
42
42
  end
43
43
 
44
+ include Bridge::Methods
45
+ js_methods %w[observe disconnect takeRecords]
44
46
  def __js_call__(method, args)
45
47
  case method
46
48
  when "observe"
data/lib/dommy/promise.rb CHANGED
@@ -51,6 +51,8 @@ module Dommy
51
51
  @handlers = []
52
52
  end
53
53
 
54
+ include Bridge::Methods
55
+ js_methods %w[then catch]
54
56
  def __js_call__(method, args)
55
57
  case method
56
58
  when "then"
@@ -133,8 +135,12 @@ module Dommy
133
135
  other.__js_call__(
134
136
  "then",
135
137
  [
136
- proc { |resolved| fulfill(resolved) },
137
- proc { |reason| reject(reason) }
138
+ # Return nil, not the result of fulfill/reject: those return `self`
139
+ # (a PromiseValue), and run_handler would treat that as a thenable to
140
+ # chain onto — re-adopting forever. These are adoption sinks; their
141
+ # return value must be ignored.
142
+ proc { |resolved| fulfill(resolved); nil },
143
+ proc { |reason| reject(reason); nil }
138
144
  ]
139
145
  )
140
146
  self
@@ -167,13 +173,15 @@ module Dommy
167
173
  return
168
174
  end
169
175
 
170
- result = invoke_callback(callback, @value)
171
- if result.is_a?(PromiseValue)
176
+ result = CallableInvoker.invoke(callback, @value)
177
+ if result.is_a?(PromiseValue) && !result.equal?(handler.child)
178
+ # Adopt the returned thenable. The continuation procs return nil so their
179
+ # `self`-returning fulfill/reject don't get re-adopted (infinite chain).
172
180
  result.__js_call__(
173
181
  "then",
174
182
  [
175
- proc { |resolved| handler.child.fulfill(resolved) },
176
- proc { |reason| handler.child.reject(reason) }
183
+ proc { |resolved| handler.child.fulfill(resolved); nil },
184
+ proc { |reason| handler.child.reject(reason); nil }
177
185
  ]
178
186
  )
179
187
  else
@@ -187,13 +195,5 @@ module Dommy
187
195
  def propagate(child)
188
196
  @state == :fulfilled ? child.fulfill(@value) : child.reject(@value)
189
197
  end
190
-
191
- def invoke_callback(callback, value)
192
- if callback.respond_to?(:__js_call__)
193
- callback.__js_call__("call", [value])
194
- else
195
- callback.call(value)
196
- end
197
- end
198
198
  end
199
199
  end