dommy 0.14.0 → 0.16.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 (198) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +15 -1
  3. data/lib/dommy/attr.rb +56 -32
  4. data/lib/dommy/backend.rb +251 -208
  5. data/lib/dommy/blob.rb +24 -0
  6. data/lib/dommy/bridge/methods.rb +12 -2
  7. data/lib/dommy/bridge/values.rb +29 -2
  8. data/lib/dommy/bridge/wire_tags.rb +8 -4
  9. data/lib/dommy/browser.rb +328 -53
  10. data/lib/dommy/callable_invoker.rb +27 -31
  11. data/lib/dommy/character_data.rb +21 -28
  12. data/lib/dommy/class_list.rb +55 -23
  13. data/lib/dommy/cookie_jar.rb +408 -0
  14. data/lib/dommy/cookie_store.rb +104 -49
  15. data/lib/dommy/css.rb +37 -8
  16. data/lib/dommy/custom_elements.rb +360 -116
  17. data/lib/dommy/data_transfer.rb +22 -2
  18. data/lib/dommy/document.rb +1258 -346
  19. data/lib/dommy/dom_exception.rb +6 -0
  20. data/lib/dommy/dom_parser.rb +46 -14
  21. data/lib/dommy/dom_string_list.rb +36 -0
  22. data/lib/dommy/element.rb +408 -569
  23. data/lib/dommy/element_internals.rb +215 -0
  24. data/lib/dommy/encodings.rb +10 -0
  25. data/lib/dommy/event.rb +382 -148
  26. data/lib/dommy/event_source.rb +8 -34
  27. data/lib/dommy/fetch.rb +8 -12
  28. data/lib/dommy/file_reader.rb +5 -29
  29. data/lib/dommy/form_data.rb +53 -16
  30. data/lib/dommy/form_entry_list.rb +107 -138
  31. data/lib/dommy/fragment.rb +14 -16
  32. data/lib/dommy/history.rb +247 -90
  33. data/lib/dommy/html_canvas_element.rb +15 -35
  34. data/lib/dommy/html_collection.rb +76 -50
  35. data/lib/dommy/html_elements/base.rb +377 -71
  36. data/lib/dommy/html_elements/embedded.rb +350 -106
  37. data/lib/dommy/html_elements/forms/form.rb +424 -383
  38. data/lib/dommy/html_elements/forms/input.rb +351 -189
  39. data/lib/dommy/html_elements/forms/select.rb +97 -81
  40. data/lib/dommy/html_elements/interactive.rb +285 -72
  41. data/lib/dommy/html_elements/media.rb +73 -32
  42. data/lib/dommy/html_elements/metadata.rb +226 -78
  43. data/lib/dommy/html_elements/miscellaneous.rb +58 -14
  44. data/lib/dommy/html_elements/tables.rb +20 -42
  45. data/lib/dommy/html_elements.rb +28 -3
  46. data/lib/dommy/hyperlink_utils.rb +19 -8
  47. data/lib/dommy/interaction/drag_and_drop.rb +187 -0
  48. data/lib/dommy/interaction/event_synthesis.rb +182 -24
  49. data/lib/dommy/interaction/field_interactor.rb +18 -6
  50. data/lib/dommy/interaction/form_submission.rb +12 -6
  51. data/lib/dommy/interaction/key_sender.rb +102 -17
  52. data/lib/dommy/interaction.rb +1 -0
  53. data/lib/dommy/internal/accessibility_tree.rb +19 -7
  54. data/lib/dommy/internal/accessibility_visibility.rb +2 -2
  55. data/lib/dommy/internal/accessible_description.rb +3 -3
  56. data/lib/dommy/internal/accessible_name.rb +33 -45
  57. data/lib/dommy/internal/an_plus_b.rb +125 -0
  58. data/lib/dommy/internal/aria_role.rb +27 -18
  59. data/lib/dommy/internal/aria_state.rb +7 -7
  60. data/lib/dommy/internal/autofill.rb +2 -2
  61. data/lib/dommy/internal/backend_prefilter.rb +21 -18
  62. data/lib/dommy/internal/ce_reactions.rb +285 -0
  63. data/lib/dommy/internal/ce_reactions_table.rb +296 -0
  64. data/lib/dommy/internal/child_list.rb +39 -0
  65. data/lib/dommy/internal/child_navigable.rb +155 -0
  66. data/lib/dommy/internal/child_node.rb +44 -41
  67. data/lib/dommy/internal/close_watcher_manager.rb +238 -0
  68. data/lib/dommy/internal/complex_match.rb +129 -0
  69. data/lib/dommy/internal/constraint_validation.rb +130 -0
  70. data/lib/dommy/internal/css/ancestor_filter.rb +107 -0
  71. data/lib/dommy/internal/css/cascade.rb +7 -7
  72. data/lib/dommy/internal/css/cascaded_declarations.rb +14 -20
  73. data/lib/dommy/internal/css/computed_style_builder.rb +30 -3
  74. data/lib/dommy/internal/css/custom_properties.rb +63 -80
  75. data/lib/dommy/internal/css/parser.rb +53 -63
  76. data/lib/dommy/internal/css/renderability.rb +6 -0
  77. data/lib/dommy/internal/css/rule_index.rb +170 -108
  78. data/lib/dommy/internal/css/rule_index_inputs.rb +135 -0
  79. data/lib/dommy/internal/css/selector_dependencies.rb +8 -2
  80. data/lib/dommy/internal/css/style_cache.rb +9 -6
  81. data/lib/dommy/internal/css/ua_stylesheet.rb +37 -2
  82. data/lib/dommy/internal/css_source.rb +223 -0
  83. data/lib/dommy/internal/css_syntax.rb +153 -0
  84. data/lib/dommy/internal/declarative_shadow_roots.rb +182 -0
  85. data/lib/dommy/internal/directionality.rb +2 -2
  86. data/lib/dommy/internal/document_generations.rb +91 -18
  87. data/lib/dommy/internal/document_interaction_state.rb +293 -9
  88. data/lib/dommy/internal/dom_matching.rb +7 -7
  89. data/lib/dommy/internal/element_aria.rb +65 -23
  90. data/lib/dommy/internal/element_css_inline_style.rb +26 -0
  91. data/lib/dommy/internal/element_editing.rb +41 -0
  92. data/lib/dommy/internal/element_nonce.rb +28 -0
  93. data/lib/dommy/internal/element_popover.rb +310 -0
  94. data/lib/dommy/internal/element_shadow.rb +107 -33
  95. data/lib/dommy/internal/element_state.rb +131 -26
  96. data/lib/dommy/internal/element_top_layer.rb +21 -51
  97. data/lib/dommy/internal/enumerated_keyword_sets.rb +40 -0
  98. data/lib/dommy/internal/event_handler_tables.rb +241 -0
  99. data/lib/dommy/internal/event_handlers.rb +149 -0
  100. data/lib/dommy/internal/exception_report.rb +1 -1
  101. data/lib/dommy/internal/focusability.rb +267 -0
  102. data/lib/dommy/internal/form_associated_custom_elements.rb +127 -0
  103. data/lib/dommy/internal/html_or_svg_or_mathml_element.rb +86 -0
  104. data/lib/dommy/internal/html_serialization.rb +292 -0
  105. data/lib/dommy/internal/idl_reflection.rb +202 -0
  106. data/lib/dommy/internal/idl_reflection_table.rb +589 -0
  107. data/lib/dommy/internal/infra.rb +36 -0
  108. data/lib/dommy/internal/input_type.rb +90 -20
  109. data/lib/dommy/internal/insertion_point.rb +27 -3
  110. data/lib/dommy/internal/js_number.rb +60 -0
  111. data/lib/dommy/internal/light_dismiss.rb +178 -0
  112. data/lib/dommy/internal/literal_lookup.rb +103 -0
  113. data/lib/dommy/internal/mime_type.rb +8 -0
  114. data/lib/dommy/internal/mutation_coordinator.rb +94 -60
  115. data/lib/dommy/internal/node_adopter.rb +165 -97
  116. data/lib/dommy/internal/node_factory.rb +111 -49
  117. data/lib/dommy/internal/node_identity.rb +1 -1
  118. data/lib/dommy/internal/node_traversal.rb +30 -13
  119. data/lib/dommy/internal/node_wrapper_cache.rb +34 -148
  120. data/lib/dommy/internal/observable_callback.rb +2 -5
  121. data/lib/dommy/internal/observer_options.rb +24 -14
  122. data/lib/dommy/internal/origin.rb +86 -0
  123. data/lib/dommy/internal/parent_node.rb +12 -10
  124. data/lib/dommy/internal/popover_invoker_element.rb +67 -0
  125. data/lib/dommy/internal/popover_stack.rb +120 -0
  126. data/lib/dommy/internal/post_insertion_steps.rb +146 -77
  127. data/lib/dommy/internal/reflected_attributes.rb +173 -43
  128. data/lib/dommy/internal/rendered_text/collector.rb +12 -9
  129. data/lib/dommy/internal/rendered_text/fragment.rb +6 -2
  130. data/lib/dommy/internal/selector_arguments.rb +2 -16
  131. data/lib/dommy/internal/selector_index.rb +19 -10
  132. data/lib/dommy/internal/selector_match.rb +190 -0
  133. data/lib/dommy/internal/selector_matcher.rb +91 -256
  134. data/lib/dommy/internal/selector_parser.rb +176 -100
  135. data/lib/dommy/internal/sequential_focus_navigation.rb +210 -0
  136. data/lib/dommy/internal/shadow_root_registry.rb +14 -6
  137. data/lib/dommy/internal/sibling_positions.rb +59 -0
  138. data/lib/dommy/internal/svg_uri_reference.rb +33 -0
  139. data/lib/dommy/internal/template_content_registry.rb +11 -5
  140. data/lib/dommy/internal/text_selection.rb +185 -32
  141. data/lib/dommy/internal/toggle_events.rb +6 -4
  142. data/lib/dommy/internal/unsafe_html.rb +107 -0
  143. data/lib/dommy/internal/url_parser.rb +84 -8
  144. data/lib/dommy/internal/user_activation.rb +192 -0
  145. data/lib/dommy/internal/web_idl.rb +160 -16
  146. data/lib/dommy/internal/window_constructors.rb +34 -14
  147. data/lib/dommy/internal/xml_serialization.rb +9 -45
  148. data/lib/dommy/js/constructor_resolver.rb +18 -0
  149. data/lib/dommy/js/custom_element_bridge.rb +57 -82
  150. data/lib/dommy/js/dom_interfaces.rb +51 -15
  151. data/lib/dommy/js/host_bridge.rb +189 -40
  152. data/lib/dommy/js/host_callbacks.rb +10 -0
  153. data/lib/dommy/js/host_runtime.js +1921 -416
  154. data/lib/dommy/js/marshaller.rb +13 -4
  155. data/lib/dommy/js/module_loader.rb +46 -5
  156. data/lib/dommy/js/module_preload.rb +194 -0
  157. data/lib/dommy/js/runtime.rb +9 -0
  158. data/lib/dommy/js/script_boot.rb +200 -114
  159. data/lib/dommy/js/serialized_record.rb +32 -0
  160. data/lib/dommy/js/webidl_event_handlers.js +45 -0
  161. data/lib/dommy/js/webidl_members.js +196 -0
  162. data/lib/dommy/js/webidl_signatures.js +2055 -0
  163. data/lib/dommy/js/webidl_tables.js +72 -101
  164. data/lib/dommy/js/window_wiring.rb +9 -5
  165. data/lib/dommy/location.rb +129 -76
  166. data/lib/dommy/mathml_elements.rb +13 -0
  167. data/lib/dommy/media_query_list.rb +4 -13
  168. data/lib/dommy/message_channel.rb +137 -64
  169. data/lib/dommy/mutation_observer.rb +3 -7
  170. data/lib/dommy/navigation.rb +61 -11
  171. data/lib/dommy/navigator.rb +132 -10
  172. data/lib/dommy/node.rb +81 -44
  173. data/lib/dommy/notification.rb +10 -6
  174. data/lib/dommy/parser.rb +5 -16
  175. data/lib/dommy/promise.rb +20 -26
  176. data/lib/dommy/range.rb +20 -47
  177. data/lib/dommy/resources.rb +38 -3
  178. data/lib/dommy/scheduler.rb +176 -48
  179. data/lib/dommy/shadow_root.rb +125 -25
  180. data/lib/dommy/storage.rb +135 -25
  181. data/lib/dommy/streams.rb +2 -6
  182. data/lib/dommy/structured_data.rb +45 -0
  183. data/lib/dommy/style_declaration.rb +38 -6
  184. data/lib/dommy/svg_elements.rb +25 -11
  185. data/lib/dommy/text_codec.rb +2 -14
  186. data/lib/dommy/tree_walker.rb +12 -26
  187. data/lib/dommy/url.rb +12 -9
  188. data/lib/dommy/url_pattern.rb +1 -9
  189. data/lib/dommy/validity_state.rb +109 -107
  190. data/lib/dommy/version.rb +1 -1
  191. data/lib/dommy/web_socket.rb +10 -33
  192. data/lib/dommy/window.rb +817 -105
  193. data/lib/dommy/worker.rb +4 -28
  194. data/lib/dommy/xml_http_request.rb +32 -45
  195. data/lib/dommy.rb +43 -10
  196. metadata +52 -5
  197. data/lib/dommy/backend/makiri_adapter.rb +0 -408
  198. data/lib/dommy/internal/cookie_jar.rb +0 -29
