dommy 0.13.0 → 0.14.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 (155) hide show
  1. checksums.yaml +4 -4
  2. data/lib/dommy/backend/makiri_adapter.rb +63 -29
  3. data/lib/dommy/backend.rb +32 -9
  4. data/lib/dommy/bridge/constructor.rb +72 -0
  5. data/lib/dommy/bridge/errors.rb +42 -0
  6. data/lib/dommy/bridge/methods.rb +21 -14
  7. data/lib/dommy/bridge/sentinels.rb +39 -0
  8. data/lib/dommy/bridge/values.rb +78 -0
  9. data/lib/dommy/{js → bridge}/wire_tags.rb +14 -11
  10. data/lib/dommy/bridge.rb +22 -250
  11. data/lib/dommy/browser.rb +112 -11
  12. data/lib/dommy/character_data.rb +539 -0
  13. data/lib/dommy/class_list.rb +311 -0
  14. data/lib/dommy/crypto.rb +12 -1
  15. data/lib/dommy/css.rb +12 -0
  16. data/lib/dommy/data_transfer.rb +106 -0
  17. data/lib/dommy/document.rb +307 -369
  18. data/lib/dommy/dom_parser.rb +3 -0
  19. data/lib/dommy/element.rb +199 -1817
  20. data/lib/dommy/encodings/tables.rb +4 -4
  21. data/lib/dommy/encodings.rb +58 -27
  22. data/lib/dommy/event.rb +227 -1
  23. data/lib/dommy/event_source.rb +41 -0
  24. data/lib/dommy/form_data.rb +6 -64
  25. data/lib/dommy/form_entry_list.rb +227 -0
  26. data/lib/dommy/fragment.rb +320 -0
  27. data/lib/dommy/history.rb +3 -0
  28. data/lib/dommy/html_collection.rb +18 -8
  29. data/lib/dommy/html_elements/base.rb +338 -0
  30. data/lib/dommy/html_elements/embedded.rb +237 -0
  31. data/lib/dommy/html_elements/forms/form.rb +1088 -0
  32. data/lib/dommy/html_elements/forms/input.rb +642 -0
  33. data/lib/dommy/html_elements/forms/select.rb +536 -0
  34. data/lib/dommy/html_elements/forms.rb +8 -0
  35. data/lib/dommy/html_elements/interactive.rb +382 -0
  36. data/lib/dommy/html_elements/media.rb +329 -0
  37. data/lib/dommy/html_elements/metadata.rb +353 -0
  38. data/lib/dommy/html_elements/miscellaneous.rb +77 -0
  39. data/lib/dommy/html_elements/tables.rb +468 -0
  40. data/lib/dommy/html_elements.rb +11 -5322
  41. data/lib/dommy/hyperlink_utils.rb +171 -0
  42. data/lib/dommy/interaction/debug.rb +7 -6
  43. data/lib/dommy/interaction/dom_summary.rb +65 -40
  44. data/lib/dommy/interaction/driver.rb +29 -115
  45. data/lib/dommy/interaction/event_synthesis.rb +97 -4
  46. data/lib/dommy/interaction/field_interactor.rb +1 -13
  47. data/lib/dommy/interaction/form_submission.rb +89 -167
  48. data/lib/dommy/interaction/key_sender.rb +121 -0
  49. data/lib/dommy/interaction/mime_types.rb +25 -0
  50. data/lib/dommy/interaction.rb +2 -0
  51. data/lib/dommy/internal/accessibility_tree.rb +32 -9
  52. data/lib/dommy/internal/accessibility_visibility.rb +44 -0
  53. data/lib/dommy/internal/accessible_name.rb +85 -65
  54. data/lib/dommy/internal/aria_role.rb +21 -6
  55. data/lib/dommy/internal/aria_snapshot.rb +5 -0
  56. data/lib/dommy/internal/aria_state.rb +24 -5
  57. data/lib/dommy/internal/autofill.rb +141 -0
  58. data/lib/dommy/internal/backend_prefilter.rb +170 -0
  59. data/lib/dommy/internal/bounded_cache.rb +50 -0
  60. data/lib/dommy/internal/child_node.rb +9 -34
  61. data/lib/dommy/internal/css/calc.rb +143 -145
  62. data/lib/dommy/internal/css/cascade.rb +38 -393
  63. data/lib/dommy/internal/css/cascaded_declarations.rb +245 -0
  64. data/lib/dommy/internal/css/color.rb +61 -34
  65. data/lib/dommy/internal/css/computed_style_builder.rb +205 -0
  66. data/lib/dommy/internal/css/computed_style_declaration.rb +20 -4
  67. data/lib/dommy/internal/css/counters.rb +60 -37
  68. data/lib/dommy/internal/css/custom_properties.rb +87 -39
  69. data/lib/dommy/internal/css/media_query.rb +228 -203
  70. data/lib/dommy/internal/css/parser.rb +29 -12
  71. data/lib/dommy/internal/css/property_registry.rb +32 -56
  72. data/lib/dommy/internal/css/renderability.rb +61 -0
  73. data/lib/dommy/internal/css/rule_index.rb +17 -140
  74. data/lib/dommy/internal/css/selector_dependencies.rb +162 -0
  75. data/lib/dommy/internal/css/style_cache.rb +61 -0
  76. data/lib/dommy/internal/css/ua_stylesheet.rb +40 -1
  77. data/lib/dommy/internal/css/value_tokens.rb +60 -0
  78. data/lib/dommy/internal/directionality.rb +188 -0
  79. data/lib/dommy/internal/document_generations.rb +169 -0
  80. data/lib/dommy/internal/document_interaction_state.rb +47 -0
  81. data/lib/dommy/internal/document_live_ranges.rb +97 -0
  82. data/lib/dommy/internal/dom_matching.rb +28 -12
  83. data/lib/dommy/internal/element_aria.rb +163 -0
  84. data/lib/dommy/internal/element_geometry.rb +69 -0
  85. data/lib/dommy/internal/element_matching.rb +5 -12
  86. data/lib/dommy/internal/element_shadow.rb +117 -0
  87. data/lib/dommy/internal/element_state.rb +232 -0
  88. data/lib/dommy/internal/element_tasks.rb +19 -0
  89. data/lib/dommy/internal/element_top_layer.rb +63 -0
  90. data/lib/dommy/internal/enumerated_keyword_sets.rb +53 -0
  91. data/lib/dommy/internal/exception_report.rb +1 -1
  92. data/lib/dommy/internal/input_type.rb +280 -0
  93. data/lib/dommy/internal/insertion_point.rb +62 -0
  94. data/lib/dommy/internal/mime_type.rb +74 -0
  95. data/lib/dommy/internal/mutation_coordinator.rb +57 -198
  96. data/lib/dommy/internal/namespaces.rb +41 -43
  97. data/lib/dommy/internal/node_adopter.rb +84 -77
  98. data/lib/dommy/internal/node_factory.rb +157 -0
  99. data/lib/dommy/internal/node_identity.rb +31 -0
  100. data/lib/dommy/internal/node_wrapper_cache.rb +61 -207
  101. data/lib/dommy/internal/observer_options.rb +93 -0
  102. data/lib/dommy/internal/parent_node.rb +27 -27
  103. data/lib/dommy/internal/post_insertion_steps.rb +213 -0
  104. data/lib/dommy/internal/reflected_attributes.rb +425 -13
  105. data/lib/dommy/internal/rendered_text/collector.rb +221 -0
  106. data/lib/dommy/internal/rendered_text/computed_box.rb +39 -0
  107. data/lib/dommy/internal/rendered_text/fragment.rb +55 -0
  108. data/lib/dommy/internal/rendered_text/items.rb +57 -0
  109. data/lib/dommy/internal/rendered_text/whitespace_collapser.rb +132 -0
  110. data/lib/dommy/internal/selector_arguments.rb +94 -0
  111. data/lib/dommy/internal/selector_index.rb +3 -1
  112. data/lib/dommy/internal/selector_matcher.rb +23 -383
  113. data/lib/dommy/internal/selector_parser.rb +2 -37
  114. data/lib/dommy/internal/template_content_registry.rb +112 -66
  115. data/lib/dommy/internal/text_flattening.rb +20 -0
  116. data/lib/dommy/internal/text_selection.rb +104 -0
  117. data/lib/dommy/internal/toggle_events.rb +56 -0
  118. data/lib/dommy/internal/toggle_task_tracker.rb +57 -0
  119. data/lib/dommy/internal/url_parser.rb +722 -569
  120. data/lib/dommy/internal/url_record_accessors.rb +90 -0
  121. data/lib/dommy/internal/window_constructors.rb +190 -0
  122. data/lib/dommy/internal/xml_serialization.rb +168 -80
  123. data/lib/dommy/js/crossing_profile.rb +75 -0
  124. data/lib/dommy/js/dom_interfaces.rb +22 -2
  125. data/lib/dommy/js/host_bridge.rb +43 -187
  126. data/lib/dommy/js/host_callbacks.rb +96 -0
  127. data/lib/dommy/js/host_runtime.js +840 -763
  128. data/lib/dommy/js/import_map.rb +10 -1
  129. data/lib/dommy/js/marshaller.rb +94 -65
  130. data/lib/dommy/js/observable_runtime.js +125 -146
  131. data/lib/dommy/js/runtime.rb +10 -43
  132. data/lib/dommy/js/runtime_registry.rb +46 -0
  133. data/lib/dommy/js/script_boot.rb +38 -73
  134. data/lib/dommy/js/webidl_tables.js +619 -0
  135. data/lib/dommy/js/window_wiring.rb +69 -0
  136. data/lib/dommy/location.rb +152 -103
  137. data/lib/dommy/mutation_observer.rb +3 -57
  138. data/lib/dommy/navigation.rb +65 -12
  139. data/lib/dommy/node.rb +46 -20
  140. data/lib/dommy/promise.rb +58 -4
  141. data/lib/dommy/resources.rb +19 -1
  142. data/lib/dommy/shadow_root.rb +11 -5
  143. data/lib/dommy/style_declaration.rb +284 -0
  144. data/lib/dommy/svg_elements.rb +1 -45
  145. data/lib/dommy/url.rb +20 -69
  146. data/lib/dommy/url_pattern/regexp_translator.rb +1 -6
  147. data/lib/dommy/url_pattern/tokenizer.rb +4 -2
  148. data/lib/dommy/validity_state.rb +383 -0
  149. data/lib/dommy/version.rb +1 -1
  150. data/lib/dommy/window.rb +30 -166
  151. data/lib/dommy/xml_http_request.rb +35 -4
  152. data/lib/dommy.rb +23 -3
  153. metadata +74 -7
  154. data/lib/dommy/bridge/constructor_registry.rb +0 -28
  155. data/lib/dommy/internal/css_pseudo_handlers.rb +0 -378
