dommy 0.6.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.
- checksums.yaml +4 -4
- data/README.md +30 -38
- data/lib/dommy/animation.rb +10 -2
- data/lib/dommy/attr.rb +197 -32
- data/lib/dommy/backend/nokogiri_adapter.rb +127 -0
- data/lib/dommy/backend/nokolexbor_adapter.rb +117 -0
- data/lib/dommy/backend.rb +175 -0
- data/lib/dommy/blob.rb +30 -11
- data/lib/dommy/bridge/constructor_registry.rb +28 -0
- data/lib/dommy/bridge/methods.rb +57 -0
- data/lib/dommy/bridge.rb +97 -0
- data/lib/dommy/callable_invoker.rb +36 -0
- data/lib/dommy/compression_streams.rb +4 -4
- data/lib/dommy/cookie_store.rb +4 -2
- data/lib/dommy/crypto.rb +16 -9
- data/lib/dommy/css.rb +53 -7
- data/lib/dommy/custom_elements.rb +33 -9
- data/lib/dommy/data_transfer.rb +4 -0
- data/lib/dommy/document.rb +693 -60
- data/lib/dommy/dom_parser.rb +29 -15
- data/lib/dommy/element.rb +1147 -438
- data/lib/dommy/event.rb +279 -79
- data/lib/dommy/event_source.rb +14 -10
- data/lib/dommy/fetch.rb +509 -39
- data/lib/dommy/file_reader.rb +14 -6
- data/lib/dommy/form_data.rb +3 -3
- data/lib/dommy/history.rb +46 -8
- data/lib/dommy/html_collection.rb +59 -6
- data/lib/dommy/html_elements.rb +153 -1502
- data/lib/dommy/internal/css_pseudo_handlers.rb +137 -0
- data/lib/dommy/internal/dom_matching.rb +3 -3
- data/lib/dommy/internal/global_functions.rb +26 -0
- data/lib/dommy/internal/idna.rb +16 -7
- data/lib/dommy/internal/ipv4_parser.rb +22 -7
- data/lib/dommy/internal/mutation_coordinator.rb +11 -2
- data/lib/dommy/internal/namespaces.rb +70 -0
- data/lib/dommy/internal/node_equality.rb +86 -0
- data/lib/dommy/internal/node_traversal.rb +1 -1
- data/lib/dommy/internal/node_wrapper_cache.rb +77 -31
- data/lib/dommy/internal/observable_callback.rb +1 -5
- data/lib/dommy/internal/parent_node.rb +126 -0
- data/lib/dommy/internal/reflected_attributes.rb +103 -13
- data/lib/dommy/internal/selector_parser.rb +664 -0
- data/lib/dommy/internal/template_content_registry.rb +6 -6
- data/lib/dommy/internal/url_parser.rb +677 -0
- data/lib/dommy/intersection_observer.rb +4 -2
- data/lib/dommy/location.rb +10 -4
- data/lib/dommy/media_query_list.rb +10 -4
- data/lib/dommy/message_channel.rb +41 -11
- data/lib/dommy/mutation_observer.rb +76 -23
- data/lib/dommy/navigator.rb +38 -24
- data/lib/dommy/node.rb +158 -16
- data/lib/dommy/notification.rb +6 -4
- data/lib/dommy/parser.rb +13 -13
- data/lib/dommy/performance.rb +4 -0
- data/lib/dommy/performance_observer.rb +4 -2
- data/lib/dommy/promise.rb +14 -14
- data/lib/dommy/range.rb +74 -5
- data/lib/dommy/resize_observer.rb +4 -2
- data/lib/dommy/scheduler.rb +34 -13
- data/lib/dommy/shadow_root.rb +31 -60
- data/lib/dommy/storage.rb +2 -0
- data/lib/dommy/streams.rb +40 -49
- data/lib/dommy/svg_elements.rb +204 -3606
- data/lib/dommy/text_codec.rb +178 -25
- data/lib/dommy/tree_walker.rb +270 -81
- data/lib/dommy/url.rb +305 -450
- data/lib/dommy/url_pattern.rb +2 -0
- data/lib/dommy/version.rb +1 -1
- data/lib/dommy/web_socket.rb +49 -19
- data/lib/dommy/window.rb +205 -203
- data/lib/dommy/worker.rb +12 -12
- data/lib/dommy/xml_http_request.rb +32 -7
- data/lib/dommy.rb +19 -2
- metadata +22 -27
data/lib/dommy/document.rb
CHANGED
|
@@ -11,28 +11,288 @@ require_relative "internal/observer_manager"
|
|
|
11
11
|
require_relative "internal/template_content_registry"
|
|
12
12
|
|
|
13
13
|
module Dommy
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
14
|
+
# DocumentType (`<!doctype html>`) — exposes name / publicId / systemId and
|
|
15
|
+
# nodeType=10. HTML5 doctypes carry empty public/system IDs, but
|
|
16
|
+
# `implementation.createDocumentType` can set them.
|
|
17
17
|
class DocumentType
|
|
18
18
|
include Node
|
|
19
19
|
|
|
20
20
|
attr_reader :name
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
# `owner_document:` links a live doctype (document.doctype) to its document so
|
|
23
|
+
# the ChildNode methods (remove/before/after/replaceWith) act on the tree; a
|
|
24
|
+
# standalone doctype (DOMImplementation.createDocumentType) has none, so those
|
|
25
|
+
# methods are no-ops per spec.
|
|
26
|
+
def initialize(name, public_id = "", system_id = "", owner_document: nil)
|
|
23
27
|
@name = name.to_s
|
|
28
|
+
@public_id = public_id.to_s
|
|
29
|
+
@system_id = system_id.to_s
|
|
30
|
+
@owner_document = owner_document
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# ChildNode mixin — the doctype's parent is the document.
|
|
34
|
+
def remove
|
|
35
|
+
@owner_document&.__internal_remove_doctype__(self)
|
|
36
|
+
nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def before(*nodes)
|
|
40
|
+
return nil unless @owner_document
|
|
41
|
+
|
|
42
|
+
@owner_document.__internal_insert_at_doctype__(nodes, after: false)
|
|
43
|
+
nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def after(*nodes)
|
|
47
|
+
return nil unless @owner_document
|
|
48
|
+
|
|
49
|
+
@owner_document.__internal_insert_at_doctype__(nodes, after: true)
|
|
50
|
+
nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def replace_with(*nodes)
|
|
54
|
+
return nil unless @owner_document
|
|
55
|
+
|
|
56
|
+
@owner_document.__internal_insert_at_doctype__(nodes, after: false)
|
|
57
|
+
remove
|
|
58
|
+
nil
|
|
24
59
|
end
|
|
25
60
|
|
|
26
61
|
def __js_get__(key)
|
|
27
62
|
case key
|
|
28
63
|
when "name"
|
|
29
64
|
@name
|
|
65
|
+
when "nodeName"
|
|
66
|
+
# WHATWG: a DocumentType's nodeName is its name.
|
|
67
|
+
@name
|
|
30
68
|
when "nodeType"
|
|
31
69
|
10
|
|
32
70
|
when "publicId"
|
|
33
|
-
|
|
71
|
+
@public_id
|
|
34
72
|
when "systemId"
|
|
35
|
-
|
|
73
|
+
@system_id
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
include EventTarget
|
|
78
|
+
|
|
79
|
+
def __internal_event_parent__
|
|
80
|
+
nil
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
include Bridge::Methods
|
|
84
|
+
js_methods %w[isEqualNode isSameNode getRootNode hasChildNodes normalize compareDocumentPosition
|
|
85
|
+
appendChild insertBefore removeChild replaceChild before after replaceWith remove
|
|
86
|
+
addEventListener removeEventListener dispatchEvent]
|
|
87
|
+
def __js_call__(method, args)
|
|
88
|
+
case method
|
|
89
|
+
when "hasChildNodes"
|
|
90
|
+
false
|
|
91
|
+
when "isEqualNode"
|
|
92
|
+
is_equal_node(args[0])
|
|
93
|
+
when "isSameNode"
|
|
94
|
+
is_same_node(args[0])
|
|
95
|
+
when "getRootNode"
|
|
96
|
+
get_root_node(args[0])
|
|
97
|
+
when "compareDocumentPosition"
|
|
98
|
+
compare_document_position(args[0])
|
|
99
|
+
when "appendChild", "insertBefore"
|
|
100
|
+
raise DOMException::HierarchyRequestError, "a DocumentType may not have children"
|
|
101
|
+
when "removeChild", "replaceChild"
|
|
102
|
+
raise DOMException::NotFoundError, "the node to be removed is not a child of this node"
|
|
103
|
+
when "before"
|
|
104
|
+
before(*args)
|
|
105
|
+
when "after"
|
|
106
|
+
after(*args)
|
|
107
|
+
when "replaceWith"
|
|
108
|
+
replace_with(*args)
|
|
109
|
+
when "remove"
|
|
110
|
+
remove
|
|
111
|
+
when "normalize"
|
|
112
|
+
nil
|
|
113
|
+
when "addEventListener"
|
|
114
|
+
add_event_listener(args[0], args[1], args[2])
|
|
115
|
+
when "removeEventListener"
|
|
116
|
+
remove_event_listener(args[0], args[1], args[2])
|
|
117
|
+
when "dispatchEvent"
|
|
118
|
+
dispatch_event(args[0])
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# ProcessingInstruction (`<?target data?>`) — a CharacterData-like node with a
|
|
124
|
+
# `target`; created via `document.createProcessingInstruction`.
|
|
125
|
+
class ProcessingInstruction
|
|
126
|
+
include Node
|
|
127
|
+
|
|
128
|
+
attr_reader :target
|
|
129
|
+
|
|
130
|
+
def initialize(target, data)
|
|
131
|
+
@target = target.to_s
|
|
132
|
+
@data = data.to_s
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
include EventTarget
|
|
136
|
+
|
|
137
|
+
def data = @data
|
|
138
|
+
|
|
139
|
+
def __js_get__(key)
|
|
140
|
+
case key
|
|
141
|
+
when "target"
|
|
142
|
+
@target
|
|
143
|
+
when "data", "nodeValue", "textContent"
|
|
144
|
+
@data
|
|
145
|
+
when "nodeName"
|
|
146
|
+
@target
|
|
147
|
+
when "nodeType"
|
|
148
|
+
7
|
|
149
|
+
when "length"
|
|
150
|
+
@data.length
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def __js_set__(key, value)
|
|
155
|
+
case key
|
|
156
|
+
when "data", "nodeValue", "textContent"
|
|
157
|
+
@data = value.to_s
|
|
158
|
+
else
|
|
159
|
+
return Bridge::UNHANDLED
|
|
160
|
+
end
|
|
161
|
+
nil
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# A PI is CharacterData: its data methods are string operations on @data.
|
|
165
|
+
def substring_data(offset, count)
|
|
166
|
+
o = offset.to_i
|
|
167
|
+
raise DOMException::IndexSizeError, "offset out of bounds" if o.negative? || o > @data.length
|
|
168
|
+
|
|
169
|
+
@data[o, count.to_i] || ""
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def append_data(value)
|
|
173
|
+
@data += value.to_s
|
|
174
|
+
nil
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def insert_data(offset, value)
|
|
178
|
+
o = offset.to_i
|
|
179
|
+
raise DOMException::IndexSizeError, "offset out of bounds" if o.negative? || o > @data.length
|
|
180
|
+
|
|
181
|
+
@data = @data[0, o].to_s + value.to_s + (@data[o..] || "")
|
|
182
|
+
nil
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def delete_data(offset, count)
|
|
186
|
+
o = offset.to_i
|
|
187
|
+
raise DOMException::IndexSizeError, "offset out of bounds" if o.negative? || o > @data.length
|
|
188
|
+
|
|
189
|
+
@data = @data[0, o].to_s + (@data[(o + count.to_i)..] || "")
|
|
190
|
+
nil
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def replace_data(offset, count, value)
|
|
194
|
+
delete_data(offset, count)
|
|
195
|
+
insert_data(offset, value)
|
|
196
|
+
nil
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def __internal_event_parent__
|
|
200
|
+
nil
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
include Bridge::Methods
|
|
204
|
+
js_methods %w[isEqualNode isSameNode getRootNode normalize hasChildNodes
|
|
205
|
+
appendData insertData deleteData replaceData substringData compareDocumentPosition
|
|
206
|
+
appendChild insertBefore removeChild replaceChild before after replaceWith remove
|
|
207
|
+
addEventListener removeEventListener dispatchEvent]
|
|
208
|
+
def __js_call__(method, args)
|
|
209
|
+
case method
|
|
210
|
+
when "hasChildNodes"
|
|
211
|
+
false
|
|
212
|
+
when "isEqualNode"
|
|
213
|
+
is_equal_node(args[0])
|
|
214
|
+
when "isSameNode"
|
|
215
|
+
is_same_node(args[0])
|
|
216
|
+
when "getRootNode"
|
|
217
|
+
get_root_node(args[0])
|
|
218
|
+
when "compareDocumentPosition"
|
|
219
|
+
compare_document_position(args[0])
|
|
220
|
+
when "appendChild", "insertBefore"
|
|
221
|
+
raise DOMException::HierarchyRequestError, "a ProcessingInstruction may not have children"
|
|
222
|
+
when "removeChild", "replaceChild"
|
|
223
|
+
raise DOMException::NotFoundError, "the node to be removed is not a child of this node"
|
|
224
|
+
when "before", "after", "replaceWith", "remove"
|
|
225
|
+
# ChildNode mixin: a created PI has no parent, so these are no-ops per
|
|
226
|
+
# spec (they act only when the node is inserted in a tree).
|
|
227
|
+
nil
|
|
228
|
+
when "normalize"
|
|
229
|
+
nil
|
|
230
|
+
when "substringData"
|
|
231
|
+
substring_data(args[0], args[1])
|
|
232
|
+
when "appendData"
|
|
233
|
+
append_data(args[0])
|
|
234
|
+
when "insertData"
|
|
235
|
+
insert_data(args[0], args[1])
|
|
236
|
+
when "deleteData"
|
|
237
|
+
delete_data(args[0], args[1])
|
|
238
|
+
when "replaceData"
|
|
239
|
+
replace_data(args[0], args[1], args[2])
|
|
240
|
+
when "addEventListener"
|
|
241
|
+
add_event_listener(args[0], args[1], args[2])
|
|
242
|
+
when "removeEventListener"
|
|
243
|
+
remove_event_listener(args[0], args[1], args[2])
|
|
244
|
+
when "dispatchEvent"
|
|
245
|
+
dispatch_event(args[0])
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# `document.implementation` — the DOMImplementation. Only the node factories
|
|
251
|
+
# WPT exercises are provided; createDocument/createHTMLDocument are not yet
|
|
252
|
+
# implemented (foreign documents).
|
|
253
|
+
class DOMImplementation
|
|
254
|
+
def initialize(document)
|
|
255
|
+
@document = document
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def create_document_type(qualified_name, public_id, system_id)
|
|
259
|
+
DocumentType.new(qualified_name, public_id, system_id)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# createDocument(namespace, qualifiedName, doctype?) — a fresh XML document,
|
|
263
|
+
# with a document element (namespace, qualifiedName) when qualifiedName is
|
|
264
|
+
# non-empty. (The doctype argument is accepted but not stored, as document
|
|
265
|
+
# equality compares only structure that survives wrap_node.)
|
|
266
|
+
def create_document(namespace, qualified_name, _doctype = nil)
|
|
267
|
+
doc = Document.new(nil, nokogiri_doc: Backend.document_class.new)
|
|
268
|
+
qn = qualified_name.to_s
|
|
269
|
+
unless qn.empty?
|
|
270
|
+
el = doc.send(:create_element_ns, namespace, qualified_name)
|
|
271
|
+
doc.nokogiri_doc.root = el.__dommy_backend_node__
|
|
272
|
+
end
|
|
273
|
+
doc
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# createHTMLDocument(title?) — a fresh HTML document (doctype + html > head,
|
|
277
|
+
# body), with an optional <title>.
|
|
278
|
+
def create_html_document(title = nil)
|
|
279
|
+
doc = Document.new(nil, nokogiri_doc: Backend.parse("<!DOCTYPE html><html><head></head><body></body></html>"))
|
|
280
|
+
doc.title = title.to_s unless title.nil? || title.equal?(Bridge::UNDEFINED)
|
|
281
|
+
doc
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def __js_get__(_key) = nil
|
|
285
|
+
|
|
286
|
+
include Bridge::Methods
|
|
287
|
+
js_methods %w[createDocumentType createDocument createHTMLDocument]
|
|
288
|
+
def __js_call__(method, args)
|
|
289
|
+
case method
|
|
290
|
+
when "createDocumentType"
|
|
291
|
+
create_document_type(args[0], args[1], args[2])
|
|
292
|
+
when "createDocument"
|
|
293
|
+
create_document(args[0], args[1], args[2])
|
|
294
|
+
when "createHTMLDocument"
|
|
295
|
+
create_html_document(args[0])
|
|
36
296
|
end
|
|
37
297
|
end
|
|
38
298
|
end
|
|
@@ -44,8 +304,11 @@ module Dommy
|
|
|
44
304
|
include EventTarget
|
|
45
305
|
include Node
|
|
46
306
|
|
|
47
|
-
attr_reader :
|
|
307
|
+
attr_reader :nokogiri_doc
|
|
48
308
|
attr_accessor :default_view
|
|
309
|
+
# content_type defaults to "text/html"; settable so an integration layer
|
|
310
|
+
# can reflect the response Content-Type. Read-only over the JS bridge.
|
|
311
|
+
attr_accessor :content_type
|
|
49
312
|
|
|
50
313
|
def initialize(host = nil, nokogiri_doc: nil, default_view: nil)
|
|
51
314
|
@host = host
|
|
@@ -56,9 +319,31 @@ module Dommy
|
|
|
56
319
|
@cookie_jar = Internal::CookieJar.new
|
|
57
320
|
@template_content_registry = Internal::TemplateContentRegistry.new(self)
|
|
58
321
|
@mutation_coordinator = Internal::MutationCoordinator.new(self, @observer_manager)
|
|
59
|
-
@
|
|
60
|
-
|
|
61
|
-
@
|
|
322
|
+
@node_iterators = []
|
|
323
|
+
@nokogiri_doc = nokogiri_doc || Backend.parse("<!doctype html><html><head></head><body></body></html>")
|
|
324
|
+
@content_type = "text/html"
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# Whether this is an "HTML document" in the DOM sense (created by the HTML
|
|
328
|
+
# parser / `text/html`), as opposed to an XML document. It drives the
|
|
329
|
+
# case-folding rules: `createElement` lowercases names and `Element#tagName`
|
|
330
|
+
# uppercases HTML-namespace names only in an HTML document. An XML or XHTML
|
|
331
|
+
# document (e.g. an `application/xhtml+xml` / `text/xml` resource) preserves
|
|
332
|
+
# case.
|
|
333
|
+
def html_document?
|
|
334
|
+
@content_type == "text/html"
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# `document.compatMode` — "CSS1Compat" in no-quirks mode, "BackCompat" in
|
|
338
|
+
# quirks mode. A missing doctype is quirks; a bare `<!DOCTYPE html>` (no
|
|
339
|
+
# public/system identifier) is no-quirks. (The full quirks algorithm keys off
|
|
340
|
+
# specific legacy public ids; this covers the common cases.)
|
|
341
|
+
def compat_mode
|
|
342
|
+
dt = @nokogiri_doc.internal_subset
|
|
343
|
+
return "BackCompat" unless dt
|
|
344
|
+
return "CSS1Compat" if dt.name.to_s.downcase == "html" && dt.external_id.nil?
|
|
345
|
+
|
|
346
|
+
"BackCompat"
|
|
62
347
|
end
|
|
63
348
|
|
|
64
349
|
# ----- Public Ruby API (snake_case) -----
|
|
@@ -72,13 +357,38 @@ module Dommy
|
|
|
72
357
|
end
|
|
73
358
|
|
|
74
359
|
def document_element
|
|
75
|
-
|
|
360
|
+
# The document's root element — `<html>` for HTML, the actual root for XML.
|
|
361
|
+
wrap_node(@nokogiri_doc.root)
|
|
76
362
|
end
|
|
77
363
|
|
|
78
364
|
def head
|
|
79
365
|
wrap_node(@nokogiri_doc.at_css("head"))
|
|
80
366
|
end
|
|
81
367
|
|
|
368
|
+
# Resolve `body` fresh from the tree (not memoized) so it tracks a swapped
|
|
369
|
+
# `<body>` — e.g. Turbo's page render does
|
|
370
|
+
# `documentElement.replaceChild(newBody, body)`, after which a stale cached
|
|
371
|
+
# wrapper would keep returning the detached old body. wrap_node caches by
|
|
372
|
+
# node, so identity (`document.body === document.body`) still holds.
|
|
373
|
+
def body
|
|
374
|
+
wrap_node(@nokogiri_doc.at_css("body"))
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# Serialize the whole document to HTML (including the doctype).
|
|
378
|
+
def to_html
|
|
379
|
+
@nokogiri_doc.to_html
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
# XPath queries returning wrapped nodes (Element / TextNode / etc).
|
|
383
|
+
def at_xpath(expression)
|
|
384
|
+
node = @nokogiri_doc.at_xpath(expression)
|
|
385
|
+
node && wrap_node(node)
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def xpath(expression)
|
|
389
|
+
@nokogiri_doc.xpath(expression).map { |node| wrap_node(node) }
|
|
390
|
+
end
|
|
391
|
+
|
|
82
392
|
# `document.URL` / `documentURI` — both return location.href in
|
|
83
393
|
# real browsers (legacy aliases of the same field).
|
|
84
394
|
def url
|
|
@@ -116,6 +426,15 @@ module Dommy
|
|
|
116
426
|
view.location.__js_get__("hostname").to_s
|
|
117
427
|
end
|
|
118
428
|
|
|
429
|
+
# `document.origin` — serialized origin of the document URL, mirroring
|
|
430
|
+
# `window.location.origin`. Empty when there is no associated window.
|
|
431
|
+
def origin
|
|
432
|
+
view = @default_view
|
|
433
|
+
return "" unless view&.location
|
|
434
|
+
|
|
435
|
+
view.location.__js_get__("origin").to_s
|
|
436
|
+
end
|
|
437
|
+
|
|
119
438
|
# `document.referrer` — Dommy never has a referring page, so this
|
|
120
439
|
# is always empty.
|
|
121
440
|
def referrer
|
|
@@ -157,6 +476,15 @@ module Dommy
|
|
|
157
476
|
end
|
|
158
477
|
end
|
|
159
478
|
|
|
479
|
+
# All child nodes of the document (doctype + document element, …), as a live,
|
|
480
|
+
# cached NodeList — unlike `children`, which is element-only. Cached so
|
|
481
|
+
# `document.childNodes === document.childNodes` and mutations are reflected.
|
|
482
|
+
def child_nodes
|
|
483
|
+
@live_child_nodes ||= LiveNodeList.new do
|
|
484
|
+
@nokogiri_doc.children.map { |n| wrap_node(n) }.compact
|
|
485
|
+
end
|
|
486
|
+
end
|
|
487
|
+
|
|
160
488
|
def child_element_count
|
|
161
489
|
children.size
|
|
162
490
|
end
|
|
@@ -172,10 +500,21 @@ module Dommy
|
|
|
172
500
|
# Currently-focused element (or body if none). Updated via
|
|
173
501
|
# `el.focus()` / `el.blur()`.
|
|
174
502
|
def active_element
|
|
175
|
-
@active_element ||
|
|
503
|
+
@active_element || body
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
# `document.contains(node)` — true if `node` is the document itself or any
|
|
507
|
+
# node attached to its tree (per Node.contains, which all nodes including the
|
|
508
|
+
# document expose). Per spec, false for null / a non-Node.
|
|
509
|
+
def contains?(other)
|
|
510
|
+
return true if other.equal?(self)
|
|
511
|
+
return false unless other.respond_to?(:__dommy_backend_node__)
|
|
512
|
+
|
|
513
|
+
node = other.__dommy_backend_node__
|
|
514
|
+
node.document == @nokogiri_doc && node.ancestors.include?(@nokogiri_doc)
|
|
176
515
|
end
|
|
177
516
|
|
|
178
|
-
def
|
|
517
|
+
def __internal_set_active_element__(el)
|
|
179
518
|
@active_element = el
|
|
180
519
|
end
|
|
181
520
|
|
|
@@ -198,13 +537,32 @@ module Dommy
|
|
|
198
537
|
TreeWalker.new(root, what_to_show, filter)
|
|
199
538
|
end
|
|
200
539
|
|
|
540
|
+
# WebIDL `unsigned long whatToShow = 0xFFFFFFFF`: an omitted or `undefined`
|
|
541
|
+
# argument uses the default; `null` coerces to 0; otherwise ToUint32.
|
|
542
|
+
def coerce_what_to_show(args, index)
|
|
543
|
+
return NodeFilter::SHOW_ALL if args.length <= index
|
|
544
|
+
value = args[index]
|
|
545
|
+
return NodeFilter::SHOW_ALL if defined?(Bridge::UNDEFINED) && value.equal?(Bridge::UNDEFINED)
|
|
546
|
+
return 0 if value.nil?
|
|
547
|
+
|
|
548
|
+
value.to_i % (2**32)
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
# A `null`/`undefined` filter argument means "no filter".
|
|
552
|
+
def normalize_filter(value)
|
|
553
|
+
return nil if value.nil?
|
|
554
|
+
return nil if defined?(Bridge::UNDEFINED) && value.equal?(Bridge::UNDEFINED)
|
|
555
|
+
|
|
556
|
+
value
|
|
557
|
+
end
|
|
558
|
+
|
|
201
559
|
# Copy a node from another document into this one. The returned
|
|
202
560
|
# wrapper is owned by `this`. Per spec, the source node is left
|
|
203
561
|
# in place. `deep: true` copies the entire subtree.
|
|
204
562
|
def import_node(node, deep = false)
|
|
205
|
-
return nil unless node.respond_to?(:
|
|
563
|
+
return nil unless node.respond_to?(:__dommy_backend_node__)
|
|
206
564
|
|
|
207
|
-
copy = clone_into_doc(node.
|
|
565
|
+
copy = clone_into_doc(node.__dommy_backend_node__, deep)
|
|
208
566
|
wrap_node(copy)
|
|
209
567
|
end
|
|
210
568
|
|
|
@@ -212,17 +570,32 @@ module Dommy
|
|
|
212
570
|
# node is detached from its previous owner and its ownerDocument
|
|
213
571
|
# becomes this. Returns the (possibly re-wrapped) node.
|
|
214
572
|
def adopt_node(node)
|
|
215
|
-
return nil unless node.respond_to?(:
|
|
573
|
+
return nil unless node.respond_to?(:__dommy_backend_node__)
|
|
216
574
|
|
|
217
|
-
src = node.
|
|
575
|
+
src = node.__dommy_backend_node__
|
|
218
576
|
src.unlink if src.parent
|
|
219
|
-
moved = if src.document == @nokogiri_doc
|
|
220
|
-
src
|
|
221
|
-
else
|
|
222
|
-
clone_into_doc(src, true)
|
|
223
|
-
end
|
|
224
577
|
|
|
225
|
-
|
|
578
|
+
# Same document: just return the wrapper after the detach above.
|
|
579
|
+
return wrap_node(src) if src.document == @nokogiri_doc
|
|
580
|
+
|
|
581
|
+
# Cross-document: Nokogiri reassigns `src.document` when src is
|
|
582
|
+
# added under a node owned by another document. We transiently
|
|
583
|
+
# attach to our root, then unlink so src ends up free-floating
|
|
584
|
+
# but now belongs to @nokogiri_doc. The underlying Ruby object
|
|
585
|
+
# identity is preserved.
|
|
586
|
+
src_doc_wrapper = node.instance_variable_get(:@document)
|
|
587
|
+
@nokogiri_doc.root.add_child(src)
|
|
588
|
+
src.unlink
|
|
589
|
+
|
|
590
|
+
# Move the caller's Dommy wrapper from the source document's
|
|
591
|
+
# wrapper cache into ours, and re-point its @document. This
|
|
592
|
+
# keeps `adopt_node(x).equal?(x)` true across documents.
|
|
593
|
+
node.instance_variable_set(:@document, self)
|
|
594
|
+
if src_doc_wrapper.respond_to?(:__internal_reset_wrapper__)
|
|
595
|
+
src_doc_wrapper.__internal_reset_wrapper__(src)
|
|
596
|
+
end
|
|
597
|
+
@node_wrapper_cache.register(src, node)
|
|
598
|
+
node
|
|
226
599
|
end
|
|
227
600
|
|
|
228
601
|
# Legacy `document.createEvent("EventName")` factory. Returns an
|
|
@@ -268,7 +641,7 @@ module Dommy
|
|
|
268
641
|
# is the read side.
|
|
269
642
|
attr_reader :fullscreen_element
|
|
270
643
|
|
|
271
|
-
def
|
|
644
|
+
def __internal_set_fullscreen_element__(element)
|
|
272
645
|
previous = @fullscreen_element
|
|
273
646
|
@fullscreen_element = element
|
|
274
647
|
return if previous == element
|
|
@@ -297,7 +670,11 @@ module Dommy
|
|
|
297
670
|
# `document.createNodeIterator(root, whatToShow?, filter?)` —
|
|
298
671
|
# flat depth-first iteration.
|
|
299
672
|
def create_node_iterator(root, what_to_show = NodeFilter::SHOW_ALL, filter = nil)
|
|
300
|
-
NodeIterator.new(root, what_to_show, filter)
|
|
673
|
+
iterator = NodeIterator.new(root, what_to_show, filter)
|
|
674
|
+
# Track live iterators so node removal can run the "NodeIterator
|
|
675
|
+
# pre-removing steps" (adjusting referenceNode) before a node detaches.
|
|
676
|
+
@node_iterators << iterator
|
|
677
|
+
iterator
|
|
301
678
|
end
|
|
302
679
|
|
|
303
680
|
# Minimal DocumentType — represents the `<!doctype html>` line.
|
|
@@ -305,7 +682,114 @@ module Dommy
|
|
|
305
682
|
# stub object whose only useful field is `name`. Tests just need
|
|
306
683
|
# `nodeType == 10`.
|
|
307
684
|
def doctype
|
|
308
|
-
|
|
685
|
+
return nil if @doctype_removed
|
|
686
|
+
|
|
687
|
+
@doctype ||= DocumentType.new("html", owner_document: self)
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
def implementation
|
|
691
|
+
@implementation ||= DOMImplementation.new(self)
|
|
692
|
+
end
|
|
693
|
+
|
|
694
|
+
def create_processing_instruction(target, data)
|
|
695
|
+
ProcessingInstruction.new(target, data)
|
|
696
|
+
end
|
|
697
|
+
|
|
698
|
+
# Append a node as a child of the document itself (e.g. a comment alongside
|
|
699
|
+
# the document element). Adopts the node into this document.
|
|
700
|
+
def append_child(node)
|
|
701
|
+
return node unless node.respond_to?(:__dommy_backend_node__)
|
|
702
|
+
|
|
703
|
+
@nokogiri_doc.add_child(node.__dommy_backend_node__)
|
|
704
|
+
node
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
# ParentNode / Node mutation on the document's direct children (the doctype
|
|
708
|
+
# and the document element). Operate on the Nokogiri document node; string
|
|
709
|
+
# arguments (which would need a text child the document can't hold) are
|
|
710
|
+
# ignored rather than raising.
|
|
711
|
+
def document_insert(args, prepend:)
|
|
712
|
+
nodes = args.filter_map { |a| backend_node(a) }
|
|
713
|
+
if prepend && (first = @nokogiri_doc.children.first)
|
|
714
|
+
nodes.reverse_each { |n| first.add_previous_sibling(n) }
|
|
715
|
+
else
|
|
716
|
+
nodes.each { |n| @nokogiri_doc.add_child(n) }
|
|
717
|
+
end
|
|
718
|
+
nil
|
|
719
|
+
end
|
|
720
|
+
|
|
721
|
+
def document_replace_children(args)
|
|
722
|
+
@nokogiri_doc.children.each(&:unlink)
|
|
723
|
+
args.filter_map { |a| backend_node(a) }.each { |n| @nokogiri_doc.add_child(n) }
|
|
724
|
+
nil
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
def document_remove_child(node)
|
|
728
|
+
# The doctype is synthesized from the Nokogiri DTD rather than wrapped as a
|
|
729
|
+
# tree node, so remove the internal subset directly.
|
|
730
|
+
return __internal_remove_doctype__(node) if node.is_a?(DocumentType)
|
|
731
|
+
|
|
732
|
+
bn = backend_node(node)
|
|
733
|
+
raise DOMException::NotFoundError, "node is not a child of this document" unless bn && bn.parent == @nokogiri_doc
|
|
734
|
+
|
|
735
|
+
bn.unlink
|
|
736
|
+
node
|
|
737
|
+
end
|
|
738
|
+
|
|
739
|
+
def document_insert_before(node, ref)
|
|
740
|
+
bn = backend_node(node)
|
|
741
|
+
return node unless bn
|
|
742
|
+
|
|
743
|
+
ref_node = ref && backend_node(ref)
|
|
744
|
+
if ref_node && ref_node.parent == @nokogiri_doc
|
|
745
|
+
ref_node.add_previous_sibling(bn)
|
|
746
|
+
else
|
|
747
|
+
@nokogiri_doc.add_child(bn)
|
|
748
|
+
end
|
|
749
|
+
node
|
|
750
|
+
end
|
|
751
|
+
|
|
752
|
+
def document_replace_child(new_child, old_child)
|
|
753
|
+
old_bn = backend_node(old_child)
|
|
754
|
+
raise DOMException::NotFoundError, "node is not a child of this document" unless old_bn && old_bn.parent == @nokogiri_doc
|
|
755
|
+
|
|
756
|
+
new_bn = backend_node(new_child)
|
|
757
|
+
old_bn.add_previous_sibling(new_bn) if new_bn
|
|
758
|
+
old_bn.unlink
|
|
759
|
+
old_child
|
|
760
|
+
end
|
|
761
|
+
|
|
762
|
+
# Called by DocumentType#remove — the doctype is synthesized from the DTD, so
|
|
763
|
+
# remove the internal subset and mark it gone.
|
|
764
|
+
def __internal_remove_doctype__(_doctype)
|
|
765
|
+
@doctype_removed = true
|
|
766
|
+
@nokogiri_doc.internal_subset&.unlink
|
|
767
|
+
nil
|
|
768
|
+
end
|
|
769
|
+
|
|
770
|
+
# Called by DocumentType#before/#after — insert `nodes` before the doctype
|
|
771
|
+
# (at the document start) or after it (just before the document element).
|
|
772
|
+
def __internal_insert_at_doctype__(nodes, after:)
|
|
773
|
+
bns = nodes.filter_map { |n| backend_node(n) }
|
|
774
|
+
if after
|
|
775
|
+
root = @nokogiri_doc.root
|
|
776
|
+
root ? bns.each { |n| root.add_previous_sibling(n) } : bns.each { |n| @nokogiri_doc.add_child(n) }
|
|
777
|
+
else
|
|
778
|
+
first = @nokogiri_doc.children.first
|
|
779
|
+
first ? bns.reverse_each { |n| first.add_previous_sibling(n) } : bns.each { |n| @nokogiri_doc.add_child(n) }
|
|
780
|
+
end
|
|
781
|
+
nil
|
|
782
|
+
end
|
|
783
|
+
|
|
784
|
+
# `document.cloneNode(deep)` → a fresh Document over a (deep) copy of the
|
|
785
|
+
# Nokogiri tree, preserving the content type.
|
|
786
|
+
def clone_node(deep)
|
|
787
|
+
copy = deep ? @nokogiri_doc.dup : Backend.document_class.new
|
|
788
|
+
Document.new(nil, nokogiri_doc: copy).tap { |d| d.content_type = @content_type }
|
|
789
|
+
end
|
|
790
|
+
|
|
791
|
+
def backend_node(node)
|
|
792
|
+
node.respond_to?(:__dommy_backend_node__) ? node.__dommy_backend_node__ : nil
|
|
309
793
|
end
|
|
310
794
|
|
|
311
795
|
# Delegate to CookieJar
|
|
@@ -331,6 +815,10 @@ module Dommy
|
|
|
331
815
|
@node_wrapper_cache.get_elements_by_name(name)
|
|
332
816
|
end
|
|
333
817
|
|
|
818
|
+
def get_elements_by_tag_name_ns(namespace, local_name)
|
|
819
|
+
HTMLCollection.elements_by_tag_name_ns(@nokogiri_doc, self, namespace, local_name)
|
|
820
|
+
end
|
|
821
|
+
|
|
334
822
|
# `document.write(html)` — legacy API. Appends parsed nodes to the
|
|
335
823
|
# body. Real browsers only re-stream the DOM during initial parse;
|
|
336
824
|
# this stub is enough for tests that fire write() during teardown.
|
|
@@ -339,8 +827,9 @@ module Dommy
|
|
|
339
827
|
fragment = Parser.fragment(html, owner_doc: @nokogiri_doc)
|
|
340
828
|
removed = []
|
|
341
829
|
added = fragment.children.to_a
|
|
342
|
-
|
|
343
|
-
|
|
830
|
+
body_node = body.__dommy_backend_node__
|
|
831
|
+
added.each { |node| body_node.add_child(node) }
|
|
832
|
+
notify_child_list_mutation(target_node: body_node, added_nodes: added, removed_nodes: removed)
|
|
344
833
|
nil
|
|
345
834
|
end
|
|
346
835
|
|
|
@@ -368,6 +857,10 @@ module Dommy
|
|
|
368
857
|
@node_wrapper_cache.create_comment(text)
|
|
369
858
|
end
|
|
370
859
|
|
|
860
|
+
def create_cdata_section(text)
|
|
861
|
+
@node_wrapper_cache.create_cdata_section(text)
|
|
862
|
+
end
|
|
863
|
+
|
|
371
864
|
def create_document_fragment
|
|
372
865
|
@node_wrapper_cache.create_document_fragment
|
|
373
866
|
end
|
|
@@ -379,11 +872,13 @@ module Dommy
|
|
|
379
872
|
def __js_get__(key)
|
|
380
873
|
case key
|
|
381
874
|
when "body"
|
|
382
|
-
|
|
875
|
+
body
|
|
383
876
|
when "head"
|
|
384
877
|
head
|
|
385
878
|
when "doctype"
|
|
386
879
|
doctype
|
|
880
|
+
when "implementation"
|
|
881
|
+
implementation
|
|
387
882
|
when "defaultView"
|
|
388
883
|
@default_view
|
|
389
884
|
when "fullscreenElement"
|
|
@@ -393,7 +888,8 @@ module Dommy
|
|
|
393
888
|
when "scrollingElement"
|
|
394
889
|
wrap_node(@nokogiri_doc.at_css("html"))
|
|
395
890
|
when "documentElement"
|
|
396
|
-
|
|
891
|
+
# The document's root element — `<html>` for HTML, the actual root for XML.
|
|
892
|
+
wrap_node(@nokogiri_doc.root)
|
|
397
893
|
when "title"
|
|
398
894
|
read_title
|
|
399
895
|
when "cookie"
|
|
@@ -408,6 +904,37 @@ module Dommy
|
|
|
408
904
|
base_uri
|
|
409
905
|
when "domain"
|
|
410
906
|
domain
|
|
907
|
+
when "origin"
|
|
908
|
+
origin
|
|
909
|
+
when "contentType"
|
|
910
|
+
content_type
|
|
911
|
+
when "location"
|
|
912
|
+
# document.location is the same Location object as window.location.
|
|
913
|
+
@default_view&.__js_get__("location")
|
|
914
|
+
when "characterSet", "charset", "inputEncoding"
|
|
915
|
+
# The DOM is held as Ruby strings (UTF-8); we don't model other encodings.
|
|
916
|
+
"UTF-8"
|
|
917
|
+
when "dir"
|
|
918
|
+
document_element&.get_attribute("dir") || ""
|
|
919
|
+
when "designMode"
|
|
920
|
+
@design_mode || "off"
|
|
921
|
+
when "lastModified"
|
|
922
|
+
@last_modified || "01/01/1970 00:00:00"
|
|
923
|
+
when "readyState"
|
|
924
|
+
# The document is fully parsed by the time scripts run against it (there
|
|
925
|
+
# is no incremental network parse), so it is always "complete". Code that
|
|
926
|
+
# gates on `document.readyState === "loading"` (e.g. Turbo's preloader)
|
|
927
|
+
# therefore takes the already-loaded path.
|
|
928
|
+
"complete"
|
|
929
|
+
when "visibilityState"
|
|
930
|
+
# There's no real viewport/tab; the document is treated as the visible,
|
|
931
|
+
# foreground page (so `nextRepaint`-style code uses requestAnimationFrame,
|
|
932
|
+
# and `=== "visible"` checks pass).
|
|
933
|
+
"visible"
|
|
934
|
+
when "hidden"
|
|
935
|
+
false
|
|
936
|
+
when "compatMode"
|
|
937
|
+
compat_mode
|
|
411
938
|
when "referrer"
|
|
412
939
|
referrer
|
|
413
940
|
when "links"
|
|
@@ -418,8 +945,28 @@ module Dommy
|
|
|
418
945
|
scripts
|
|
419
946
|
when "images"
|
|
420
947
|
images
|
|
948
|
+
when "embeds", "plugins"
|
|
949
|
+
# Both reflect the same list of <embed> elements.
|
|
950
|
+
HTMLCollection.new { @nokogiri_doc.css("embed").map { |n| wrap_node(n) }.compact }
|
|
951
|
+
when "anchors"
|
|
952
|
+
# Historically `<a name>` (with a name attribute), not every link.
|
|
953
|
+
HTMLCollection.new { @nokogiri_doc.css("a[name]").map { |n| wrap_node(n) }.compact }
|
|
954
|
+
when "styleSheets"
|
|
955
|
+
# No CSS engine; expose an empty (but present + iterable) StyleSheetList
|
|
956
|
+
# so `document.styleSheets.length` / iteration don't blow up.
|
|
957
|
+
NodeList.new
|
|
421
958
|
when "children"
|
|
422
959
|
children
|
|
960
|
+
when "childNodes"
|
|
961
|
+
child_nodes
|
|
962
|
+
when "firstChild"
|
|
963
|
+
child_nodes.to_a.first
|
|
964
|
+
when "lastChild"
|
|
965
|
+
child_nodes.to_a.last
|
|
966
|
+
when "parentNode", "parentElement", "nextSibling", "previousSibling", "ownerDocument"
|
|
967
|
+
# A document is the tree root: no parent or siblings, and its
|
|
968
|
+
# ownerDocument is null per spec.
|
|
969
|
+
nil
|
|
423
970
|
when "childElementCount"
|
|
424
971
|
child_element_count
|
|
425
972
|
when "firstElementChild"
|
|
@@ -439,13 +986,62 @@ module Dommy
|
|
|
439
986
|
write_title(value.to_s)
|
|
440
987
|
when "cookie"
|
|
441
988
|
self.cookie = value.to_s
|
|
989
|
+
when "dir"
|
|
990
|
+
document_element&.set_attribute("dir", value.to_s)
|
|
991
|
+
when "designMode"
|
|
992
|
+
# Enumerated: only "on"/"off" (case-insensitive), else ignored.
|
|
993
|
+
v = value.to_s.downcase
|
|
994
|
+
@design_mode = v if %w[on off].include?(v)
|
|
995
|
+
when "location"
|
|
996
|
+
# `document.location = url` navigates, same as `location.href = url`.
|
|
997
|
+
loc = @default_view&.__js_get__("location")
|
|
998
|
+
loc&.__js_set__("href", value)
|
|
999
|
+
else
|
|
1000
|
+
return Bridge::UNHANDLED
|
|
442
1001
|
end
|
|
443
1002
|
|
|
444
1003
|
nil
|
|
445
1004
|
end
|
|
446
1005
|
|
|
1006
|
+
include Bridge::Methods
|
|
1007
|
+
js_methods %w[
|
|
1008
|
+
exitFullscreen startViewTransition createElement createElementNS createTextNode
|
|
1009
|
+
createComment createCDATASection createProcessingInstruction createDocumentFragment querySelector querySelectorAll getElementById
|
|
1010
|
+
getElementsByClassName getElementsByTagName getElementsByTagNameNS getElementsByName createAttribute
|
|
1011
|
+
createAttributeNS createTreeWalker createNodeIterator createRange createEvent importNode
|
|
1012
|
+
adoptNode hasFocus getSelection elementFromPoint queryCommandSupported addEventListener
|
|
1013
|
+
removeEventListener dispatchEvent write writeln open close isEqualNode appendChild
|
|
1014
|
+
hasChildNodes contains append prepend replaceChildren removeChild insertBefore replaceChild
|
|
1015
|
+
cloneNode normalize
|
|
1016
|
+
]
|
|
447
1017
|
def __js_call__(method, args)
|
|
448
1018
|
case method
|
|
1019
|
+
when "hasChildNodes"
|
|
1020
|
+
@nokogiri_doc.children.any?
|
|
1021
|
+
when "contains"
|
|
1022
|
+
contains?(args[0])
|
|
1023
|
+
when "isEqualNode"
|
|
1024
|
+
is_equal_node(args[0])
|
|
1025
|
+
when "appendChild"
|
|
1026
|
+
append_child(args[0])
|
|
1027
|
+
when "append"
|
|
1028
|
+
document_insert(args, prepend: false)
|
|
1029
|
+
when "prepend"
|
|
1030
|
+
document_insert(args, prepend: true)
|
|
1031
|
+
when "replaceChildren"
|
|
1032
|
+
document_replace_children(args)
|
|
1033
|
+
when "removeChild"
|
|
1034
|
+
document_remove_child(args[0])
|
|
1035
|
+
when "insertBefore"
|
|
1036
|
+
document_insert_before(args[0], args[1])
|
|
1037
|
+
when "replaceChild"
|
|
1038
|
+
document_replace_child(args[0], args[1])
|
|
1039
|
+
when "cloneNode"
|
|
1040
|
+
clone_node(args[0])
|
|
1041
|
+
when "normalize"
|
|
1042
|
+
nil # the document has no text children to merge
|
|
1043
|
+
when "writeln"
|
|
1044
|
+
write(*(args + ["\n"]))
|
|
449
1045
|
when "exitFullscreen"
|
|
450
1046
|
exit_fullscreen
|
|
451
1047
|
when "startViewTransition"
|
|
@@ -468,16 +1064,22 @@ module Dommy
|
|
|
468
1064
|
create_text_node(args[0])
|
|
469
1065
|
when "createComment"
|
|
470
1066
|
create_comment(args[0])
|
|
1067
|
+
when "createCDATASection"
|
|
1068
|
+
create_cdata_section(args[0])
|
|
1069
|
+
when "createProcessingInstruction"
|
|
1070
|
+
create_processing_instruction(args[0], args[1])
|
|
471
1071
|
when "createDocumentFragment"
|
|
472
1072
|
create_document_fragment
|
|
473
1073
|
when "querySelector"
|
|
474
|
-
query_selector(args
|
|
1074
|
+
query_selector(Internal.css_query_arg!(args))
|
|
475
1075
|
when "querySelectorAll"
|
|
476
|
-
query_selector_all(args
|
|
1076
|
+
query_selector_all(Internal.css_query_arg!(args))
|
|
477
1077
|
when "getElementById"
|
|
478
1078
|
get_element_by_id(args[0])
|
|
479
1079
|
when "getElementsByClassName"
|
|
480
1080
|
get_elements_by_class_name(args[0])
|
|
1081
|
+
when "getElementsByTagNameNS"
|
|
1082
|
+
get_elements_by_tag_name_ns(args[0], args[1])
|
|
481
1083
|
when "getElementsByTagName"
|
|
482
1084
|
get_elements_by_tag_name(args[0])
|
|
483
1085
|
when "getElementsByName"
|
|
@@ -487,9 +1089,11 @@ module Dommy
|
|
|
487
1089
|
when "createAttributeNS"
|
|
488
1090
|
create_attribute_ns(args[0], args[1])
|
|
489
1091
|
when "createTreeWalker"
|
|
490
|
-
create_tree_walker(args[0], args
|
|
1092
|
+
create_tree_walker(args[0], coerce_what_to_show(args, 1), normalize_filter(args[2]))
|
|
491
1093
|
when "createNodeIterator"
|
|
492
|
-
create_node_iterator(args[0], args
|
|
1094
|
+
create_node_iterator(args[0], coerce_what_to_show(args, 1), normalize_filter(args[2]))
|
|
1095
|
+
when "createRange"
|
|
1096
|
+
create_range
|
|
493
1097
|
when "createEvent"
|
|
494
1098
|
create_event(args[0])
|
|
495
1099
|
when "importNode"
|
|
@@ -507,7 +1111,7 @@ module Dommy
|
|
|
507
1111
|
when "addEventListener"
|
|
508
1112
|
add_event_listener(args[0], args[1], args[2])
|
|
509
1113
|
when "removeEventListener"
|
|
510
|
-
remove_event_listener(args[0], args[1])
|
|
1114
|
+
remove_event_listener(args[0], args[1], args[2])
|
|
511
1115
|
when "dispatchEvent"
|
|
512
1116
|
dispatch_event(args[0])
|
|
513
1117
|
when "write"
|
|
@@ -521,7 +1125,7 @@ module Dommy
|
|
|
521
1125
|
end
|
|
522
1126
|
end
|
|
523
1127
|
|
|
524
|
-
def
|
|
1128
|
+
def __internal_event_parent__
|
|
525
1129
|
@default_view
|
|
526
1130
|
end
|
|
527
1131
|
|
|
@@ -533,7 +1137,7 @@ module Dommy
|
|
|
533
1137
|
# Clear the cached wrapper so the next `wrap_node` creates a new
|
|
534
1138
|
# one. Used by `customElements.define` to upgrade nodes that were
|
|
535
1139
|
# constructed before the registration landed.
|
|
536
|
-
def
|
|
1140
|
+
def __internal_reset_wrapper__(nokogiri_node)
|
|
537
1141
|
@node_wrapper_cache.reset_wrapper(nokogiri_node)
|
|
538
1142
|
end
|
|
539
1143
|
|
|
@@ -543,15 +1147,15 @@ module Dommy
|
|
|
543
1147
|
# node back to its shadow boundary.
|
|
544
1148
|
# Delegate to ShadowRootRegistry
|
|
545
1149
|
|
|
546
|
-
def
|
|
1150
|
+
def __internal_register_shadow_fragment__(fragment_node, shadow_root)
|
|
547
1151
|
@shadow_registry.register(fragment_node, shadow_root)
|
|
548
1152
|
end
|
|
549
1153
|
|
|
550
|
-
def
|
|
1154
|
+
def __internal_shadow_root_for_fragment__(fragment_node)
|
|
551
1155
|
@shadow_registry.find_for_fragment(fragment_node)
|
|
552
1156
|
end
|
|
553
1157
|
|
|
554
|
-
def
|
|
1158
|
+
def __internal_shadow_root_containing__(node)
|
|
555
1159
|
@shadow_registry.find_enclosing(node)
|
|
556
1160
|
end
|
|
557
1161
|
|
|
@@ -560,23 +1164,23 @@ module Dommy
|
|
|
560
1164
|
# break the whole mutation pipeline.
|
|
561
1165
|
# Delegate to MutationCoordinator
|
|
562
1166
|
|
|
563
|
-
def
|
|
1167
|
+
def __internal_notify_connected__(element)
|
|
564
1168
|
@mutation_coordinator.notify_connected(element)
|
|
565
1169
|
end
|
|
566
1170
|
|
|
567
|
-
def
|
|
1171
|
+
def __internal_notify_disconnected__(element)
|
|
568
1172
|
@mutation_coordinator.notify_disconnected(element)
|
|
569
1173
|
end
|
|
570
1174
|
|
|
571
|
-
def
|
|
1175
|
+
def __internal_notify_connected_subtree__(nk)
|
|
572
1176
|
@mutation_coordinator.notify_connected_subtree(nk)
|
|
573
1177
|
end
|
|
574
1178
|
|
|
575
|
-
def
|
|
1179
|
+
def __internal_notify_disconnected_subtree__(nk)
|
|
576
1180
|
@mutation_coordinator.notify_disconnected_subtree(nk)
|
|
577
1181
|
end
|
|
578
1182
|
|
|
579
|
-
def
|
|
1183
|
+
def __internal_notify_attribute_changed__(element, name, old_value, new_value)
|
|
580
1184
|
@mutation_coordinator.notify_attribute_changed(element, name, old_value, new_value)
|
|
581
1185
|
end
|
|
582
1186
|
|
|
@@ -604,11 +1208,43 @@ module Dommy
|
|
|
604
1208
|
)
|
|
605
1209
|
end
|
|
606
1210
|
|
|
607
|
-
|
|
1211
|
+
# Unlink a backend node from its parent and queue a childList removal record
|
|
1212
|
+
# capturing the node's position (previous/next sibling) BEFORE the unlink, so
|
|
1213
|
+
# the record's previousSibling/nextSibling are correct (the coordinator can't
|
|
1214
|
+
# recover them once the node is detached). Used by every remove path.
|
|
1215
|
+
def remove_node_with_notify(node)
|
|
1216
|
+
parent = node.parent
|
|
1217
|
+
return unless parent
|
|
1218
|
+
|
|
1219
|
+
prev_w = node.previous_sibling && wrap_node(node.previous_sibling)
|
|
1220
|
+
next_w = node.next_sibling && wrap_node(node.next_sibling)
|
|
1221
|
+
run_node_iterator_pre_remove(node)
|
|
1222
|
+
node.unlink
|
|
1223
|
+
notify_child_list_mutation(
|
|
1224
|
+
target_node: parent,
|
|
1225
|
+
added_nodes: [],
|
|
1226
|
+
removed_nodes: [node],
|
|
1227
|
+
previous_sibling: prev_w,
|
|
1228
|
+
next_sibling: next_w
|
|
1229
|
+
)
|
|
1230
|
+
end
|
|
1231
|
+
|
|
1232
|
+
# Run the "NodeIterator pre-removing steps" for every live iterator before
|
|
1233
|
+
# `backend_node` is detached, so referenceNode/pointerBeforeReferenceNode
|
|
1234
|
+
# stay valid. `backend_node` must still be attached (tree intact) here.
|
|
1235
|
+
def run_node_iterator_pre_remove(backend_node)
|
|
1236
|
+
return if @node_iterators.empty?
|
|
1237
|
+
|
|
1238
|
+
removed = wrap_node(backend_node)
|
|
1239
|
+
@node_iterators.each { |iter| iter.pre_remove(removed) }
|
|
1240
|
+
end
|
|
1241
|
+
|
|
1242
|
+
def notify_attribute_mutation(target_node:, attribute_name:, old_value:, namespace: nil)
|
|
608
1243
|
@mutation_coordinator.notify_attribute_mutation(
|
|
609
1244
|
target_node: target_node,
|
|
610
1245
|
attribute_name: attribute_name,
|
|
611
|
-
old_value: old_value
|
|
1246
|
+
old_value: old_value,
|
|
1247
|
+
namespace: namespace
|
|
612
1248
|
)
|
|
613
1249
|
end
|
|
614
1250
|
|
|
@@ -619,11 +1255,6 @@ module Dommy
|
|
|
619
1255
|
)
|
|
620
1256
|
end
|
|
621
1257
|
|
|
622
|
-
# Spec-permitted name pattern (XML "Name" production restricted to
|
|
623
|
-
# ASCII for practicality). Used by `createElement` and
|
|
624
|
-
# `createAttribute` to validate the argument.
|
|
625
|
-
NAME_RE = /\A[A-Za-z_][\w\-.:]*\z/.freeze
|
|
626
|
-
|
|
627
1258
|
# Delegate factory methods to NodeWrapperCache
|
|
628
1259
|
|
|
629
1260
|
def create_element(name)
|
|
@@ -675,17 +1306,17 @@ module Dommy
|
|
|
675
1306
|
# adoptNode for cross-document transfer.
|
|
676
1307
|
def clone_into_doc(source, deep)
|
|
677
1308
|
copy = if source.element?
|
|
678
|
-
new_el =
|
|
679
|
-
|
|
1309
|
+
new_el = Backend.create_element(source.name, @nokogiri_doc)
|
|
1310
|
+
Backend.attribute_nodes(source).each { |a| new_el[a.name] = a.value }
|
|
680
1311
|
new_el
|
|
681
1312
|
elsif source.text?
|
|
682
|
-
|
|
683
|
-
elsif source.is_a?(
|
|
684
|
-
|
|
1313
|
+
Backend.create_text(source.content, @nokogiri_doc)
|
|
1314
|
+
elsif source.is_a?(Backend.comment_class)
|
|
1315
|
+
Backend.create_comment(source.content, @nokogiri_doc)
|
|
685
1316
|
else
|
|
686
1317
|
# Fallback: serialize + reparse via fragment for unusual types.
|
|
687
1318
|
fragment = Parser.fragment(source.to_html, owner_doc: @nokogiri_doc)
|
|
688
|
-
fragment.children.first ||
|
|
1319
|
+
fragment.children.first || Backend.create_text("", @nokogiri_doc)
|
|
689
1320
|
end
|
|
690
1321
|
|
|
691
1322
|
if deep && source.respond_to?(:children)
|
|
@@ -709,12 +1340,12 @@ module Dommy
|
|
|
709
1340
|
|
|
710
1341
|
title = head.at_css("title")
|
|
711
1342
|
unless title
|
|
712
|
-
title =
|
|
1343
|
+
title = Backend.create_element("title", @nokogiri_doc)
|
|
713
1344
|
head.add_child(title)
|
|
714
1345
|
end
|
|
715
1346
|
|
|
716
1347
|
title.children.each(&:unlink)
|
|
717
|
-
title.add_child(
|
|
1348
|
+
title.add_child(Backend.create_text(value, @nokogiri_doc))
|
|
718
1349
|
end
|
|
719
1350
|
|
|
720
1351
|
end
|
|
@@ -756,6 +1387,8 @@ module Dommy
|
|
|
756
1387
|
end
|
|
757
1388
|
end
|
|
758
1389
|
|
|
1390
|
+
include Bridge::Methods
|
|
1391
|
+
js_methods %w[skipTransition]
|
|
759
1392
|
def __js_call__(method, _args)
|
|
760
1393
|
case method
|
|
761
1394
|
when "skipTransition"
|