@@ -7,14 +7,20 @@ module Dommy
7
7
  module CallableInvoker
8
8
  module_function
9
9
 
10
- # Invoke `callback` with `args`. A JS-bridged callable goes through
11
- # `__js_call__("call", args)`; a Ruby callable through `call(*args)`. A nil
12
- # or non-callable callback is a no-op (returns nil).
13
- def invoke(callback, *args)
14
- return if callback.nil?
10
+ # A callback the page passed from JS — a function, or a NodeFilter
11
+ # object. Told by type, not by answering __js_call__: every host object
12
+ # answers that, for its own methods.
13
+ def js_callable?(callback) = callback.is_a?(Js::Callable)
15
14
 
16
- if callback.respond_to?(:__js_call__)
17
- callback.__js_call__("call", args)
15
+ # Something #invoke can call: a JS callable, or a Ruby one.
16
+ def callable?(callback) = js_callable?(callback) || callback.respond_to?(:call)
17
+
18
+ # Invoke `callback` with `args`, a JS throw swallowed by the bridge. `this:`
19
+ # is the receiver a JS function sees (a Ruby callable has none). A nil or
20
+ # non-callable callback is a no-op (returns nil).
21
+ def invoke(callback, *args, this: nil)
22
+ if js_callable?(callback)
23
+ callback.__js_invoke__(args, this: this)
18
24
  elsif callback.respond_to?(:call)