@@ -0,0 +1,539 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dommy
4
+ # Text, comments, CDATA sections and processing instructions —
5
+ # the nodes whose content is a string rather than a subtree.
6
+ #
7
+ # Lived in element.rb, which is for Element.
8
+ # CharacterData base — TextNode and CommentNode share the data /
9
+ # nodeValue / textContent API and `remove` / `cloneNode` semantics.
10
+ class CharacterDataNode
11
+ include Node
12
+ include EventTarget
13
+ # `before` / `after` / `replaceWith` (+ their argument coercion) — the same
14
+ # spec-correct implementation Element uses, minus appendChild/insertBefore.
15
+ include Internal::ChildNode
16
+ include Internal::LeafNode
17
+
18
+ # The owning Dommy document (as Element exposes), so cross-document adoption
19
+ # checks work for text/comment nodes too.
20
+ attr_reader :document
21
+
22
+ def __dommy_backend_node__ = @__node__
23
+
24
+ # EventTarget needs a parent for event propagation; a character-data node
25
+ # bubbles to its parent element.
26
+ def __internal_event_parent__
27
+ @__node__.parent && @document.wrap_node(@__node__.parent)
28
+ end
29
+
30
+ # Text.splitText / CharacterData split: break the node at `offset` (a UTF-16
31
+ # code unit index), keeping [0, offset) here and returning a new sibling node
32
+ # with the remainder.
33
+ def split_text(offset)
34
+ off = offset.to_i
35
+ full = @__node__.content
36
+ length = utf16_length(full)
37
+ raise DOMException::IndexSizeError, "offset #{off} is out of bounds" if off.negative? || off > length
38
+
39
+ count = length - off
40
+ new_node = @document.create_text_node(utf16_slice(full, off, count))
41
+ new_bn = new_node.__dommy_backend_node__
42
+ parent = @__node__.parent
43
+ if parent
44
+ # Step 7.1 — insert the new node right after self. Its live range steps
45
+ # run here, where the algorithm puts them; its childList RECORD is
46
+ # queued at the very end instead (see below).
47
+ # WHATWG "split a Text node" step 7.1 inserts the new node right after
48
+ # self, so insert step 5 runs first, against self's next sibling.
49
+ @document.__internal_ranges_will_insert__(parent, @__node__.next, 1)
50
+ @__node__.add_next_sibling(new_bn)
51
+ # Live ranges past the split point move to the tail node (the generic
52
+ # insert step above already shifted boundaries sitting further along).
53
+ # Step 7 only runs for a node that HAS a parent: splitting a detached
54
+ # node leaves every boundary on the node itself, to be clamped by the
55
+ # truncation below.
56
+ @document.__internal_ranges_split_text__(self, off, new_node)
57
+ end
58
+ # Step 8 — "replace data with node, offset, count, the empty string": the
59
+ # truncation is a data replacement, so it carries the replace-data live
60
+ # range rules (the ones that clamp a boundary in a detached node).
61
+ write_data(utf16_slice(full, 0, off))
62
+ @document.__internal_ranges_replaced_data__(self, off, count, 0)
63
+ # The one place Dommy queues MutationRecords out of algorithm order. Read
64
+ # literally, the insertion (step 7.1) precedes the data replacement (step
65
+ # 8), so the records would be childList then characterData. Every shipping
66
+ # engine emits them the other way round: Blink's Text::splitText calls
67
+ # DidModifyData before InsertBefore, WebCore matches it, and Gecko's
68
+ # Text::SplitText not only matches but says in a comment that nsRange
69
+ # DEPENDS on the data notification preceding the insertion. Confirmed by
70
+ # running the same script in all three — Chromium 141, WebKitGTK 2.52.6
71
+ # and Firefox all answer [characterData, childList], while all three
72
+ # answer [childList, characterData] for those same two mutations performed
73
+ # explicitly, so this is specific to splitText and not a general
74
+ # reordering. Only the record order moves: the tree and every live range
75
+ # boundary still follow the steps exactly as written.
76
+ # https://github.com/takahashim/dommy/issues/23
77
+ @document.notify_child_list_mutation(target_node: parent, added_nodes: [new_bn], removed_nodes: []) if parent
78
+ new_node
79
+ end
80
+
81
+ def initialize(document, nokogiri_node)
82
+ @document = document
83
+ @__node__ = nokogiri_node
84
+ end
85
+
86
+ # Snake_case facade (CRuby idiomatic)
87
+
88
+ def data
89
+ @__node__.content
90
+ end
91
+
92
+ 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
+ old_length = utf16_length(@__node__.content)
96
+ write_data(value)
97
+ @document.__internal_ranges_replaced_data__(self, 0, old_length, utf16_length(@__node__.content))
98
+ end
99
+
100
+ def node_value
101
+ @__node__.content
102
+ end
103
+
104
+ def node_value=(value)
105
+ write_data(value)
106
+ end
107
+
108
+ def text_content
109
+ @__node__.content
110
+ end
111
+
112
+ def text_content=(value)
113
+ write_data(value)
114
+ end
115
+
116
+ def remove
117
+ @document.remove_node_with_notify(@__node__)
118
+ nil
119
+ end
120
+
121
+ def parent_node
122
+ @__node__.parent && @document.wrap_node(@__node__.parent)
123
+ end
124
+
125
+ # parentElement is the parent only when it is an element (a document or
126
+ # fragment parent is a parentNode but not a parentElement).
127
+ def parent_element
128
+ @document.wrap_node(@__node__.parent) if @__node__.parent&.element?
129
+ end
130
+
131
+ def next_sibling
132
+ @__node__.next && @document.wrap_node(@__node__.next)
133
+ end
134
+
135
+ def previous_sibling
136
+ @__node__.previous && @document.wrap_node(@__node__.previous)
137
+ end
138
+
139
+ def [](key)
140
+ __js_get__(key.to_s)
141
+ end
142
+
143
+ def []=(key, value)
144
+ __js_set__(key.to_s, value)
145
+ end
146
+
147
+ # WHATWG nodeName for character-data nodes is a per-type constant
148
+ # ("#text" / "#comment" / "#cdata-section"), not the element name.
149
+ def node_name
150
+ case node_type
151
+ when 3 then "#text"
152
+ when 4 then "#cdata-section"
153
+ when 8 then "#comment"
154
+ end
155
+ end
156
+
157
+ # CharacterData length / mutation methods. Offsets and counts are UTF-16 code
158
+ # units per spec; for BMP text (the common case) Ruby char indices match.
159
+ # Each mutating op routes through write_data, which fires the characterData
160
+ # MutationObserver record.
161
+
162
+ def length
163
+ utf16_length(@__node__.content)
164
+ end
165
+
166
+ # CharacterData offsets and counts are measured in UTF-16 code units, not
167
+ # Unicode code points, so an astral character (e.g. an emoji) counts as 2.
168
+ # Range boundary offsets mean the same thing, so the conversion itself lives
169
+ # in Internal::Utf16 and both share it.
170
+ def utf16_length(str)
171
+ Internal::Utf16.length(str)
172
+ end
173
+
174
+ def utf16_slice(str, offset, count)
175
+ Internal::Utf16.slice(str, offset, count)
176
+ end
177
+
178
+ def substring_data(offset, count)
179
+ s = @__node__.content
180
+ len = utf16_length(s)
181
+ o = to_uint32(offset)
182
+ raise DOMException::IndexSizeError, "offset out of bounds" if o > len
183
+
184
+ c = [to_uint32(count), len - o].min
185
+ utf16_slice(s, o, c)
186
+ end
187
+
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
+ def append_data(value)
205
+ write_data(@__node__.content + dom_string(value))
206
+ end
207
+
208
+ # WebIDL DOMString coercion for a CharacterData mutation argument: JS null
209
+ # becomes the string "null" (undefined already stringifies to "undefined").
210
+ def dom_string(value)
211
+ value.nil? ? "null" : value.to_s
212
+ end
213
+
214
+ def insert_data(offset, value)
215
+ replace_data(offset, 0, value)
216
+ end
217
+
218
+ def delete_data(offset, count)
219
+ replace_data(offset, count, "")
220
+ end
221
+
222
+ def replace_data(offset, count, value)
223
+ s = @__node__.content
224
+ len = utf16_length(s)
225
+ o = to_uint32(offset)
226
+ raise DOMException::IndexSizeError, "offset out of bounds" if o > len
227
+
228
+ c = [to_uint32(count), len - o].min
229
+ inserted = dom_string(value)
230
+ write_data(utf16_slice(s, 0, o) + inserted + utf16_slice(s, o + c, len - (o + c)))
231
+ # Live ranges whose boundary sits in (or past) the replaced run follow it.
232
+ @document.__internal_ranges_replaced_data__(self, o, c, utf16_length(inserted))
233
+ end
234
+
235
+ def __js_get__(key)
236
+ case key
237
+ when "nodeType"
238
+ node_type
239
+ when "nodeName"
240
+ node_name
241
+ when "textContent"
242
+ @__node__.content
243
+ when "data"
244
+ @__node__.content
245
+ when "nodeValue"
246
+ @__node__.content
247
+ when "length"
248
+ length
249
+ when "parentNode"
250
+ parent_node
251
+ when "parentElement"
252
+ parent_element
253
+ when "ownerDocument"
254
+ @document
255
+ when "nextSibling"
256
+ next_sibling
257
+ when "previousSibling"
258
+ previous_sibling
259
+ when "childNodes"
260
+ # CharacterData is a leaf node: childNodes is always an empty (but
261
+ # present and iterable) NodeList, and firstChild/lastChild are null.
262
+ # DOM-walking code (e.g. idiomorph's morphChildren) iterates
263
+ # `node.childNodes` on every node, so a missing one crashes it.
264
+ NodeList.new
265
+ when "firstChild", "lastChild"
266
+ nil
267
+ when "assignedSlot"
268
+ assigned_slot
269
+ else
270
+ Bridge::ABSENT # unknown property: JS undefined, `in` absent
271
+ end
272
+ end
273
+
274
+ # Slottable mixin: the <slot> this text node is assigned to. A text node has
275
+ # no `slot` attribute, so it targets the default (unnamed) slot of its parent
276
+ # element's shadow tree; a closed shadow tree hides the assignment (null).
277
+ def assigned_slot
278
+ parent = @__node__.parent
279
+ return nil unless parent.respond_to?(:element?) && parent.element?
280
+
281
+ host = @document.wrap_node(parent)
282
+ return nil unless host.respond_to?(:shadow_root)
283
+
284
+ sr = host.shadow_root
285
+ return nil unless sr
286
+ return nil if sr.__js_get__("mode") == "closed"
287
+
288
+ sr.query_selector_all("slot").find do |slot|
289
+ (slot.respond_to?(:name) ? slot.name.to_s : "") == ""
290
+ end
291
+ end
292
+
293
+ def __js_set__(key, value)
294
+ case key
295
+ when "textContent", "data", "nodeValue"
296
+ write_data(value)
297
+ end
298
+
299
+ nil
300
+ end
301
+
302
+ include Bridge::Methods
303
+ js_methods %w[remove before after replaceWith isEqualNode hasChildNodes
304
+ appendData insertData deleteData replaceData substringData contains
305
+ isSameNode getRootNode normalize splitText compareDocumentPosition
306
+ lookupNamespaceURI lookupPrefix isDefaultNamespace
307
+ appendChild insertBefore removeChild replaceChild
308
+ addEventListener removeEventListener dispatchEvent]
309
+ def __js_call__(method, args)
310
+ case method
311
+ when "hasChildNodes"
312
+ false
313
+ when "contains"
314
+ # A leaf node contains only itself (no descendants).
315
+ args[0].respond_to?(:__dommy_backend_node__) &&
316
+ args[0].__dommy_backend_node__ == @__node__
317
+ when "appendChild"
318
+ append_child(args[0])
319
+ when "insertBefore"
320
+ insert_before(args[0], args[1])
321
+ when "replaceChild"
322
+ replace_child(args[0], args[1])
323
+ when "removeChild"
324
+ remove_child(args[0])
325
+ when "compareDocumentPosition"
326
+ compare_document_position(args[0])
327
+ when "isSameNode"
328
+ is_same_node(args[0])
329
+ when "getRootNode"
330
+ get_root_node(args[0])
331
+ when "lookupNamespaceURI"
332
+ lookup_namespace_uri(args[0])
333
+ when "lookupPrefix"
334
+ lookup_prefix(args[0])
335
+ when "isDefaultNamespace"
336
+ is_default_namespace(args[0])
337
+ when "normalize"
338
+ nil # a leaf has no child text runs to merge
339
+ when "splitText"
340
+ split_text(args[0])
341
+ when "addEventListener"
342
+ add_event_listener(args[0], args[1], args[2])
343
+ when "removeEventListener"
344
+ remove_event_listener(args[0], args[1], args[2])
345
+ when "dispatchEvent"
346
+ dispatch_event(args[0])
347
+ when "appendData"
348
+ raise Bridge::TypeError, "appendData requires 1 argument." if args.empty?
349
+
350
+ append_data(args[0])
351
+ when "insertData"
352
+ insert_data(args[0], args[1])
353
+ when "deleteData"
354
+ delete_data(args[0], args[1])
355
+ when "replaceData"
356
+ replace_data(args[0], args[1], args[2])
357
+ when "substringData"
358
+ raise Bridge::TypeError, "substringData requires 2 arguments." if args.length < 2
359
+
360
+ substring_data(args[0], args[1])
361
+ when "remove"
362
+ remove
363
+ Bridge::UNDEFINED # ChildNode#remove is void -> JS undefined, not null
364
+ when "before"
365
+ before(*args)
366
+ when "after"
367
+ after(*args)
368
+ when "replaceWith"
369
+ replace_with(*args)
370
+ when "isEqualNode"
371
+ is_equal_node(args[0])
372
+ end
373
+ end
374
+
375
+ # ChildNode mixin: WHATWG DOM defines `before`, `after`, `replaceWith` on
376
+ # all child nodes, including Text and Comment. The spec-correct algorithm
377
+ # (viable previous/next sibling, forward insertion, string coercion,
378
+ # Fragment/cross-document adoption) lives in Internal::ParentNode and is
379
+ # shared with Element — these just forward to it.
380
+
381
+ def before(*args)
382
+ child_node_before(args)
383
+ end
384
+
385
+ def after(*args)
386
+ child_node_after(args)
387
+ end
388
+
389
+ def replace_with(*args)
390
+ child_node_replace_with(args)
391
+ end
392
+
393
+ private
394
+
395
+ def write_data(value)
396
+ old = @__node__.content
397
+ @__node__.content = value.to_s
398
+ @document.notify_character_data_mutation(target_node: @__node__, old_value: old)
399
+ end
400
+ end
401
+
402
+ class TextNode < CharacterDataNode
403
+ def node_type
404
+ 3
405
+ end
406
+
407
+ # WHATWG Text.wholeText — the concatenated data of this node together with
408
+ # its contiguous, logically-adjacent Text / CDATASection siblings (node
409
+ # types 3 and 4), in document order.
410
+ def whole_text
411
+ run = [@__node__]
412
+ prev = @__node__.previous
413
+ while prev && [3, 4].include?(prev.node_type)
414
+ run.unshift(prev)
415
+ prev = prev.previous
416
+ end
417
+ nxt = @__node__.next
418
+ while nxt && [3, 4].include?(nxt.node_type)
419
+ run.push(nxt)
420
+ nxt = nxt.next
421
+ end
422
+ run.map { |bn| bn.content.to_s }.join
423
+ end
424
+
425
+ def __js_get__(key)
426
+ return whole_text if key == "wholeText"
427
+
428
+ super
429
+ end
430
+
431
+ # Node.cloneNode. CharacterData has no children, so `deep` decides nothing:
432
+ # the copy is a new node of the SAME interface carrying the same data, owned
433
+ # by this node's document. Each subclass builds its own kind — a CDATASection
434
+ # that cloned to a Text would be the wrong node.
435
+ def clone_node(_deep = false)
436
+ @document.create_text_node(@__node__.text)
437
+ end
438
+
439
+ # Own __js_call__ methods, on top of CharacterDataNode's.
440
+ js_methods %w[cloneNode]
441
+ def __js_call__(method, args)
442
+ case method
443
+ when "cloneNode"
444
+ clone_node(args.first)
445
+ else
446
+ super
447
+ end
448
+ end
449
+ end
450
+
451
+ # CDATASection — a Text subtype (nodeType 4). CharacterData methods and the
452
+ # "#cdata-section" nodeName come from CharacterDataNode via node_type.
453
+
454
+ # CDATASection — a Text subtype (nodeType 4). CharacterData methods and the
455
+ # "#cdata-section" nodeName come from CharacterDataNode via node_type.
456
+ class CDATASectionNode < TextNode
457
+ def node_type
458
+ 4
459
+ end
460
+
461
+ def clone_node(_deep = false)
462
+ @document.create_cdata_section(@__node__.content)
463
+ end
464
+ end
465
+
466
+ class CommentNode < CharacterDataNode
467
+ def node_type
468
+ 8
469
+ end
470
+
471
+ def clone_node(_deep = false)
472
+ @document.create_comment(@__node__.content)
473
+ end
474
+
475
+ # Own __js_call__ methods, on top of CharacterDataNode's.
476
+ js_methods %w[cloneNode]
477
+ def __js_call__(method, args)
478
+ case method
479
+ when "cloneNode"
480
+ clone_node(args.first)
481
+ else
482
+ super
483
+ end
484
+ end
485
+ end
486
+
487
+ # ProcessingInstruction (`<?target data?>`, nodeType 7) — CharacterData with a
488
+ # `target`. Backed by a real backend node (created via
489
+ # document.createProcessingInstruction), so it participates in the tree like
490
+ # Text/Comment: insertion, ChildNode methods, identity caching and
491
+ # serialization all come from the shared CharacterDataNode machinery.
492
+
493
+ # ProcessingInstruction (`<?target data?>`, nodeType 7) — CharacterData with a
494
+ # `target`. Backed by a real backend node (created via
495
+ # document.createProcessingInstruction), so it participates in the tree like
496
+ # Text/Comment: insertion, ChildNode methods, identity caching and
497
+ # serialization all come from the shared CharacterDataNode machinery.
498
+ class ProcessingInstructionNode < CharacterDataNode
499
+ def node_type
500
+ 7
501
+ end
502
+
503
+ # WHATWG: a ProcessingInstruction's nodeName is its target.
504
+ def node_name
505
+ @__node__.target
506
+ end
507
+
508
+ def target
509
+ @__node__.target
510
+ end
511
+
512
+ def __js_get__(key)
513
+ case key
514
+ when "target"
515
+ @__node__.target
516
+ else
517
+ super
518
+ end
519
+ end
520
+
521
+ def clone_node(_deep = false)
522
+ @document.create_processing_instruction(@__node__.target, @__node__.content)
523
+ end
524
+
525
+ # Own __js_call__ methods, on top of CharacterDataNode's.
526
+ js_methods %w[cloneNode]
527
+ def __js_call__(method, args)
528
+ case method
529
+ when "cloneNode"
530
+ clone_node(args.first)
531
+ else
532
+ super
533
+ end
534
+ end
535
+ end
536
+
537
+ # (`LiveChildren` removed — `el.children` now returns a
538
+ # `Dommy::HTMLCollection` initialized with a re-evaluating block.)
539
+ end