dommy 0.5.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 +7 -0
- data/README.md +213 -0
- data/lib/dommy/attr.rb +200 -0
- data/lib/dommy/blob.rb +182 -0
- data/lib/dommy/bridge.rb +141 -0
- data/lib/dommy/css.rb +283 -0
- data/lib/dommy/custom_elements.rb +125 -0
- data/lib/dommy/data_transfer.rb +98 -0
- data/lib/dommy/document.rb +674 -0
- data/lib/dommy/dom_exception.rb +258 -0
- data/lib/dommy/dom_parser.rb +88 -0
- data/lib/dommy/element.rb +1975 -0
- data/lib/dommy/event.rb +589 -0
- data/lib/dommy/fetch.rb +241 -0
- data/lib/dommy/form_data.rb +208 -0
- data/lib/dommy/html_collection.rb +207 -0
- data/lib/dommy/html_elements.rb +4455 -0
- data/lib/dommy/internal/cookie_jar.rb +27 -0
- data/lib/dommy/internal/dom_matching.rb +141 -0
- data/lib/dommy/internal/mutation_coordinator.rb +172 -0
- data/lib/dommy/internal/node_traversal.rb +36 -0
- data/lib/dommy/internal/node_wrapper_cache.rb +179 -0
- data/lib/dommy/internal/observer_manager.rb +31 -0
- data/lib/dommy/internal/observer_matcher.rb +31 -0
- data/lib/dommy/internal/scope_resolution.rb +27 -0
- data/lib/dommy/internal/shadow_root_registry.rb +35 -0
- data/lib/dommy/internal/template_content_registry.rb +97 -0
- data/lib/dommy/minitest/assertions.rb +105 -0
- data/lib/dommy/minitest.rb +17 -0
- data/lib/dommy/navigator.rb +271 -0
- data/lib/dommy/node.rb +218 -0
- data/lib/dommy/observer.rb +199 -0
- data/lib/dommy/parser.rb +29 -0
- data/lib/dommy/promise.rb +199 -0
- data/lib/dommy/router.rb +275 -0
- data/lib/dommy/rspec/capy_style_matchers.rb +356 -0
- data/lib/dommy/rspec/matchers.rb +230 -0
- data/lib/dommy/rspec.rb +18 -0
- data/lib/dommy/scheduler.rb +135 -0
- data/lib/dommy/shadow_root.rb +255 -0
- data/lib/dommy/storage.rb +112 -0
- data/lib/dommy/test_helpers.rb +78 -0
- data/lib/dommy/tree_walker.rb +425 -0
- data/lib/dommy/url.rb +479 -0
- data/lib/dommy/version.rb +5 -0
- data/lib/dommy/world.rb +209 -0
- data/lib/dommy.rb +119 -0
- metadata +110 -0
|
@@ -0,0 +1,674 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
require_relative "internal/node_wrapper_cache"
|
|
6
|
+
require_relative "internal/mutation_coordinator"
|
|
7
|
+
require_relative "internal/shadow_root_registry"
|
|
8
|
+
require_relative "internal/cookie_jar"
|
|
9
|
+
require_relative "internal/node_traversal"
|
|
10
|
+
require_relative "internal/observer_manager"
|
|
11
|
+
require_relative "internal/template_content_registry"
|
|
12
|
+
|
|
13
|
+
module Dommy
|
|
14
|
+
# Stub DocumentType (`<!doctype html>`) — exposes `name` and `nodeType=10`.
|
|
15
|
+
# Real browsers also expose `publicId` / `systemId` which we leave empty
|
|
16
|
+
# since HTML5 doctypes don't carry those.
|
|
17
|
+
class DocumentType
|
|
18
|
+
include Node
|
|
19
|
+
|
|
20
|
+
attr_reader :name
|
|
21
|
+
|
|
22
|
+
def initialize(name)
|
|
23
|
+
@name = name.to_s
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def __js_get__(key)
|
|
27
|
+
case key
|
|
28
|
+
when "name"
|
|
29
|
+
@name
|
|
30
|
+
when "nodeType"
|
|
31
|
+
10
|
|
32
|
+
when "publicId"
|
|
33
|
+
""
|
|
34
|
+
when "systemId"
|
|
35
|
+
""
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# `document` — the entry point for DOM construction and querying.
|
|
41
|
+
# Wrapper caching keeps DOM identity stable across repeated
|
|
42
|
+
# traversals (`body.children[0].parentElement`).
|
|
43
|
+
class Document
|
|
44
|
+
include EventTarget
|
|
45
|
+
include Node
|
|
46
|
+
|
|
47
|
+
attr_reader :body, :nokogiri_doc
|
|
48
|
+
attr_accessor :default_view
|
|
49
|
+
|
|
50
|
+
def initialize(host = nil, nokogiri_doc: nil, default_view: nil)
|
|
51
|
+
@host = host
|
|
52
|
+
@default_view = default_view
|
|
53
|
+
@node_wrapper_cache = Internal::NodeWrapperCache.new(self)
|
|
54
|
+
@observer_manager = Internal::ObserverManager.new
|
|
55
|
+
@shadow_registry = Internal::ShadowRootRegistry.new
|
|
56
|
+
@cookie_jar = Internal::CookieJar.new
|
|
57
|
+
@template_content_registry = Internal::TemplateContentRegistry.new(self)
|
|
58
|
+
@mutation_coordinator = Internal::MutationCoordinator.new(self, @observer_manager)
|
|
59
|
+
@nokogiri_doc = nokogiri_doc || Nokogiri::HTML5("<!doctype html><html><head></head><body></body></html>")
|
|
60
|
+
body_node = @nokogiri_doc.at_css("body")
|
|
61
|
+
@body = wrap_node(body_node) if body_node
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# ----- Public Ruby API (snake_case) -----
|
|
65
|
+
|
|
66
|
+
def title
|
|
67
|
+
read_title
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def title=(value)
|
|
71
|
+
write_title(value.to_s)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def document_element
|
|
75
|
+
wrap_node(@nokogiri_doc.at_css("html"))
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def head
|
|
79
|
+
wrap_node(@nokogiri_doc.at_css("head"))
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# `document.URL` / `documentURI` — both return location.href in
|
|
83
|
+
# real browsers (legacy aliases of the same field).
|
|
84
|
+
def url
|
|
85
|
+
view = @default_view
|
|
86
|
+
view&.location ? view.location.href : ""
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
alias document_uri url
|
|
90
|
+
|
|
91
|
+
# `document.baseURI` — resolves the first `<base href>` (if any)
|
|
92
|
+
# relative to the document URL; otherwise just the document URL.
|
|
93
|
+
# When `<base href>` is itself absolute, that wins. Browsers also
|
|
94
|
+
# ignore subsequent <base> elements; we mirror that.
|
|
95
|
+
def base_uri
|
|
96
|
+
doc_url = url
|
|
97
|
+
base_el = @nokogiri_doc.at_css("base[href]")
|
|
98
|
+
return doc_url unless base_el
|
|
99
|
+
|
|
100
|
+
href = base_el["href"].to_s
|
|
101
|
+
return doc_url if href.empty?
|
|
102
|
+
|
|
103
|
+
begin
|
|
104
|
+
URI.join(doc_url.to_s.empty? ? "about:blank" : doc_url, href).to_s
|
|
105
|
+
rescue URI::InvalidURIError
|
|
106
|
+
doc_url
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# `document.domain` — host portion of the URL. Real browsers
|
|
111
|
+
# restrict cross-origin reads of this; we just return the bare host.
|
|
112
|
+
def domain
|
|
113
|
+
view = @default_view
|
|
114
|
+
return "" unless view&.location
|
|
115
|
+
|
|
116
|
+
view.location.__js_get__("hostname").to_s
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# `document.referrer` — Dommy never has a referring page, so this
|
|
120
|
+
# is always empty.
|
|
121
|
+
def referrer
|
|
122
|
+
""
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Live HTMLCollection helpers — each call re-queries the
|
|
126
|
+
# document so post-mutation reads reflect the current state.
|
|
127
|
+
def links
|
|
128
|
+
HTMLCollection.new do
|
|
129
|
+
@nokogiri_doc.css("a[href], area[href]").map { |n| wrap_node(n) }.compact
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def forms
|
|
134
|
+
HTMLCollection.new do
|
|
135
|
+
@nokogiri_doc.css("form").map { |n| wrap_node(n) }.compact
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def scripts
|
|
140
|
+
HTMLCollection.new do
|
|
141
|
+
@nokogiri_doc.css("script").map { |n| wrap_node(n) }.compact
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def images
|
|
146
|
+
HTMLCollection.new do
|
|
147
|
+
@nokogiri_doc.css("img").map { |n| wrap_node(n) }.compact
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# ParentNode mixin (operates on the document's element children —
|
|
152
|
+
# in practice the `<html>` root).
|
|
153
|
+
def children
|
|
154
|
+
HTMLCollection.new do
|
|
155
|
+
root = @nokogiri_doc.root
|
|
156
|
+
root ? [wrap_node(root)].compact : []
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def child_element_count
|
|
161
|
+
children.size
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def first_element_child
|
|
165
|
+
wrap_node(@nokogiri_doc.root)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def last_element_child
|
|
169
|
+
wrap_node(@nokogiri_doc.root)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Currently-focused element (or body if none). Updated via
|
|
173
|
+
# `el.focus()` / `el.blur()`.
|
|
174
|
+
def active_element
|
|
175
|
+
@active_element || @body
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def __set_active_element__(el)
|
|
179
|
+
@active_element = el
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Create a detached Attr. `setAttributeNode` attaches it to an
|
|
183
|
+
# element. Per spec, name must match the XML Name production —
|
|
184
|
+
# invalid names throw InvalidCharacterError.
|
|
185
|
+
def create_attribute(name)
|
|
186
|
+
@node_wrapper_cache.create_attribute(name)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def create_attribute_ns(namespace_uri, qualified_name)
|
|
190
|
+
@node_wrapper_cache.create_attribute_ns(namespace_uri, qualified_name)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# `document.createTreeWalker(root, whatToShow?, filter?)` — stateful
|
|
194
|
+
# tree traversal with sibling/parent navigation. `filter` may be a
|
|
195
|
+
# Ruby Proc, a JS-bridge callable, or an object with
|
|
196
|
+
# `accept_node` / `acceptNode`.
|
|
197
|
+
def create_tree_walker(root, what_to_show = NodeFilter::SHOW_ALL, filter = nil)
|
|
198
|
+
TreeWalker.new(root, what_to_show, filter)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Copy a node from another document into this one. The returned
|
|
202
|
+
# wrapper is owned by `this`. Per spec, the source node is left
|
|
203
|
+
# in place. `deep: true` copies the entire subtree.
|
|
204
|
+
def import_node(node, deep = false)
|
|
205
|
+
return nil unless node.respond_to?(:__node__)
|
|
206
|
+
|
|
207
|
+
copy = clone_into_doc(node.__node__, deep)
|
|
208
|
+
wrap_node(copy)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Move a node from another document into this one. The source
|
|
212
|
+
# node is detached from its previous owner and its ownerDocument
|
|
213
|
+
# becomes this. Returns the (possibly re-wrapped) node.
|
|
214
|
+
def adopt_node(node)
|
|
215
|
+
return nil unless node.respond_to?(:__node__)
|
|
216
|
+
|
|
217
|
+
src = node.__node__
|
|
218
|
+
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
|
+
|
|
225
|
+
wrap_node(moved)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Legacy `document.createEvent("EventName")` factory. Returns an
|
|
229
|
+
# Event subclass instance whose init still has to be called
|
|
230
|
+
# (`event.initEvent(type, bubbles, cancelable)`). Matches the
|
|
231
|
+
# mapping happy-dom and linkedom use.
|
|
232
|
+
def create_event(type_name)
|
|
233
|
+
name = type_name.to_s
|
|
234
|
+
case name
|
|
235
|
+
when "Event", "Events", "HTMLEvents"
|
|
236
|
+
Event.new("")
|
|
237
|
+
when "CustomEvent"
|
|
238
|
+
CustomEvent.new("")
|
|
239
|
+
when "MouseEvent", "MouseEvents"
|
|
240
|
+
MouseEvent.new("")
|
|
241
|
+
when "KeyboardEvent", "KeyboardEvents"
|
|
242
|
+
KeyboardEvent.new("")
|
|
243
|
+
else
|
|
244
|
+
Event.new("")
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Stubs for layout / focus / selection / execCommand APIs that
|
|
249
|
+
# don't apply to a layout-less DOM. They exist so callers don't
|
|
250
|
+
# hit NoMethodError; semantics are documented as no-op.
|
|
251
|
+
|
|
252
|
+
def has_focus?
|
|
253
|
+
true
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
alias has_focus has_focus?
|
|
257
|
+
|
|
258
|
+
def get_selection
|
|
259
|
+
nil
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def element_from_point(_x, _y)
|
|
263
|
+
nil
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def query_command_supported(_command)
|
|
267
|
+
false
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# `document.createNodeIterator(root, whatToShow?, filter?)` —
|
|
271
|
+
# flat depth-first iteration.
|
|
272
|
+
def create_node_iterator(root, what_to_show = NodeFilter::SHOW_ALL, filter = nil)
|
|
273
|
+
NodeIterator.new(root, what_to_show, filter)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Minimal DocumentType — represents the `<!doctype html>` line.
|
|
277
|
+
# Always present in HTML5 documents we parse, so we synthesize a
|
|
278
|
+
# stub object whose only useful field is `name`. Tests just need
|
|
279
|
+
# `nodeType == 10`.
|
|
280
|
+
def doctype
|
|
281
|
+
@doctype ||= DocumentType.new("html")
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# Delegate to CookieJar
|
|
285
|
+
|
|
286
|
+
def cookie
|
|
287
|
+
@cookie_jar.to_cookie_string
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def cookie=(value)
|
|
291
|
+
@cookie_jar.set_cookie(value)
|
|
292
|
+
nil
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def create_element_ns(namespace_uri, qualified_name)
|
|
296
|
+
@node_wrapper_cache.create_element_ns(namespace_uri, qualified_name)
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def get_elements_by_tag_name(name)
|
|
300
|
+
@node_wrapper_cache.get_elements_by_tag_name(name)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def get_elements_by_name(name)
|
|
304
|
+
@node_wrapper_cache.get_elements_by_name(name)
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# `document.write(html)` — legacy API. Appends parsed nodes to the
|
|
308
|
+
# body. Real browsers only re-stream the DOM during initial parse;
|
|
309
|
+
# this stub is enough for tests that fire write() during teardown.
|
|
310
|
+
def write(*args)
|
|
311
|
+
html = args.join
|
|
312
|
+
fragment = Parser.fragment(html, owner_doc: @nokogiri_doc)
|
|
313
|
+
removed = []
|
|
314
|
+
added = fragment.children.to_a
|
|
315
|
+
added.each { |node| @body.__node__.add_child(node) }
|
|
316
|
+
notify_child_list_mutation(target_node: @body.__node__, added_nodes: added, removed_nodes: removed)
|
|
317
|
+
nil
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# No-ops — real browsers reset the DOM on `open()` and flush
|
|
321
|
+
# pending writes on `close()`. We don't model the parse pipeline.
|
|
322
|
+
def open
|
|
323
|
+
nil
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def close
|
|
327
|
+
nil
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def [](key)
|
|
331
|
+
__js_get__(key.to_s)
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def []=(key, value)
|
|
335
|
+
__js_set__(key.to_s, value)
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# Create a Comment node. Wraps the Nokogiri comment so it flows
|
|
339
|
+
# through the same wrap_node identity machinery as Element / TextNode.
|
|
340
|
+
def create_comment(text)
|
|
341
|
+
@node_wrapper_cache.create_comment(text)
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def create_document_fragment
|
|
345
|
+
@node_wrapper_cache.create_document_fragment
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def get_elements_by_class_name(name)
|
|
349
|
+
@node_wrapper_cache.get_elements_by_class_name(name)
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def __js_get__(key)
|
|
353
|
+
case key
|
|
354
|
+
when "body"
|
|
355
|
+
@body
|
|
356
|
+
when "head"
|
|
357
|
+
head
|
|
358
|
+
when "doctype"
|
|
359
|
+
doctype
|
|
360
|
+
when "defaultView"
|
|
361
|
+
@default_view
|
|
362
|
+
when "documentElement"
|
|
363
|
+
wrap_node(@nokogiri_doc.at_css("html"))
|
|
364
|
+
when "title"
|
|
365
|
+
read_title
|
|
366
|
+
when "cookie"
|
|
367
|
+
cookie
|
|
368
|
+
when "nodeType"
|
|
369
|
+
9
|
|
370
|
+
when "activeElement"
|
|
371
|
+
active_element
|
|
372
|
+
when "URL", "documentURI"
|
|
373
|
+
url
|
|
374
|
+
when "baseURI"
|
|
375
|
+
base_uri
|
|
376
|
+
when "domain"
|
|
377
|
+
domain
|
|
378
|
+
when "referrer"
|
|
379
|
+
referrer
|
|
380
|
+
when "links"
|
|
381
|
+
links
|
|
382
|
+
when "forms"
|
|
383
|
+
forms
|
|
384
|
+
when "scripts"
|
|
385
|
+
scripts
|
|
386
|
+
when "images"
|
|
387
|
+
images
|
|
388
|
+
when "children"
|
|
389
|
+
children
|
|
390
|
+
when "childElementCount"
|
|
391
|
+
child_element_count
|
|
392
|
+
when "firstElementChild"
|
|
393
|
+
first_element_child
|
|
394
|
+
when "lastElementChild"
|
|
395
|
+
last_element_child
|
|
396
|
+
when "nodeName"
|
|
397
|
+
"#document"
|
|
398
|
+
else
|
|
399
|
+
nil
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def __js_set__(key, value)
|
|
404
|
+
case key
|
|
405
|
+
when "title"
|
|
406
|
+
write_title(value.to_s)
|
|
407
|
+
when "cookie"
|
|
408
|
+
self.cookie = value.to_s
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
nil
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def __js_call__(method, args)
|
|
415
|
+
case method
|
|
416
|
+
when "createElement"
|
|
417
|
+
create_element(args[0])
|
|
418
|
+
when "createElementNS"
|
|
419
|
+
create_element_ns(args[0], args[1])
|
|
420
|
+
when "createTextNode"
|
|
421
|
+
create_text_node(args[0])
|
|
422
|
+
when "createComment"
|
|
423
|
+
create_comment(args[0])
|
|
424
|
+
when "createDocumentFragment"
|
|
425
|
+
create_document_fragment
|
|
426
|
+
when "querySelector"
|
|
427
|
+
query_selector(args[0])
|
|
428
|
+
when "querySelectorAll"
|
|
429
|
+
query_selector_all(args[0])
|
|
430
|
+
when "getElementById"
|
|
431
|
+
get_element_by_id(args[0])
|
|
432
|
+
when "getElementsByClassName"
|
|
433
|
+
get_elements_by_class_name(args[0])
|
|
434
|
+
when "getElementsByTagName"
|
|
435
|
+
get_elements_by_tag_name(args[0])
|
|
436
|
+
when "getElementsByName"
|
|
437
|
+
get_elements_by_name(args[0])
|
|
438
|
+
when "createAttribute"
|
|
439
|
+
create_attribute(args[0])
|
|
440
|
+
when "createAttributeNS"
|
|
441
|
+
create_attribute_ns(args[0], args[1])
|
|
442
|
+
when "createTreeWalker"
|
|
443
|
+
create_tree_walker(args[0], args[1] || NodeFilter::SHOW_ALL, args[2])
|
|
444
|
+
when "createNodeIterator"
|
|
445
|
+
create_node_iterator(args[0], args[1] || NodeFilter::SHOW_ALL, args[2])
|
|
446
|
+
when "createEvent"
|
|
447
|
+
create_event(args[0])
|
|
448
|
+
when "importNode"
|
|
449
|
+
import_node(args[0], args[1])
|
|
450
|
+
when "adoptNode"
|
|
451
|
+
adopt_node(args[0])
|
|
452
|
+
when "hasFocus"
|
|
453
|
+
has_focus?
|
|
454
|
+
when "getSelection"
|
|
455
|
+
get_selection
|
|
456
|
+
when "elementFromPoint"
|
|
457
|
+
element_from_point(args[0], args[1])
|
|
458
|
+
when "queryCommandSupported"
|
|
459
|
+
query_command_supported(args[0])
|
|
460
|
+
when "addEventListener"
|
|
461
|
+
add_event_listener(args[0], args[1], args[2])
|
|
462
|
+
when "removeEventListener"
|
|
463
|
+
remove_event_listener(args[0], args[1])
|
|
464
|
+
when "dispatchEvent"
|
|
465
|
+
dispatch_event(args[0])
|
|
466
|
+
when "write"
|
|
467
|
+
write(*args)
|
|
468
|
+
when "open"
|
|
469
|
+
open
|
|
470
|
+
when "close"
|
|
471
|
+
close
|
|
472
|
+
else
|
|
473
|
+
nil
|
|
474
|
+
end
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
def __event_parent__
|
|
478
|
+
@default_view
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
# Delegate node wrapping to NodeWrapperCache
|
|
482
|
+
def wrap_node(node)
|
|
483
|
+
@node_wrapper_cache.wrap(node)
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
# Clear the cached wrapper so the next `wrap_node` creates a new
|
|
487
|
+
# one. Used by `customElements.define` to upgrade nodes that were
|
|
488
|
+
# constructed before the registration landed.
|
|
489
|
+
def __reset_wrapper__(nokogiri_node)
|
|
490
|
+
@node_wrapper_cache.reset_wrapper(nokogiri_node)
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
# ShadowRoot identity registry: map a Nokogiri DocumentFragment
|
|
494
|
+
# (the shadow tree's backing node) to the wrapping ShadowRoot so
|
|
495
|
+
# slot assignment and event composition can walk from any inner
|
|
496
|
+
# node back to its shadow boundary.
|
|
497
|
+
# Delegate to ShadowRootRegistry
|
|
498
|
+
|
|
499
|
+
def __register_shadow_fragment__(fragment_node, shadow_root)
|
|
500
|
+
@shadow_registry.register(fragment_node, shadow_root)
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
def __shadow_root_for_fragment__(fragment_node)
|
|
504
|
+
@shadow_registry.find_for_fragment(fragment_node)
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
def __shadow_root_containing__(node)
|
|
508
|
+
@shadow_registry.find_enclosing(node)
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
# Lifecycle callback dispatchers. Errors raised inside user
|
|
512
|
+
# callbacks are swallowed so a single buggy custom element can't
|
|
513
|
+
# break the whole mutation pipeline.
|
|
514
|
+
# Delegate to MutationCoordinator
|
|
515
|
+
|
|
516
|
+
def __notify_connected__(element)
|
|
517
|
+
@mutation_coordinator.notify_connected(element)
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
def __notify_disconnected__(element)
|
|
521
|
+
@mutation_coordinator.notify_disconnected(element)
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
def __notify_connected_subtree__(nk)
|
|
525
|
+
@mutation_coordinator.notify_connected_subtree(nk)
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
def __notify_disconnected_subtree__(nk)
|
|
529
|
+
@mutation_coordinator.notify_disconnected_subtree(nk)
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
def __notify_attribute_changed__(element, name, old_value, new_value)
|
|
533
|
+
@mutation_coordinator.notify_attribute_changed(element, name, old_value, new_value)
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
def register_observer(observer)
|
|
537
|
+
@mutation_coordinator.register_observer(observer)
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
def unregister_observer(observer)
|
|
541
|
+
@mutation_coordinator.unregister_observer(observer)
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
def notify_child_list_mutation(
|
|
545
|
+
target_node:,
|
|
546
|
+
added_nodes:,
|
|
547
|
+
removed_nodes:,
|
|
548
|
+
previous_sibling: nil,
|
|
549
|
+
next_sibling: nil
|
|
550
|
+
)
|
|
551
|
+
@mutation_coordinator.notify_child_list_mutation(
|
|
552
|
+
target_node: target_node,
|
|
553
|
+
added_nodes: added_nodes,
|
|
554
|
+
removed_nodes: removed_nodes,
|
|
555
|
+
previous_sibling: previous_sibling,
|
|
556
|
+
next_sibling: next_sibling
|
|
557
|
+
)
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
def notify_attribute_mutation(target_node:, attribute_name:, old_value:)
|
|
561
|
+
@mutation_coordinator.notify_attribute_mutation(
|
|
562
|
+
target_node: target_node,
|
|
563
|
+
attribute_name: attribute_name,
|
|
564
|
+
old_value: old_value
|
|
565
|
+
)
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
def notify_character_data_mutation(target_node:, old_value:)
|
|
569
|
+
@mutation_coordinator.notify_character_data_mutation(
|
|
570
|
+
target_node: target_node,
|
|
571
|
+
old_value: old_value
|
|
572
|
+
)
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
# Spec-permitted name pattern (XML "Name" production restricted to
|
|
576
|
+
# ASCII for practicality). Used by `createElement` and
|
|
577
|
+
# `createAttribute` to validate the argument.
|
|
578
|
+
NAME_RE = /\A[A-Za-z_][\w\-.:]*\z/.freeze
|
|
579
|
+
|
|
580
|
+
# Delegate factory methods to NodeWrapperCache
|
|
581
|
+
|
|
582
|
+
def create_element(name)
|
|
583
|
+
@node_wrapper_cache.create_element(name)
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
def create_text_node(text)
|
|
587
|
+
@node_wrapper_cache.create_text_node(text)
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
def query_selector(selector)
|
|
591
|
+
@node_wrapper_cache.query_selector(selector)
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
def query_selector_all(selector)
|
|
595
|
+
@node_wrapper_cache.query_selector_all(selector)
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
def get_element_by_id(id)
|
|
599
|
+
@node_wrapper_cache.get_element_by_id(id)
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
# ----- template content helpers (called from Element) -----
|
|
603
|
+
|
|
604
|
+
def attach_template_content(template_element, html)
|
|
605
|
+
@template_content_registry.attach(template_element, html)
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
def template_content_fragment(template_element)
|
|
609
|
+
@template_content_registry.fragment_for(template_element)
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
def template_content_inner_html(template_element)
|
|
613
|
+
@template_content_registry.inner_html_of(template_element)
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
def migrate_template_descendants(root)
|
|
617
|
+
@template_content_registry.migrate_descendants(root)
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
def has_template_content?(nokogiri_node)
|
|
621
|
+
@template_content_registry.has_content?(nokogiri_node)
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
private
|
|
625
|
+
|
|
626
|
+
# Build a Nokogiri copy of the given node inside our @nokogiri_doc.
|
|
627
|
+
# `deep: true` recurses into children. Used by importNode and
|
|
628
|
+
# adoptNode for cross-document transfer.
|
|
629
|
+
def clone_into_doc(source, deep)
|
|
630
|
+
copy = if source.element?
|
|
631
|
+
new_el = Nokogiri::XML::Node.new(source.name, @nokogiri_doc)
|
|
632
|
+
source.attribute_nodes.each { |a| new_el[a.name] = a.value }
|
|
633
|
+
new_el
|
|
634
|
+
elsif source.text?
|
|
635
|
+
Nokogiri::XML::Text.new(source.content, @nokogiri_doc)
|
|
636
|
+
elsif source.is_a?(Nokogiri::XML::Comment)
|
|
637
|
+
Nokogiri::XML::Comment.new(@nokogiri_doc, source.content)
|
|
638
|
+
else
|
|
639
|
+
# Fallback: serialize + reparse via fragment for unusual types.
|
|
640
|
+
fragment = Parser.fragment(source.to_html, owner_doc: @nokogiri_doc)
|
|
641
|
+
fragment.children.first || Nokogiri::XML::Text.new("", @nokogiri_doc)
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
if deep && source.respond_to?(:children)
|
|
645
|
+
source.children.each do |child|
|
|
646
|
+
copy.add_child(clone_into_doc(child, true))
|
|
647
|
+
end
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
copy
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
def read_title
|
|
654
|
+
head = @nokogiri_doc.at_css("head")
|
|
655
|
+
title = head&.at_css("title")
|
|
656
|
+
title ? title.text : ""
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
def write_title(value)
|
|
660
|
+
head = @nokogiri_doc.at_css("head")
|
|
661
|
+
return unless head
|
|
662
|
+
|
|
663
|
+
title = head.at_css("title")
|
|
664
|
+
unless title
|
|
665
|
+
title = Nokogiri::XML::Node.new("title", @nokogiri_doc)
|
|
666
|
+
head.add_child(title)
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
title.children.each(&:unlink)
|
|
670
|
+
title.add_child(Nokogiri::XML::Text.new(value, @nokogiri_doc))
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
end
|
|
674
|
+
end
|