19
25
  callback.call(*args)
20
26
  end
@@ -23,41 +29,31 @@ module Dommy
23
29
  # Invoke `callback` so a JS throw SURFACES as a Ruby exception instead of
24
30
  # being swallowed by the bridge. WHATWG requires a task's exception to be
25
31
  # reported at the global ("report an exception") rather than silently
26
- # dropped, so the entry points that own a task — a timer / rAF callback —
27
- # invoke this form and report whatever comes out. The plain `invoke` keeps
28
- # the swallowing behavior for callers that handle their own errors.
29
- def invoke_raising(callback, *args)
30
- return if callback.nil?
32
+ # dropped, so the entry points that own a task — a timer / rAF callback, a
33
+ # promise reaction, a NodeFilter — invoke this form and handle whatever
34
+ # comes out. A Ruby callable raises naturally.
35
+ def invoke_raising(callback, *args, this: nil)
36
+ return callback.__js_invoke__(args, this: this, raising: true) if js_callable?(callback)
31
37
 
32
- if callback.respond_to?(:__js_invoke__)
33
- callback.__js_invoke__(args, raising: true)
34
- else
35
- invoke(callback, *args)
36
- end
38
+ invoke(callback, *args)
37
39
  end
38
40
 
39
41
  # Invoke a DOM event listener per the EventTarget rule: an object with
