dommy 0.9.0 → 0.10.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/lib/dommy/attr.rb +46 -8
- data/lib/dommy/backend/makiri_adapter.rb +34 -0
- data/lib/dommy/backend.rb +29 -0
- data/lib/dommy/blob.rb +48 -6
- data/lib/dommy/browser.rb +239 -36
- data/lib/dommy/callable_invoker.rb +6 -2
- data/lib/dommy/document.rb +525 -57
- data/lib/dommy/element.rb +479 -239
- data/lib/dommy/event.rb +296 -15
- data/lib/dommy/fetch.rb +431 -25
- data/lib/dommy/history.rb +24 -3
- data/lib/dommy/html_collection.rb +145 -21
- data/lib/dommy/html_elements.rb +1675 -266
- data/lib/dommy/interaction/driver.rb +155 -14
- data/lib/dommy/interaction/event_synthesis.rb +115 -7
- data/lib/dommy/interaction/field_interactor.rb +60 -0
- data/lib/dommy/internal/child_node.rb +199 -0
- data/lib/dommy/internal/css/cascade.rb +44 -2
- data/lib/dommy/internal/global_functions.rb +23 -10
- data/lib/dommy/internal/mutation_coordinator.rb +27 -7
- data/lib/dommy/internal/node_wrapper_cache.rb +56 -14
- data/lib/dommy/internal/observer_manager.rb +6 -0
- data/lib/dommy/internal/observer_matcher.rb +6 -8
- data/lib/dommy/internal/parent_node.rb +37 -73
- data/lib/dommy/internal/selector_matcher.rb +89 -0
- data/lib/dommy/internal/selector_parser.rb +44 -7
- data/lib/dommy/js/custom_element_bridge.rb +13 -2
- data/lib/dommy/js/dom_interfaces.rb +19 -4
- data/lib/dommy/js/host_bridge.rb +21 -0
- data/lib/dommy/js/host_runtime.js +926 -64
- data/lib/dommy/js/script_boot.rb +80 -0
- data/lib/dommy/location.rb +76 -13
- data/lib/dommy/mutation_observer.rb +3 -4
- data/lib/dommy/navigation.rb +263 -0
- data/lib/dommy/node.rb +180 -27
- data/lib/dommy/range.rb +15 -3
- data/lib/dommy/scheduler.rb +16 -0
- data/lib/dommy/shadow_root.rb +33 -0
- data/lib/dommy/storage.rb +61 -10
- data/lib/dommy/tree_walker.rb +18 -36
- data/lib/dommy/url.rb +4 -2
- data/lib/dommy/version.rb +1 -1
- data/lib/dommy/web_socket.rb +45 -2
- data/lib/dommy/window.rb +126 -7
- data/lib/dommy/xml_http_request.rb +30 -4
- data/lib/dommy.rb +2 -0
- metadata +6 -10
data/lib/dommy/document.rb
CHANGED
|
@@ -14,20 +14,76 @@ module Dommy
|
|
|
14
14
|
# DocumentType (`<!doctype html>`) — exposes name / publicId / systemId and
|
|
15
15
|
# nodeType=10. HTML5 doctypes carry empty public/system IDs, but
|
|
16
16
|
# `implementation.createDocumentType` can set them.
|
|
17
|
+
#
|
|
18
|
+
# Two modes:
|
|
19
|
+
# * node-backed — wraps the Makiri DocumentType node of a parsed document
|
|
20
|
+
# (`document.doctype`). Participates in the tree machinery
|
|
21
|
+
# (compareDocumentPosition / getRootNode / sibling links) like any other
|
|
22
|
+
# backend-backed node, via the shared Node mixin.
|
|
23
|
+
# * synthetic — a standalone doctype (`implementation.createDocumentType`)
|
|
24
|
+
# carrying just name/public/system id and an owner document. No backend node,
|
|
25
|
+
# so it stays tree-DISCONNECTED per its detached nature.
|
|
17
26
|
class DocumentType
|
|
18
27
|
include Node
|
|
19
28
|
|
|
20
|
-
|
|
29
|
+
# Mixed into a node-backed doctype only, so a synthetic one does NOT respond
|
|
30
|
+
# to `__dommy_backend_node__` — leaving the Node mixin's guards (which key off
|
|
31
|
+
# `respond_to?(:__dommy_backend_node__)`) to treat it as disconnected.
|
|
32
|
+
module NodeBacked
|
|
33
|
+
def __dommy_backend_node__ = @__node__
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# `owner_document:` links a synthetic doctype to its document so the ChildNode
|
|
37
|
+
# methods can act on the tree; a standalone one has none, so those methods are
|
|
38
|
+
# no-ops per spec. `backend_node:` + `document:` build the node-backed variant
|
|
39
|
+
# (a parsed-tree doctype or the createDocumentType factory node), which reads
|
|
40
|
+
# name/publicId/systemId straight off the node (the factory preserves case).
|
|
41
|
+
def initialize(name = "", public_id = "", system_id = "", owner_document: nil, backend_node: nil, document: nil)
|
|
42
|
+
@__node__ = backend_node
|
|
43
|
+
if backend_node
|
|
44
|
+
@document = document
|
|
45
|
+
@owner_document = document
|
|
46
|
+
extend(NodeBacked)
|
|
47
|
+
else
|
|
48
|
+
@name = name.to_s
|
|
49
|
+
@public_id = public_id.to_s
|
|
50
|
+
@system_id = system_id.to_s
|
|
51
|
+
@owner_document = owner_document
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# The document this doctype currently belongs to (nil for a detached
|
|
56
|
+
# synthetic doctype). Lets a cross-document appendChild/insert detect that
|
|
57
|
+
# the node must be adopted (re-created) into the destination backend.
|
|
58
|
+
def document
|
|
59
|
+
@document || @owner_document
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def name
|
|
63
|
+
@__node__ ? @__node__.name : @name
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Makiri reports nil for an absent public/system id; DOM exposes "".
|
|
67
|
+
def public_id
|
|
68
|
+
@__node__ ? @__node__.public_id.to_s : @public_id
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def system_id
|
|
72
|
+
@__node__ ? @__node__.system_id.to_s : @system_id
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def parent_node
|
|
76
|
+
# wrap_node maps the backend document node (the doctype's parent) to the
|
|
77
|
+
# Dommy Document.
|
|
78
|
+
@__node__ && @__node__.parent && @document.wrap_node(@__node__.parent)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def next_sibling
|
|
82
|
+
@__node__ && @__node__.next && @document.wrap_node(@__node__.next)
|
|
83
|
+
end
|
|
21
84
|
|
|
22
|
-
|
|
23
|
-
|
|
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)
|
|
27
|
-
@name = name.to_s
|
|
28
|
-
@public_id = public_id.to_s
|
|
29
|
-
@system_id = system_id.to_s
|
|
30
|
-
@owner_document = owner_document
|
|
85
|
+
def previous_sibling
|
|
86
|
+
@__node__ && @__node__.previous && @document.wrap_node(@__node__.previous)
|
|
31
87
|
end
|
|
32
88
|
|
|
33
89
|
# ChildNode mixin — the doctype's parent is the document.
|
|
@@ -61,33 +117,70 @@ module Dommy
|
|
|
61
117
|
def __js_get__(key)
|
|
62
118
|
case key
|
|
63
119
|
when "name"
|
|
64
|
-
|
|
120
|
+
name
|
|
65
121
|
when "nodeName"
|
|
66
122
|
# WHATWG: a DocumentType's nodeName is its name.
|
|
67
|
-
|
|
123
|
+
name
|
|
68
124
|
when "nodeType"
|
|
69
125
|
10
|
|
70
126
|
when "publicId"
|
|
71
|
-
|
|
127
|
+
public_id
|
|
72
128
|
when "systemId"
|
|
73
|
-
|
|
129
|
+
system_id
|
|
74
130
|
when "ownerDocument"
|
|
75
131
|
@owner_document
|
|
132
|
+
when "parentNode"
|
|
133
|
+
parent_node
|
|
134
|
+
when "parentElement"
|
|
135
|
+
nil
|
|
136
|
+
when "nextSibling"
|
|
137
|
+
next_sibling
|
|
138
|
+
when "previousSibling"
|
|
139
|
+
previous_sibling
|
|
140
|
+
when "childNodes"
|
|
141
|
+
NodeList.new
|
|
142
|
+
when "firstChild", "lastChild"
|
|
143
|
+
nil
|
|
76
144
|
end
|
|
77
145
|
end
|
|
78
146
|
|
|
79
147
|
include EventTarget
|
|
80
148
|
|
|
81
149
|
def __internal_event_parent__
|
|
82
|
-
|
|
150
|
+
parent_node
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Node.cloneNode on a doctype: a detached copy with the same name/publicId/
|
|
154
|
+
# systemId (a doctype is a leaf, so `deep` is irrelevant). Node-backed when a
|
|
155
|
+
# backend factory is available, else synthetic — either reports the same
|
|
156
|
+
# values and isEqualNode-matches the original.
|
|
157
|
+
def clone_node(_deep = false)
|
|
158
|
+
if @__node__ && @document
|
|
159
|
+
node = begin
|
|
160
|
+
Backend.create_document_type(name, public_id, system_id, @document.backend_doc)
|
|
161
|
+
rescue StandardError
|
|
162
|
+
nil
|
|
163
|
+
end
|
|
164
|
+
return DocumentType.new(backend_node: node, document: @document) if node
|
|
165
|
+
end
|
|
166
|
+
DocumentType.new(name, public_id, system_id, owner_document: @owner_document)
|
|
83
167
|
end
|
|
84
168
|
|
|
85
169
|
include Bridge::Methods
|
|
86
170
|
js_methods %w[isEqualNode isSameNode getRootNode hasChildNodes normalize compareDocumentPosition contains
|
|
87
|
-
appendChild insertBefore removeChild replaceChild before after replaceWith remove
|
|
171
|
+
cloneNode appendChild insertBefore removeChild replaceChild before after replaceWith remove
|
|
172
|
+
lookupNamespaceURI lookupPrefix isDefaultNamespace
|
|
88
173
|
addEventListener removeEventListener dispatchEvent]
|
|
89
174
|
def __js_call__(method, args)
|
|
90
175
|
case method
|
|
176
|
+
when "lookupNamespaceURI"
|
|
177
|
+
lookup_namespace_uri(args[0])
|
|
178
|
+
when "lookupPrefix"
|
|
179
|
+
lookup_prefix(args[0])
|
|
180
|
+
when "isDefaultNamespace"
|
|
181
|
+
is_default_namespace(args[0])
|
|
182
|
+
when "cloneNode"
|
|
183
|
+
clone_node(args[0])
|
|
91
184
|
when "hasChildNodes"
|
|
92
185
|
false
|
|
93
186
|
when "contains"
|
|
@@ -101,11 +194,13 @@ module Dommy
|
|
|
101
194
|
get_root_node(args[0])
|
|
102
195
|
when "compareDocumentPosition"
|
|
103
196
|
compare_document_position(args[0])
|
|
104
|
-
when "appendChild", "insertBefore"
|
|
197
|
+
when "appendChild", "insertBefore", "replaceChild"
|
|
198
|
+
# Pre-insert / replace step 1 rejects a non-parent context node with
|
|
199
|
+
# HierarchyRequestError before the reference-child check.
|
|
105
200
|
raise Bridge::TypeError, "Argument is not a Node." unless args[0].is_a?(Dommy::Node)
|
|
106
201
|
|
|
107
202
|
raise DOMException::HierarchyRequestError, "a DocumentType may not have children"
|
|
108
|
-
when "removeChild"
|
|
203
|
+
when "removeChild"
|
|
109
204
|
raise Bridge::TypeError, "Argument is not a Node." unless args[0].is_a?(Dommy::Node)
|
|
110
205
|
|
|
111
206
|
raise DOMException::NotFoundError, "the node to be removed is not a child of this node"
|
|
@@ -117,6 +212,7 @@ module Dommy
|
|
|
117
212
|
replace_with(*args)
|
|
118
213
|
when "remove"
|
|
119
214
|
remove
|
|
215
|
+
Bridge::UNDEFINED # DocumentType (ChildNode)#remove is void -> JS undefined
|
|
120
216
|
when "normalize"
|
|
121
217
|
nil
|
|
122
218
|
when "addEventListener"
|
|
@@ -135,11 +231,24 @@ module Dommy
|
|
|
135
231
|
@document = document
|
|
136
232
|
end
|
|
137
233
|
|
|
138
|
-
# A created DocumentType's node document is the implementation's document.
|
|
139
|
-
#
|
|
140
|
-
# a
|
|
234
|
+
# A created DocumentType's node document is the implementation's document. When
|
|
235
|
+
# the backend ships a doctype factory (the HTML backend) and accepts the name,
|
|
236
|
+
# the result is a real, node-backed (but detached) DocumentType that can join
|
|
237
|
+
# the tree; otherwise it falls back to a synthetic one. (Qualified-name QName
|
|
238
|
+
# validation isn't enforced — createDocumentType is permissive, so the factory's
|
|
239
|
+
# stricter name check is bypassed via the synthetic fallback rather than
|
|
240
|
+
# raising; a couple of invalid-name WPT cases stay documented gaps.)
|
|
141
241
|
def create_document_type(qualified_name, public_id, system_id)
|
|
142
|
-
|
|
242
|
+
qn = qualified_name.to_s
|
|
243
|
+
pub = public_id.to_s
|
|
244
|
+
sys = system_id.to_s
|
|
245
|
+
node =
|
|
246
|
+
begin
|
|
247
|
+
Backend.create_document_type(qn, pub, sys, @document.backend_doc)
|
|
248
|
+
rescue ArgumentError
|
|
249
|
+
nil
|
|
250
|
+
end
|
|
251
|
+
node ? DocumentType.new(backend_node: node, document: @document) : DocumentType.new(qn, pub, sys, owner_document: @document)
|
|
143
252
|
end
|
|
144
253
|
|
|
145
254
|
# `hasFeature()` is a no-op that always returns true (DOM Standard).
|
|
@@ -147,11 +256,10 @@ module Dommy
|
|
|
147
256
|
true
|
|
148
257
|
end
|
|
149
258
|
|
|
150
|
-
# createDocument(namespace, qualifiedName, doctype?) — a fresh XML document
|
|
151
|
-
# with
|
|
152
|
-
#
|
|
153
|
-
|
|
154
|
-
def create_document(namespace, qualified_name, _doctype = nil)
|
|
259
|
+
# createDocument(namespace, qualifiedName, doctype?) — a fresh XML document
|
|
260
|
+
# with, in tree order, the doctype (when given) then a document element
|
|
261
|
+
# (namespace, qualifiedName) when qualifiedName is non-empty.
|
|
262
|
+
def create_document(namespace, qualified_name, doctype = nil)
|
|
155
263
|
doc = Document.new(nil, backend_doc: Backend.empty_xml_document)
|
|
156
264
|
# createDocument's content type is keyed off the namespace. None is
|
|
157
265
|
# "text/html", so tagName keeps its case; xhtml+xml still routes
|
|
@@ -168,9 +276,37 @@ module Dommy
|
|
|
168
276
|
el = doc.send(:create_element_ns, namespace, qualified_name)
|
|
169
277
|
Backend.set_document_root(doc.backend_doc, el.__dommy_backend_node__)
|
|
170
278
|
end
|
|
279
|
+
adopt_doctype_into(doc, doctype)
|
|
171
280
|
doc
|
|
172
281
|
end
|
|
173
282
|
|
|
283
|
+
private
|
|
284
|
+
|
|
285
|
+
# Place `doctype` (a DocumentType passed to createDocument) as `doc`'s first
|
|
286
|
+
# child. Makiri can't move a node between documents, so — like adoption — the
|
|
287
|
+
# doctype is re-created in `doc`'s backend from its name/publicId/systemId.
|
|
288
|
+
# No-op for nil/undefined, a non-DocumentType, a backend without an XML doctype
|
|
289
|
+
# factory, or a public/system id the backend rejects (createDocument itself
|
|
290
|
+
# doesn't validate those — only XML *serialization* would — so a rejection just
|
|
291
|
+
# leaves the doctype unplaced rather than throwing).
|
|
292
|
+
def adopt_doctype_into(doc, doctype)
|
|
293
|
+
return if doctype.nil? || doctype.equal?(Bridge::UNDEFINED)
|
|
294
|
+
return unless doctype.is_a?(DocumentType)
|
|
295
|
+
|
|
296
|
+
node =
|
|
297
|
+
begin
|
|
298
|
+
Backend.create_document_type(doctype.name, doctype.public_id, doctype.system_id, doc.backend_doc)
|
|
299
|
+
rescue StandardError
|
|
300
|
+
nil
|
|
301
|
+
end
|
|
302
|
+
return unless node
|
|
303
|
+
|
|
304
|
+
root = doc.backend_doc.root
|
|
305
|
+
root ? root.add_previous_sibling(node) : doc.backend_doc.add_child(node)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
public
|
|
309
|
+
|
|
174
310
|
# createHTMLDocument(title?) — a fresh HTML document (doctype + html > head,
|
|
175
311
|
# body), with an optional <title>.
|
|
176
312
|
def create_html_document(title = nil)
|
|
@@ -190,7 +326,13 @@ module Dommy
|
|
|
190
326
|
when "createDocument"
|
|
191
327
|
create_document(args[0], args[1], args[2])
|
|
192
328
|
when "createHTMLDocument"
|
|
193
|
-
|
|
329
|
+
# title is an OPTIONAL DOMString: a missing or undefined argument leaves
|
|
330
|
+
# the document title-less, but an explicit null coerces to "null".
|
|
331
|
+
if args.empty? || args[0].equal?(Bridge::UNDEFINED)
|
|
332
|
+
create_html_document
|
|
333
|
+
else
|
|
334
|
+
create_html_document(args[0].nil? ? "null" : args[0])
|
|
335
|
+
end
|
|
194
336
|
when "hasFeature"
|
|
195
337
|
has_feature
|
|
196
338
|
end
|
|
@@ -341,6 +483,10 @@ module Dommy
|
|
|
341
483
|
# public/system identifier) is no-quirks. (The full quirks algorithm keys off
|
|
342
484
|
# specific legacy public ids; this covers the common cases.)
|
|
343
485
|
def compat_mode
|
|
486
|
+
# Only HTML documents can be in quirks mode; an XML document
|
|
487
|
+
# (createDocument / DOMParser XML) is always no-quirks.
|
|
488
|
+
return "CSS1Compat" unless html_document?
|
|
489
|
+
|
|
344
490
|
dt = @backend_doc.internal_subset
|
|
345
491
|
return "BackCompat" unless dt
|
|
346
492
|
return "CSS1Compat" if dt.name.to_s.downcase == "html" && dt.external_id.nil?
|
|
@@ -364,7 +510,16 @@ module Dommy
|
|
|
364
510
|
end
|
|
365
511
|
|
|
366
512
|
def head
|
|
367
|
-
|
|
513
|
+
# The first `head` element child of the document element in the HTML
|
|
514
|
+
# namespace (readonly — assignment is a no-op, see __js_set__). Not just
|
|
515
|
+
# `at_css("head")`, which searches the whole tree and ignores namespace.
|
|
516
|
+
root = document_element
|
|
517
|
+
return nil unless root
|
|
518
|
+
|
|
519
|
+
root.child_nodes.to_a.find do |c|
|
|
520
|
+
c.respond_to?(:local_name) && c.local_name == "head" &&
|
|
521
|
+
c.respond_to?(:namespace_uri) && c.namespace_uri == "http://www.w3.org/1999/xhtml"
|
|
522
|
+
end
|
|
368
523
|
end
|
|
369
524
|
|
|
370
525
|
# Resolve `body` fresh from the tree (not memoized) so it tracks a swapped
|
|
@@ -404,10 +559,12 @@ module Dommy
|
|
|
404
559
|
end
|
|
405
560
|
|
|
406
561
|
# `document.URL` / `documentURI` — both return location.href in
|
|
407
|
-
# real browsers (legacy aliases of the same field).
|
|
562
|
+
# real browsers (legacy aliases of the same field). A document with no
|
|
563
|
+
# browsing context (createDocument / new Document / DOMParser) has the URL
|
|
564
|
+
# "about:blank", not the empty string.
|
|
408
565
|
def url
|
|
409
566
|
view = @default_view
|
|
410
|
-
view&.location ? view.location.href : ""
|
|
567
|
+
view&.location ? view.location.href : "about:blank"
|
|
411
568
|
end
|
|
412
569
|
|
|
413
570
|
alias document_uri url
|
|
@@ -524,8 +681,12 @@ module Dommy
|
|
|
524
681
|
return true if other.equal?(self)
|
|
525
682
|
return false unless other.respond_to?(:__dommy_backend_node__)
|
|
526
683
|
|
|
684
|
+
# Walk parents up to the backend document node. (The backend's #ancestors
|
|
685
|
+
# stops below the document, so it can't test document membership; the
|
|
686
|
+
# doctype in particular reports an empty ancestor list.)
|
|
527
687
|
node = other.__dommy_backend_node__
|
|
528
|
-
node.
|
|
688
|
+
node = node.parent while node && !node.equal?(@backend_doc)
|
|
689
|
+
!node.nil?
|
|
529
690
|
end
|
|
530
691
|
|
|
531
692
|
def __internal_set_active_element__(el)
|
|
@@ -608,6 +769,9 @@ module Dommy
|
|
|
608
769
|
def import_node(node, deep = false)
|
|
609
770
|
return nil unless node.respond_to?(:__dommy_backend_node__)
|
|
610
771
|
|
|
772
|
+
# WebIDL `optional boolean deep = false`: a missing / undefined argument
|
|
773
|
+
# is the default (false / shallow), not a truthy sentinel.
|
|
774
|
+
deep = false if deep.nil? || deep.equal?(Bridge::UNDEFINED)
|
|
611
775
|
copy = clone_into_doc(node.__dommy_backend_node__, deep)
|
|
612
776
|
wrap_node(copy)
|
|
613
777
|
end
|
|
@@ -616,6 +780,10 @@ module Dommy
|
|
|
616
780
|
# node is detached from its previous owner and its ownerDocument
|
|
617
781
|
# becomes this. Returns the (possibly re-wrapped) node.
|
|
618
782
|
def adopt_node(node)
|
|
783
|
+
# WHATWG adopt: a Document can't be adopted into another document.
|
|
784
|
+
if node.is_a?(Dommy::Document)
|
|
785
|
+
raise DOMException::NotSupportedError, "A Document node cannot be adopted."
|
|
786
|
+
end
|
|
619
787
|
return nil unless node.respond_to?(:__dommy_backend_node__)
|
|
620
788
|
|
|
621
789
|
src = node.__dommy_backend_node__
|
|
@@ -624,6 +792,27 @@ module Dommy
|
|
|
624
792
|
# Same document: just return the wrapper after the detach above.
|
|
625
793
|
return wrap_node(src) if src.document == @backend_doc
|
|
626
794
|
|
|
795
|
+
# Cross-document DocumentType: Makiri can't import a doctype node between
|
|
796
|
+
# arenas, so re-create it in this document's backend from its public
|
|
797
|
+
# name / publicId / systemId (as createDocument does), then reseat the
|
|
798
|
+
# caller's wrapper onto the new node so JS identity survives the move.
|
|
799
|
+
if node.is_a?(DocumentType)
|
|
800
|
+
adopted = begin
|
|
801
|
+
Backend.create_document_type(node.name, node.public_id, node.system_id, @backend_doc)
|
|
802
|
+
rescue StandardError
|
|
803
|
+
nil
|
|
804
|
+
end
|
|
805
|
+
return node unless adopted
|
|
806
|
+
|
|
807
|
+
src_doc_wrapper = node.instance_variable_get(:@document)
|
|
808
|
+
src_doc_wrapper.__internal_reset_wrapper__(src) if src_doc_wrapper.respond_to?(:__internal_reset_wrapper__)
|
|
809
|
+
node.instance_variable_set(:@document, self)
|
|
810
|
+
node.instance_variable_set(:@owner_document, self)
|
|
811
|
+
node.instance_variable_set(:@__node__, adopted)
|
|
812
|
+
@node_wrapper_cache.register(adopted, node)
|
|
813
|
+
return node
|
|
814
|
+
end
|
|
815
|
+
|
|
627
816
|
# Cross-document: hand the detached source to the backend, which
|
|
628
817
|
# returns the node now owned by this document — an imported copy for
|
|
629
818
|
# Makiri (a node can't move between arenas). Drop the stale source
|
|
@@ -638,9 +827,69 @@ module Dommy
|
|
|
638
827
|
node.instance_variable_set(:@document, self)
|
|
639
828
|
node.instance_variable_set(:@__node__, adopted)
|
|
640
829
|
@node_wrapper_cache.register(adopted, node)
|
|
830
|
+
|
|
831
|
+
# A deep adopt imports a fresh copy of the whole subtree, so any live
|
|
832
|
+
# descendant wrapper (held by page script, e.g. an aria element reference)
|
|
833
|
+
# must be reseated onto its corresponding copy — otherwise it stays bound to
|
|
834
|
+
# the old document. Import preserves document order, so walk both subtrees in
|
|
835
|
+
# lockstep.
|
|
836
|
+
reseat_descendant_wrappers(src, adopted, src_doc_wrapper)
|
|
641
837
|
node
|
|
642
838
|
end
|
|
643
839
|
|
|
840
|
+
# Move each live wrapper for a descendant of `src_root` onto the matching node
|
|
841
|
+
# in `dst_root` (the imported copy), pruning it from the source document.
|
|
842
|
+
def reseat_descendant_wrappers(src_root, dst_root, src_doc)
|
|
843
|
+
return unless src_doc.respond_to?(:__internal_peek_wrapper__)
|
|
844
|
+
|
|
845
|
+
src_nodes = collect_subtree_nodes(src_root)
|
|
846
|
+
dst_nodes = collect_subtree_nodes(dst_root)
|
|
847
|
+
return unless src_nodes.length == dst_nodes.length
|
|
848
|
+
|
|
849
|
+
src_nodes.zip(dst_nodes).each do |orig, copy|
|
|
850
|
+
next if orig.equal?(src_root) # the root wrapper is reseated by the caller
|
|
851
|
+
|
|
852
|
+
wrapper = src_doc.__internal_peek_wrapper__(orig)
|
|
853
|
+
next unless wrapper
|
|
854
|
+
|
|
855
|
+
src_doc.__internal_reset_wrapper__(orig)
|
|
856
|
+
wrapper.instance_variable_set(:@document, self)
|
|
857
|
+
wrapper.instance_variable_set(:@__node__, copy)
|
|
858
|
+
@node_wrapper_cache.register(copy, wrapper)
|
|
859
|
+
end
|
|
860
|
+
end
|
|
861
|
+
|
|
862
|
+
# HTML "cloning steps": a cloned node copies interface-specific live state
|
|
863
|
+
# that the content attributes don't capture — an input's dirty value and
|
|
864
|
+
# checkedness, a textarea's dirty value, etc. Dommy keeps that state on the
|
|
865
|
+
# Ruby wrapper (not the backend node), and a deep backend clone never calls
|
|
866
|
+
# the descendants' clone_node, so walk the original and cloned subtrees in
|
|
867
|
+
# lockstep and copy each live wrapper's cloning state onto its copy. `deep`
|
|
868
|
+
# false processes only the root (a shallow clone has no children).
|
|
869
|
+
def __internal_apply_cloning_steps__(src_root_bn, clone_root_bn, deep)
|
|
870
|
+
src_nodes = deep ? collect_subtree_nodes(src_root_bn) : [src_root_bn]
|
|
871
|
+
clone_nodes = deep ? collect_subtree_nodes(clone_root_bn) : [clone_root_bn]
|
|
872
|
+
return unless src_nodes.length == clone_nodes.length
|
|
873
|
+
|
|
874
|
+
src_nodes.zip(clone_nodes).each do |orig, copy|
|
|
875
|
+
wrapper = @node_wrapper_cache.peek(orig)
|
|
876
|
+
next unless wrapper.respond_to?(:__cloning_state__)
|
|
877
|
+
|
|
878
|
+
state = wrapper.__cloning_state__
|
|
879
|
+
next if state.nil?
|
|
880
|
+
|
|
881
|
+
@node_wrapper_cache.wrap(copy).__apply_cloning_state__(state)
|
|
882
|
+
end
|
|
883
|
+
end
|
|
884
|
+
|
|
885
|
+
# A subtree's nodes in document (depth-first) order — the order Backend.adopt
|
|
886
|
+
# preserves — so a source node and its imported copy line up by index.
|
|
887
|
+
def collect_subtree_nodes(root)
|
|
888
|
+
nodes = [root]
|
|
889
|
+
root.children.each { |child| nodes.concat(collect_subtree_nodes(child)) } if root.respond_to?(:children)
|
|
890
|
+
nodes
|
|
891
|
+
end
|
|
892
|
+
|
|
644
893
|
# Legacy `document.createEvent("EventName")` factory. Returns an
|
|
645
894
|
# Event subclass instance whose init still has to be called
|
|
646
895
|
# (`event.initEvent(type, bubbles, cancelable)`). Matches the
|
|
@@ -738,14 +987,13 @@ module Dommy
|
|
|
738
987
|
@node_iterators << iterator
|
|
739
988
|
end
|
|
740
989
|
|
|
741
|
-
#
|
|
742
|
-
#
|
|
743
|
-
#
|
|
744
|
-
# `
|
|
990
|
+
# `document.doctype` — the node-backed DocumentType wrapping the parsed
|
|
991
|
+
# `<!DOCTYPE …>` node, or nil when the document declares none (or the doctype
|
|
992
|
+
# was removed, which unlinks the backend node). Shares wrapper identity with
|
|
993
|
+
# the same node in `childNodes`, since both wrap the same backend node.
|
|
745
994
|
def doctype
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
@doctype ||= DocumentType.new("html", owner_document: self)
|
|
995
|
+
node = Backend.internal_subset(@backend_doc)
|
|
996
|
+
node ? wrap_node(node) : nil
|
|
749
997
|
end
|
|
750
998
|
|
|
751
999
|
def implementation
|
|
@@ -756,9 +1004,88 @@ module Dommy
|
|
|
756
1004
|
@node_wrapper_cache.create_processing_instruction(target, data)
|
|
757
1005
|
end
|
|
758
1006
|
|
|
1007
|
+
# WHATWG "ensure pre-insertion validity", step 6 — the Document-parent
|
|
1008
|
+
# constraints an element-like parent doesn't have. `args` is the set of nodes
|
|
1009
|
+
# (and DOMStrings) being inserted; per "converting nodes into a node" they're
|
|
1010
|
+
# summed as one insertion. `child_bn` is the backend reference child (nil for
|
|
1011
|
+
# append). `ignore_existing` (replaceChildren) drops the current children
|
|
1012
|
+
# from the counts, since replace-all removes them first. `exclude` (replace)
|
|
1013
|
+
# is a current child to disregard. Raises HierarchyRequestError on violation.
|
|
1014
|
+
def ensure_document_insertion_validity!(args, child_bn, ignore_existing: false, exclude: nil)
|
|
1015
|
+
elements = 0
|
|
1016
|
+
doctypes = 0
|
|
1017
|
+
has_text = false
|
|
1018
|
+
args.each do |arg|
|
|
1019
|
+
case arg
|
|
1020
|
+
when String
|
|
1021
|
+
has_text = true
|
|
1022
|
+
when Dommy::Fragment
|
|
1023
|
+
arg.child_nodes.each do |c|
|
|
1024
|
+
if c.is_a?(Dommy::Element) then elements += 1
|
|
1025
|
+
elsif c.is_a?(Dommy::DocumentType) then doctypes += 1
|
|
1026
|
+
elsif c.is_a?(Dommy::TextNode) then has_text = true
|
|
1027
|
+
end
|
|
1028
|
+
end
|
|
1029
|
+
when Dommy::DocumentType then doctypes += 1
|
|
1030
|
+
when Dommy::Element then elements += 1
|
|
1031
|
+
when Dommy::CharacterDataNode
|
|
1032
|
+
# Comment (8) / PI (7) are valid document children; Text (3) is not.
|
|
1033
|
+
has_text = true if arg.node_type == 3
|
|
1034
|
+
when Dommy::Node
|
|
1035
|
+
# A Document / Attr / anything else is not an insertable node type.
|
|
1036
|
+
raise DOMException::HierarchyRequestError, "This node type cannot be inserted here."
|
|
1037
|
+
end
|
|
1038
|
+
end
|
|
1039
|
+
|
|
1040
|
+
# `existing` (with positions) drives the "doctype following / element
|
|
1041
|
+
# preceding child" checks; those use the FULL child list, since `child`
|
|
1042
|
+
# (the reference / node being replaced) is still in the tree. The
|
|
1043
|
+
# "already has an element/doctype child" checks, however, disregard the
|
|
1044
|
+
# node being replaced (`exclude`) — WHATWG replace's "...child that is not
|
|
1045
|
+
# child" wording.
|
|
1046
|
+
existing = ignore_existing ? [] : @backend_doc.children.to_a
|
|
1047
|
+
has_element = existing.any? { |c| c.node_type == 1 && !(exclude && c == exclude) }
|
|
1048
|
+
has_doctype = existing.any? { |c| c.node_type == 10 && !(exclude && c == exclude) }
|
|
1049
|
+
|
|
1050
|
+
# Step 6, Text/DocumentFragment-with-text: no text under a document.
|
|
1051
|
+
raise DOMException::HierarchyRequestError, "A Text node cannot be a child of a document." if has_text
|
|
1052
|
+
|
|
1053
|
+
if doctypes.positive?
|
|
1054
|
+
if doctypes > 1 || elements.positive? || has_doctype ||
|
|
1055
|
+
element_before_child?(existing, child_bn) ||
|
|
1056
|
+
(child_bn.nil? && has_element)
|
|
1057
|
+
raise DOMException::HierarchyRequestError, "A doctype cannot be inserted here."
|
|
1058
|
+
end
|
|
1059
|
+
end
|
|
1060
|
+
|
|
1061
|
+
if elements > 1
|
|
1062
|
+
raise DOMException::HierarchyRequestError, "Only one element may be a child of a document."
|
|
1063
|
+
elsif elements == 1 &&
|
|
1064
|
+
(has_element || (child_bn && child_bn.node_type == 10) || doctype_after_child?(existing, child_bn))
|
|
1065
|
+
raise DOMException::HierarchyRequestError, "An element cannot be inserted here."
|
|
1066
|
+
end
|
|
1067
|
+
end
|
|
1068
|
+
|
|
1069
|
+
# Whether any element child precedes `child_bn` in the document's child list.
|
|
1070
|
+
def element_before_child?(existing, child_bn)
|
|
1071
|
+
idx = child_bn && existing.index { |c| c == child_bn }
|
|
1072
|
+
return false unless idx
|
|
1073
|
+
|
|
1074
|
+
existing[0...idx].any? { |c| c.node_type == 1 }
|
|
1075
|
+
end
|
|
1076
|
+
|
|
1077
|
+
# Whether any doctype child follows `child_bn` in the document's child list.
|
|
1078
|
+
def doctype_after_child?(existing, child_bn)
|
|
1079
|
+
idx = child_bn && existing.index { |c| c == child_bn }
|
|
1080
|
+
return false unless idx
|
|
1081
|
+
|
|
1082
|
+
existing[idx..].any? { |c| c.node_type == 10 }
|
|
1083
|
+
end
|
|
1084
|
+
|
|
759
1085
|
# Append a node as a child of the document itself (e.g. a comment alongside
|
|
760
1086
|
# the document element). Adopts the node into this document.
|
|
761
1087
|
def append_child(node)
|
|
1088
|
+
ensure_document_insertion_validity!([node], nil)
|
|
762
1089
|
return node unless node.respond_to?(:__dommy_backend_node__)
|
|
763
1090
|
|
|
764
1091
|
# appendChild adopts a node from another document (per spec). Only needed on
|
|
@@ -773,7 +1100,9 @@ module Dommy
|
|
|
773
1100
|
# ParentNode / Node mutation on the document's direct children (the doctype
|
|
774
1101
|
# and the document element).
|
|
775
1102
|
def document_insert(args, prepend:)
|
|
776
|
-
|
|
1103
|
+
ref_bn = prepend ? @backend_doc.children.first : nil
|
|
1104
|
+
ensure_document_insertion_validity!(args, ref_bn)
|
|
1105
|
+
nodes = args.filter_map { |a| adopted_backend_node(a) }
|
|
777
1106
|
if prepend && (first = @backend_doc.children.first)
|
|
778
1107
|
nodes.reverse_each { |n| first.add_previous_sibling(n) }
|
|
779
1108
|
else
|
|
@@ -783,8 +1112,11 @@ module Dommy
|
|
|
783
1112
|
end
|
|
784
1113
|
|
|
785
1114
|
def document_replace_children(args)
|
|
1115
|
+
# replaceChildren removes the current children first, so the validity
|
|
1116
|
+
# checks ignore them (whatwg/dom#1045).
|
|
1117
|
+
ensure_document_insertion_validity!(args, nil, ignore_existing: true)
|
|
786
1118
|
@backend_doc.children.each(&:unlink)
|
|
787
|
-
args.filter_map { |a|
|
|
1119
|
+
args.filter_map { |a| adopted_backend_node(a) }.each { |n| @backend_doc.add_child(n) }
|
|
788
1120
|
nil
|
|
789
1121
|
end
|
|
790
1122
|
|
|
@@ -794,12 +1126,23 @@ module Dommy
|
|
|
794
1126
|
bn = backend_node(node)
|
|
795
1127
|
raise DOMException::NotFoundError, "node is not a child of this document" unless bn && bn.parent == @backend_doc
|
|
796
1128
|
|
|
1129
|
+
run_node_iterator_pre_remove(bn)
|
|
797
1130
|
bn.unlink
|
|
798
1131
|
node
|
|
799
1132
|
end
|
|
800
1133
|
|
|
801
1134
|
def document_insert_before(node, ref)
|
|
802
|
-
|
|
1135
|
+
# WHATWG pre-insert order: the reference child must be a child of the
|
|
1136
|
+
# parent (step 3, NotFoundError) BEFORE the node-type / document-hierarchy
|
|
1137
|
+
# checks (steps 4-6).
|
|
1138
|
+
ref_present = !(ref.nil? || (defined?(Bridge::UNDEFINED) && ref.equal?(Bridge::UNDEFINED)))
|
|
1139
|
+
ref_bn = ref_present ? backend_node(ref) : nil
|
|
1140
|
+
if ref_present && !(ref_bn && ref_bn.parent == @backend_doc)
|
|
1141
|
+
raise DOMException::NotFoundError, "The reference child is not a child of this document."
|
|
1142
|
+
end
|
|
1143
|
+
|
|
1144
|
+
ensure_document_insertion_validity!([node], ref_bn)
|
|
1145
|
+
bn = adopted_backend_node(node)
|
|
803
1146
|
return node unless bn
|
|
804
1147
|
|
|
805
1148
|
ref_node = ref && backend_node(ref)
|
|
@@ -815,17 +1158,33 @@ module Dommy
|
|
|
815
1158
|
old_bn = backend_node(old_child)
|
|
816
1159
|
raise DOMException::NotFoundError, "node is not a child of this document" unless old_bn && old_bn.parent == @backend_doc
|
|
817
1160
|
|
|
818
|
-
|
|
819
|
-
|
|
1161
|
+
# replaceChild's validity disregards the node being replaced when counting
|
|
1162
|
+
# the document's existing element / doctype children.
|
|
1163
|
+
ensure_document_insertion_validity!([new_child], old_bn, exclude: old_bn)
|
|
1164
|
+
|
|
1165
|
+
# Remove old FIRST, then adopt the incoming node. A cross-document doctype
|
|
1166
|
+
# is re-created in this backend, and Makiri's fail-closed guard refuses a
|
|
1167
|
+
# second doctype — so the old one must be gone before the new is made.
|
|
1168
|
+
ref = old_bn.next
|
|
820
1169
|
old_bn.unlink
|
|
1170
|
+
if !Backend.moves_nodes_across_documents? && new_child.respond_to?(:document) && !new_child.document.equal?(self)
|
|
1171
|
+
new_child = adopt_node(new_child)
|
|
1172
|
+
end
|
|
1173
|
+
new_bn = backend_node(new_child)
|
|
1174
|
+
if new_bn
|
|
1175
|
+
ref && ref.parent == @backend_doc ? ref.add_previous_sibling(new_bn) : @backend_doc.add_child(new_bn)
|
|
1176
|
+
end
|
|
821
1177
|
old_child
|
|
822
1178
|
end
|
|
823
1179
|
|
|
824
|
-
# Called by DocumentType#remove — the doctype
|
|
825
|
-
#
|
|
826
|
-
def __internal_remove_doctype__(
|
|
827
|
-
|
|
828
|
-
|
|
1180
|
+
# Called by DocumentType#remove — unlink the backend doctype node so the tree
|
|
1181
|
+
# (and `document.doctype`, which re-derives from the tree) no longer sees it.
|
|
1182
|
+
def __internal_remove_doctype__(doctype)
|
|
1183
|
+
node = backend_node(doctype) || Backend.internal_subset(@backend_doc)
|
|
1184
|
+
return nil unless node
|
|
1185
|
+
|
|
1186
|
+
run_node_iterator_pre_remove(node)
|
|
1187
|
+
node.unlink
|
|
829
1188
|
nil
|
|
830
1189
|
end
|
|
831
1190
|
|
|
@@ -854,6 +1213,21 @@ module Dommy
|
|
|
854
1213
|
node.respond_to?(:__dommy_backend_node__) ? node.__dommy_backend_node__ : nil
|
|
855
1214
|
end
|
|
856
1215
|
|
|
1216
|
+
# Like `backend_node`, but first adopts a node that belongs to another
|
|
1217
|
+
# document (per the insert steps) — the document-child insert paths
|
|
1218
|
+
# (append / prepend / replaceChildren / insertBefore) need this exactly as
|
|
1219
|
+
# `append_child` does, otherwise a cross-document node's backend node comes
|
|
1220
|
+
# from a foreign arena and the insertion silently drops it on a backend that
|
|
1221
|
+
# can't move nodes across documents (Makiri).
|
|
1222
|
+
def adopted_backend_node(node)
|
|
1223
|
+
return nil unless node.respond_to?(:__dommy_backend_node__)
|
|
1224
|
+
|
|
1225
|
+
if !Backend.moves_nodes_across_documents? && node.respond_to?(:document) && !node.document.equal?(self)
|
|
1226
|
+
node = adopt_node(node)
|
|
1227
|
+
end
|
|
1228
|
+
node.__dommy_backend_node__
|
|
1229
|
+
end
|
|
1230
|
+
|
|
857
1231
|
# Delegate to CookieJar
|
|
858
1232
|
|
|
859
1233
|
def cookie
|
|
@@ -916,7 +1290,8 @@ module Dommy
|
|
|
916
1290
|
# Create a Comment node. Wraps the Makiri comment so it flows
|
|
917
1291
|
# through the same wrap_node identity machinery as Element / TextNode.
|
|
918
1292
|
def create_comment(text)
|
|
919
|
-
|
|
1293
|
+
# WebIDL DOMString: JS null coerces to "null" (undefined -> "undefined").
|
|
1294
|
+
@node_wrapper_cache.create_comment(text.nil? ? "null" : text)
|
|
920
1295
|
end
|
|
921
1296
|
|
|
922
1297
|
def create_cdata_section(text)
|
|
@@ -966,6 +1341,10 @@ module Dommy
|
|
|
966
1341
|
cookie
|
|
967
1342
|
when "nodeType"
|
|
968
1343
|
9
|
|
1344
|
+
when "nodeValue", "textContent"
|
|
1345
|
+
# A Document's nodeValue and textContent are null (not the concatenated
|
|
1346
|
+
# descendant text) per the DOM.
|
|
1347
|
+
nil
|
|
969
1348
|
when "activeElement"
|
|
970
1349
|
active_element
|
|
971
1350
|
when "URL", "documentURI"
|
|
@@ -1050,10 +1429,65 @@ module Dommy
|
|
|
1050
1429
|
when "nodeName"
|
|
1051
1430
|
"#document"
|
|
1052
1431
|
else
|
|
1053
|
-
|
|
1432
|
+
# WebIDL named getter: `document.someName` exposes a named embed / form /
|
|
1433
|
+
# iframe / img / object element (or an img/object by id). Unknown
|
|
1434
|
+
# otherwise → JS undefined.
|
|
1435
|
+
named = document_named_property(key.to_s)
|
|
1436
|
+
named.nil? ? Bridge::ABSENT : named
|
|
1437
|
+
end
|
|
1438
|
+
end
|
|
1439
|
+
|
|
1440
|
+
# The document's supported property names (for `"name" in document`): the
|
|
1441
|
+
# `name` of each exposed element, plus the `id` of id-exposed img/object.
|
|
1442
|
+
def __js_named_props__
|
|
1443
|
+
names = []
|
|
1444
|
+
named_getter_nodes.each do |node|
|
|
1445
|
+
n = node["name"].to_s
|
|
1446
|
+
names << n unless n.empty?
|
|
1447
|
+
id = node["id"].to_s
|
|
1448
|
+
names << id if !id.empty? && %w[img object].include?(node.name.to_s.downcase) && !n.empty?
|
|
1054
1449
|
end
|
|
1450
|
+
names.uniq
|
|
1451
|
+
end
|
|
1452
|
+
|
|
1453
|
+
# Resolve a document named-getter property: nil when unsupported, a single
|
|
1454
|
+
# element (a named iframe yields its content window), or an HTMLCollection
|
|
1455
|
+
# when several elements share the name.
|
|
1456
|
+
def document_named_property(name)
|
|
1457
|
+
return nil if name.empty?
|
|
1458
|
+
|
|
1459
|
+
matches = named_getter_nodes.select do |node|
|
|
1460
|
+
node["name"] == name ||
|
|
1461
|
+
(node["id"] == name && %w[img object].include?(node.name.to_s.downcase) && !node["name"].to_s.empty?)
|
|
1462
|
+
end
|
|
1463
|
+
wrapped = matches.map { |node| wrap_node(node) }.compact
|
|
1464
|
+
return nil if wrapped.empty?
|
|
1465
|
+
|
|
1466
|
+
if wrapped.length == 1
|
|
1467
|
+
el = wrapped.first
|
|
1468
|
+
cw = el.respond_to?(:content_window) ? el.content_window : nil
|
|
1469
|
+
cw || el
|
|
1470
|
+
else
|
|
1471
|
+
HTMLCollection.new { document_named_property_nodes(name) }
|
|
1472
|
+
end
|
|
1473
|
+
end
|
|
1474
|
+
|
|
1475
|
+
private
|
|
1476
|
+
|
|
1477
|
+
# Elements the document's named getter exposes, in tree order.
|
|
1478
|
+
def named_getter_nodes
|
|
1479
|
+
@backend_doc.css("embed, form, iframe, img, object")
|
|
1480
|
+
end
|
|
1481
|
+
|
|
1482
|
+
def document_named_property_nodes(name)
|
|
1483
|
+
named_getter_nodes.select do |node|
|
|
1484
|
+
node["name"] == name ||
|
|
1485
|
+
(node["id"] == name && %w[img object].include?(node.name.to_s.downcase) && !node["name"].to_s.empty?)
|
|
1486
|
+
end.map { |node| wrap_node(node) }.compact
|
|
1055
1487
|
end
|
|
1056
1488
|
|
|
1489
|
+
public
|
|
1490
|
+
|
|
1057
1491
|
def __js_set__(key, value)
|
|
1058
1492
|
case key
|
|
1059
1493
|
when "title"
|
|
@@ -1070,6 +1504,10 @@ module Dommy
|
|
|
1070
1504
|
# `document.location = url` navigates, same as `location.href = url`.
|
|
1071
1505
|
loc = @default_view&.__js_get__("location")
|
|
1072
1506
|
loc&.__js_set__("href", value)
|
|
1507
|
+
when "head", "documentElement"
|
|
1508
|
+
# Readonly attributes: assignment is a silent no-op. Handle it here so the
|
|
1509
|
+
# bridge doesn't store a JS-side expando that would shadow the getter.
|
|
1510
|
+
nil
|
|
1073
1511
|
else
|
|
1074
1512
|
return Bridge::UNHANDLED
|
|
1075
1513
|
end
|
|
@@ -1084,12 +1522,19 @@ module Dommy
|
|
|
1084
1522
|
getElementsByClassName getElementsByTagName getElementsByTagNameNS getElementsByName createAttribute
|
|
1085
1523
|
createAttributeNS createTreeWalker createNodeIterator createRange createEvent importNode
|
|
1086
1524
|
adoptNode hasFocus getSelection elementFromPoint queryCommandSupported addEventListener
|
|
1087
|
-
removeEventListener dispatchEvent write writeln open close isEqualNode appendChild
|
|
1525
|
+
removeEventListener dispatchEvent write writeln open close isEqualNode isSameNode appendChild
|
|
1088
1526
|
hasChildNodes contains append prepend replaceChildren removeChild insertBefore replaceChild
|
|
1089
1527
|
cloneNode normalize compareDocumentPosition getRootNode
|
|
1528
|
+
lookupNamespaceURI lookupPrefix isDefaultNamespace
|
|
1090
1529
|
]
|
|
1091
1530
|
def __js_call__(method, args)
|
|
1092
1531
|
case method
|
|
1532
|
+
when "lookupNamespaceURI"
|
|
1533
|
+
lookup_namespace_uri(args[0])
|
|
1534
|
+
when "lookupPrefix"
|
|
1535
|
+
lookup_prefix(args[0])
|
|
1536
|
+
when "isDefaultNamespace"
|
|
1537
|
+
is_default_namespace(args[0])
|
|
1093
1538
|
when "getRootNode"
|
|
1094
1539
|
# A document is its own root (no shadow tree above it), for any options.
|
|
1095
1540
|
# Exposing this is load-bearing: React's resource hoisting computes its
|
|
@@ -1104,6 +1549,8 @@ module Dommy
|
|
|
1104
1549
|
contains?(args[0])
|
|
1105
1550
|
when "isEqualNode"
|
|
1106
1551
|
is_equal_node(args[0])
|
|
1552
|
+
when "isSameNode"
|
|
1553
|
+
is_same_node(args[0])
|
|
1107
1554
|
when "appendChild"
|
|
1108
1555
|
append_child(args[0])
|
|
1109
1556
|
when "append"
|
|
@@ -1115,6 +1562,11 @@ module Dommy
|
|
|
1115
1562
|
when "removeChild"
|
|
1116
1563
|
document_remove_child(args[0])
|
|
1117
1564
|
when "insertBefore"
|
|
1565
|
+
raise Bridge::TypeError, "insertBefore requires 2 arguments." if args.length < 2
|
|
1566
|
+
unless args[1].nil? || args[1].equal?(Bridge::UNDEFINED) || args[1].is_a?(Dommy::Node)
|
|
1567
|
+
raise Bridge::TypeError, "The reference child is not a Node."
|
|
1568
|
+
end
|
|
1569
|
+
|
|
1118
1570
|
document_insert_before(args[0], args[1])
|
|
1119
1571
|
when "replaceChild"
|
|
1120
1572
|
document_replace_child(args[0], args[1])
|
|
@@ -1246,6 +1698,10 @@ module Dommy
|
|
|
1246
1698
|
@node_wrapper_cache.wrap(node)
|
|
1247
1699
|
end
|
|
1248
1700
|
|
|
1701
|
+
def wrap_cloned_element_ns(node, namespace, prefix, local, qualified_name)
|
|
1702
|
+
@node_wrapper_cache.wrap_cloned_element_ns(node, namespace, prefix, local, qualified_name)
|
|
1703
|
+
end
|
|
1704
|
+
|
|
1249
1705
|
# Clear the cached wrapper so the next `wrap_node` creates a new
|
|
1250
1706
|
# one. Used by `customElements.define` to upgrade nodes that were
|
|
1251
1707
|
# constructed before the registration landed.
|
|
@@ -1253,6 +1709,10 @@ module Dommy
|
|
|
1253
1709
|
@node_wrapper_cache.reset_wrapper(nokogiri_node)
|
|
1254
1710
|
end
|
|
1255
1711
|
|
|
1712
|
+
def __internal_peek_wrapper__(nokogiri_node)
|
|
1713
|
+
@node_wrapper_cache.peek(nokogiri_node)
|
|
1714
|
+
end
|
|
1715
|
+
|
|
1256
1716
|
# ShadowRoot identity registry: map a Nokogiri DocumentFragment
|
|
1257
1717
|
# (the shadow tree's backing node) to the wrapping ShadowRoot so
|
|
1258
1718
|
# slot assignment and event composition can walk from any inner
|
|
@@ -1380,7 +1840,8 @@ module Dommy
|
|
|
1380
1840
|
end
|
|
1381
1841
|
|
|
1382
1842
|
def create_text_node(text)
|
|
1383
|
-
|
|
1843
|
+
# WebIDL DOMString: JS null coerces to "null" (undefined -> "undefined").
|
|
1844
|
+
@node_wrapper_cache.create_text_node(text.nil? ? "null" : text)
|
|
1384
1845
|
end
|
|
1385
1846
|
|
|
1386
1847
|
def query_selector(selector)
|
|
@@ -1402,7 +1863,9 @@ module Dommy
|
|
|
1402
1863
|
end
|
|
1403
1864
|
|
|
1404
1865
|
def get_element_by_id(id)
|
|
1405
|
-
|
|
1866
|
+
# WebIDL DOMString: a null argument coerces to "null" (so it can match an
|
|
1867
|
+
# element with id="null"); undefined already stringifies to "undefined".
|
|
1868
|
+
@node_wrapper_cache.get_element_by_id(id.nil? ? "null" : id)
|
|
1406
1869
|
end
|
|
1407
1870
|
|
|
1408
1871
|
# ----- template content helpers (called from Element) -----
|
|
@@ -1485,9 +1948,14 @@ module Dommy
|
|
|
1485
1948
|
end
|
|
1486
1949
|
|
|
1487
1950
|
def read_title
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1951
|
+
# The first title element in tree order (usually the head's), with its
|
|
1952
|
+
# child text content stripped and collapsed of ASCII whitespace per WHATWG.
|
|
1953
|
+
# ASCII whitespace is exactly tab/LF/FF/CR/space — NOT Ruby's String#strip
|
|
1954
|
+
# set, which also removes U+000B (vertical tab) and must be left intact.
|
|
1955
|
+
title = @backend_doc.at_css("title")
|
|
1956
|
+
return "" unless title
|
|
1957
|
+
|
|
1958
|
+
title.text.gsub(/[\t\n\f\r ]+/, " ").gsub(/\A[\t\n\f\r ]+|[\t\n\f\r ]+\z/, "")
|
|
1491
1959
|
end
|
|
1492
1960
|
|
|
1493
1961
|
def write_title(value)
|