40
- # `handle_event`, else a Ruby callable, else a JS-bridged callable (tried in
41
- # that order). A JS function listener's `this` must be the event's
42
- # currentTarget (the node the listener is attached to), so pass it through
43
- # when the bridge supports an explicit receiver.
44
- # `args:` overrides the single-event argument list — the special error
45
- # event handler (`window.onerror`) is called with (message, filename,
46
- # lineno, colno, error) instead of the event. An EventListener object's
47
- # handleEvent always receives the event.
42
+ # `handle_event`, else a Ruby callable, else a JS function (tried in that
43
+ # order). A JS function listener's `this` is the event's currentTarget,
44
+ # and its throw surfaces so the dispatch reports it as a window `error`
45
+ # event. `args:` overrides the single-event argument list — the special
46
+ # error event handler (`window.onerror`) is called with (message,
47
+ # filename, lineno, colno, error) instead of the event. An EventListener
48
+ # object's handleEvent always receives the event.
48
49
  def invoke_listener(listener, event, current_target = nil, args: nil)
49
50
  args ||= [event]
50
51
  if listener.respond_to?(:handle_event)
51
52
  listener.handle_event(event)
52
53
  elsif listener.respond_to?(:call) && !listener.is_a?(Module)
53
54
  listener.call(*args)
54
- elsif listener.respond_to?(:__js_invoke__)
55
- # A JS function listener: `this` is the currentTarget, and a thrown value
56
- # surfaces (as a ThrowValue) so the dispatch reports it as a window
57
- # `error` event instead of swallowing it.
55
+ elsif js_callable?(listener)
58
56
  listener.__js_invoke__(args, this: current_target, raising: true)
59
- elsif listener.respond_to?(:__js_call__)
60
- listener.__js_call__("call", args)
61
57
  end
62
58
  end
63
59
  end
@@ -89,9 +89,10 @@ module Dommy
89
89
  @__node__.content
90
90
  end
91
91
 
92
+ # Assigning `data`, `nodeValue` or `textContent` is "replace data" over
93
+ # the whole node, so a live range boundary inside it clamps to the start
94
+ # rather than dangling past the end.
92
95
  def data=(value)
93
- # Assigning `data` is "replace data" over the whole node, so a live range
94
- # boundary inside it clamps to the start rather than dangling past the end.
95
96
  old_length = utf16_length(@__node__.content)
96
97
  write_data(value)
97
98
  @document.__internal_ranges_replaced_data__(self, 0, old_length, utf16_length(@__node__.content))
@@ -102,7 +103,7 @@ module Dommy
102
103
  end
103
104
 
104
105
  def node_value=(value)
105
- write_data(value)
106
+ self.data = value
106
107
  end
107
108
 
108
109
  def text_content
@@ -110,9 +111,16 @@ module Dommy
110
111
  end
111
112
 
112
113
  def text_content=(value)
113
- write_data(value)
114
+ self.data = value
115
+ end
116
+
117
+ # Node.isConnected: whether the shadow-including root is a Document.
118
+ def is_connected?
119
+ Internal::NodeTraversal.connected?(@__node__, @document)
114
120
  end
115
121
 
122
+ alias connected? is_connected?
123
+
116
124
  def remove
117
125
  @document.remove_node_with_notify(@__node__)
118
126
  nil
@@ -178,29 +186,13 @@ module Dommy
178
186
  def substring_data(offset, count)
179
187
  s = @__node__.content
180
188
  len = utf16_length(s)
181
- o = to_uint32(offset)
189
+ o = Internal::WebIDL.unsigned_long(offset)
182
190
  raise DOMException::IndexSizeError, "offset out of bounds" if o > len
183
191
 
184
- c = [to_uint32(count), len - o].min
192
+ c = [Internal::WebIDL.unsigned_long(count), len - o].min
185
193
  utf16_slice(s, o, c)
186
194
  end
187
195
 
188
- # ECMAScript ToUint32 — WebIDL `unsigned long` conversion for a data offset
189
- # or count: ToNumber (a non-numeric string is NaN), truncate toward zero,
190
- # then take modulo 2**32 (so -1 wraps to 4294967295, 0x100000000+2 to 2).
191
- def to_uint32(value)
192
- num =
193
- case value
194
- when Integer then value
195
- when Numeric then value
196
- when nil then 0 # JS null -> 0
197
- else Float(value.to_s) rescue Float::NAN
198
- end
199
- return 0 unless num.respond_to?(:finite?) ? num.finite? : true
200
-
201
- num.to_i % (2**32)
202
- end
203
-
204
196
  def append_data(value)
205
197
  write_data(@__node__.content + dom_string(value))
206
198
  end
@@ -222,10 +214,10 @@ module Dommy
222
214
  def replace_data(offset, count, value)
223
215
  s = @__node__.content
224
216
  len = utf16_length(s)
225
- o = to_uint32(offset)
217
+ o = Internal::WebIDL.unsigned_long(offset)
226
218
  raise DOMException::IndexSizeError, "offset out of bounds" if o > len
227
219
 
228
- c = [to_uint32(count), len - o].min
220
+ c = [Internal::WebIDL.unsigned_long(count), len - o].min
229
221
  inserted = dom_string(value)
230
222
  write_data(utf16_slice(s, 0, o) + inserted + utf16_slice(s, o + c, len - (o + c)))
231
223
  # Live ranges whose boundary sits in (or past) the replaced run follow it.
@@ -256,6 +248,8 @@ module Dommy
256
248
  next_sibling
257
249
  when "previousSibling"
258
250
  previous_sibling
251
+ when "isConnected"
252
+ is_connected?
259
253
  when "childNodes"
260
254
  # CharacterData is a leaf node: childNodes is always an empty (but
261
255
  # present and iterable) NodeList, and firstChild/lastChild are null.
@@ -276,7 +270,7 @@ module Dommy
276
270
  # element's shadow tree; a closed shadow tree hides the assignment (null).
277
271
  def assigned_slot
278
272
  parent = @__node__.parent
279
- return nil unless parent.respond_to?(:element?) && parent.element?
273
+ return nil unless parent&.element?
280
274
 
281
275
  host = @document.wrap_node(parent)
282
276
  return nil unless host.respond_to?(:shadow_root)
@@ -293,7 +287,7 @@ module Dommy
293
287
  def __js_set__(key, value)
294
288
  case key
295
289
  when "textContent", "data", "nodeValue"
296
- write_data(value)
290
+ self.data = value
297
291
  end
298
292
 
299
293
  nil
@@ -312,8 +306,7 @@ module Dommy
312
306
  false
313
307
  when "contains"
314
308
  # A leaf node contains only itself (no descendants).
315
- args[0].respond_to?(:__dommy_backend_node__) &&
316
- args[0].__dommy_backend_node__ == @__node__
309
+ args[0].is_a?(Node) && args[0].__dommy_backend_node__ == @__node__
317
310
  when "appendChild"
318
311
  append_child(args[0])
319
312
  when "insertBefore"
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "internal/infra"
4
+
3
5
  module Dommy
4
6
  # DOMTokenList (`classList`, `relList`) and DOMStringMap (`dataset`) —
5
7
  # the two live views over one attribute's text.
@@ -10,9 +12,13 @@ module Dommy
10
12
 
11
13
  # `attribute` is the content attribute this token list reflects ("class" for
12
14
  # `classList`, "rel" for `relList`, "sandbox", "sizes", "for", …).
13
- def initialize(element, attribute = "class")
15
+ # `supported_tokens` is what DOM calls the element and attribute name's
16
+ # supported tokens (lowercase), or nil when they define none — see
17
+ # Internal::SupportedTokens.
18
+ def initialize(element, attribute = "class", supported_tokens = nil)
14
19
  @element = element
15
20
  @attribute = attribute
21
+ @supported_tokens = supported_tokens
16
22
  end
17
23
 
18
24
  def length
@@ -29,11 +35,11 @@ module Dommy
29
35
  end
30
36
 
31
37
  def value
32
- @element.__dommy_backend_node__[@attribute].to_s
38
+ associated_value.to_s
33
39
  end
34
40
 
35
41
  def value=(new_value)
36
- @element.set_attribute(@attribute, new_value.to_s)
42
+ write(new_value.to_s)
37
43
  end
38
44
 
39
45
  # Spec: contains() does NOT validate (no SyntaxError on empty).
@@ -49,6 +55,16 @@ module Dommy
49
55
  class_tokens.include?(token.to_s)
50
56
  end
51
57
 
58
+ # DOMTokenList's supports(token), which runs the validation steps: a
59
+ # TypeError when the element and attribute define no supported tokens
60
+ # (`classList.supports("x")`), else whether the token, ASCII-lowercased,
61
+ # is one of them (`a.relList.supports("NoOpener")` is true).
62
+ def supports?(token)
63
+ raise Bridge::TypeError, "DOMTokenList has no supported tokens" if @supported_tokens.nil?
64
+
65
+ @supported_tokens.include?(stringify_token(token).downcase(:ascii))
66
+ end
67
+
52
68
  def add(*tokens)
53
69
  update_tokens { |existing| existing | normalize_tokens(tokens) }
54
70
  nil
@@ -79,7 +95,7 @@ module Dommy
79
95
  # raw attribute string, which would then hand stale tokens to later reads).
80
96
  updated = tokens.dup
81
97
  updated[idx] = new_s
82
- @element.set_attribute(@attribute, updated.uniq.join(" "))
98
+ write(updated.uniq.join(" "))
83
99
  true
84
100
  end
85
101
 
@@ -130,10 +146,7 @@ module Dommy
130
146
  end
131
147
 
132
148
  include Bridge::Methods
133
- # NOTE: `supports` is intentionally absent — for the class attribute's token
134
- # list it must throw a TypeError, which `list.supports(...)` (not a function)
135
- # already does.
136
- js_methods %w[add remove contains toggle replace item toString]
149
+ js_methods %w[add remove contains toggle replace item supports toString]
137
150
  def __js_call__(method, args)
138
151
  case method
139
152
  when "add"
@@ -146,11 +159,13 @@ module Dommy
146
159
  # contains() does not validate; null coerces to the string "null".
147
160
  class_tokens.include?(stringify_token(args[0]))
148
161
  when "toggle"
149
- toggle(args[0], args[1])
162
+ toggle(args[0], args.fetch(1, Bridge::UNDEFINED))
150
163
  when "replace"
151
164
  replace(args[0], args[1])
152
165
  when "item"
153
166
  item(args[0])
167
+ when "supports"
168
+ supports?(args[0])
154
169
  when "toString"
155
170
  value
156
171
  else
@@ -160,16 +175,18 @@ module Dommy
160
175
 
161
176
  private
162
177
 
163
- def toggle(token, force)
178
+ # `optional boolean force`: missing (or undefined) toggles; anything
179
+ # else, null included, is a boolean read with ToBoolean.
180
+ def toggle(token, force = Bridge::UNDEFINED)
164
181
  name = validate_token(token)
165
182
  present = class_tokens.include?(name)
166
- force_given = !(force.nil? || force.equal?(Bridge::UNDEFINED))
183
+ force_given = !force.equal?(Bridge::UNDEFINED)
167
184
 
168
185
  # Spec: toggle runs the update steps only when it actually adds or removes.
169
186
  # With an explicit force that already matches the current state it's a
170
187
  # no-op — the attribute is left byte-for-byte untouched (no re-serialize).
171
188
  if force_given
172
- want = !!force
189
+ want = Internal::WebIDL.boolean(force)
173
190
  return want if want == present
174
191
 
175
192
  update_tokens { |tokens| want ? tokens | [name] : tokens - [name] }
@@ -207,7 +224,7 @@ module Dommy
207
224
  # item, iteration, and contains all operate on this set; `value`/`toString`
208
225
  # return the raw attribute. ASCII whitespace per the spec is space/tab/LF/FF/CR.
209
226
  def class_tokens
210
- raw = @element.__dommy_backend_node__[@attribute].to_s
227
+ raw = associated_value.to_s
211
228
  # Cache the parsed token list keyed by the raw attribute string: a class
212
229
  # selector match re-reads this for every element on every querySelector,
213
230
  # and the split/reject/uniq dominated heavy-SPA load profiles. The key is
@@ -216,7 +233,7 @@ module Dommy
216
233
  cached = @token_cache
217
234
  return cached[1] if cached && cached[0] == raw
218
235
 
219
- tokens = raw.split(/[ \t\n\f\r]+/).reject(&:empty?).uniq
236
+ tokens = Internal::Infra.split_on_ascii_whitespace(raw).uniq
220
237
  @token_cache = [raw, tokens]
221
238
  tokens
222
239
  end
@@ -227,9 +244,22 @@ module Dommy
227
244
  # (per spec) is an empty set with no existing attribute — don't create one.
228
245
  def update_tokens
229
246
  tokens = yield(class_tokens)
230
- return if tokens.empty? && !@element.__dommy_backend_node__.key?(@attribute)
247
+ return if tokens.empty? && associated_value.nil?
248
+
249
+ write(tokens.join(" "))
250
+ end
251
+
252
+ # The associated attribute is the one of that local name in no namespace
253
+ # (DOM's "get an attribute by namespace and local name"): a `class` set
254
+ # with setAttributeNS("urn:x", "class") is not the element's classes.
255
+ def associated_value
256
+ Backend.no_namespace_attribute_value(@element.__dommy_backend_node__, @attribute)
257
+ end
231
258
 
232
- @element.set_attribute(@attribute, tokens.join(" "))
259
+ # The same attribute, written as every other reflection writes one: its
260
+ # name is a fixed, valid local name, so there is nothing to validate.
261
+ def write(value)
262
+ @element.__internal_set_attribute_value__(@attribute, value)
233
263
  end
234
264
  end
235
265
 
@@ -247,7 +277,7 @@ module Dommy
247
277
 
248
278
  # A missing data-* attribute reads as JS `undefined` (and `"foo" in dataset`
249
279
  # is false), per DOMStringMap semantics.
250
- value = @element.__dommy_backend_node__[attr_name(name)]
280
+ value = @element.__internal_attribute_value__(attr_name(name))
251
281
  value.nil? ? Bridge::ABSENT : value
252
282
  end
253
283
 
@@ -260,7 +290,7 @@ module Dommy
260
290
  raise DOMException::InvalidCharacterError, "#{attribute.inspect} is not a valid attribute name"
261
291
  end
262
292
 
263
- @element.set_attribute(attribute, value.to_s)
293
+ @element.__internal_set_attribute_value__(attribute, value.to_s)
264
294
  nil
265
295
  end
266
296
 
@@ -271,7 +301,7 @@ module Dommy
271
301
  name = key.to_s
272
302
  return true unless supported_name?(name)
273
303
 
274
- @element.remove_attribute(attr_name(name))
304
+ @element.remove_attribute_ns(nil, attr_name(name))
275
305
  true
276
306
  end
277
307
 
@@ -280,12 +310,14 @@ module Dommy
280
310
  end
281
311
 
282
312
  # WebIDL "supported property names" for DOMStringMap: each `data-*`
283
- # attribute's name with the `data-` prefix stripped and `-x` sequences
284
- # camel-cased (`data-date-of-birth` → `dateOfBirth`, `data-` → ``).
313
+ # attribute in no namespace, its name with the `data-` prefix stripped
314
+ # and `-x` sequences camel-cased (`data-date-of-birth` → `dateOfBirth`,
315
+ # `data-` → ``).
285
316
  def __js_named_props__
286
317
  Backend.attribute_nodes(@element.__dommy_backend_node__).filter_map do |a|
287
- name = Backend.attribute_ns_info(a)[:qualified_name]
288
- next unless name.start_with?("data-")
318
+ info = Backend.attribute_ns_info(a)
319
+ name = info[:local_name]
320
+ next unless info[:namespace_uri].nil? && name.start_with?("data-")
289
321
 
290
322
  name.sub(/\Adata-/, "").gsub(/-([a-z])/) { ::Regexp.last_match(1).upcase }
291
323
  end