dommy 0.11.0 → 0.13.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/backend/makiri_adapter.rb +2 -14
- data/lib/dommy/backend.rb +0 -5
- data/lib/dommy/bridge.rb +16 -3
- data/lib/dommy/browser.rb +43 -47
- data/lib/dommy/callable_invoker.rb +16 -0
- data/lib/dommy/compression_streams.rb +12 -62
- data/lib/dommy/custom_elements.rb +22 -16
- data/lib/dommy/document.rb +241 -47
- data/lib/dommy/element.rb +71 -23
- data/lib/dommy/encodings/tables.rb +751 -0
- data/lib/dommy/encodings.rb +299 -0
- data/lib/dommy/event.rb +10 -8
- data/lib/dommy/fetch.rb +60 -49
- data/lib/dommy/form_data.rb +10 -0
- data/lib/dommy/html_elements.rb +136 -62
- data/lib/dommy/internal/child_node.rb +1 -5
- data/lib/dommy/internal/exception_report.rb +123 -0
- data/lib/dommy/internal/idna.rb +4 -4
- data/lib/dommy/internal/idna_data.rb +513 -100
- data/lib/dommy/internal/leaf_node.rb +2 -8
- data/lib/dommy/internal/mutation_coordinator.rb +46 -19
- data/lib/dommy/internal/node_wrapper_cache.rb +23 -1
- data/lib/dommy/internal/parent_node.rb +14 -74
- data/lib/dommy/internal/range_text_serializer.rb +2 -1
- data/lib/dommy/internal/selector_ast.rb +17 -0
- data/lib/dommy/internal/selector_matcher.rb +20 -21
- data/lib/dommy/internal/selector_parser.rb +104 -32
- data/lib/dommy/internal/shadow_root_registry.rb +12 -1
- data/lib/dommy/internal/url_parser.rb +70 -18
- data/lib/dommy/internal/web_idl.rb +57 -0
- data/lib/dommy/js/custom_element_bridge.rb +10 -0
- data/lib/dommy/js/dom_interfaces.rb +15 -4
- data/lib/dommy/js/error_log.rb +137 -0
- data/lib/dommy/js/handle_table.rb +15 -0
- data/lib/dommy/js/host_bridge.rb +27 -0
- data/lib/dommy/js/host_runtime.js +186 -30
- data/lib/dommy/js/marshaller.rb +22 -2
- data/lib/dommy/js/runtime.rb +8 -1
- data/lib/dommy/js/script_boot.rb +58 -14
- data/lib/dommy/js/wire_tags.rb +8 -0
- data/lib/dommy/location.rb +14 -4
- data/lib/dommy/navigator.rb +3 -1
- data/lib/dommy/node.rb +19 -3
- data/lib/dommy/promise.rb +4 -4
- data/lib/dommy/range.rb +116 -183
- data/lib/dommy/scheduler.rb +35 -6
- data/lib/dommy/selection.rb +422 -0
- data/lib/dommy/shadow_root.rb +8 -3
- data/lib/dommy/static_range.rb +65 -0
- data/lib/dommy/streams.rb +1200 -187
- data/lib/dommy/svg_elements.rb +45 -0
- data/lib/dommy/text_codec.rb +110 -213
- data/lib/dommy/tree_walker.rb +9 -17
- data/lib/dommy/url.rb +34 -32
- data/lib/dommy/url_pattern/canonicalizer.rb +183 -0
- data/lib/dommy/url_pattern/component.rb +64 -0
- data/lib/dommy/url_pattern/constructor_string_parser.rb +281 -0
- data/lib/dommy/url_pattern/init_processor.rb +163 -0
- data/lib/dommy/url_pattern/pattern_parser.rb +343 -0
- data/lib/dommy/url_pattern/regexp_translator.rb +694 -0
- data/lib/dommy/url_pattern/tokenizer.rb +199 -0
- data/lib/dommy/url_pattern.rb +310 -103
- data/lib/dommy/version.rb +1 -1
- data/lib/dommy/window.rb +164 -35
- data/lib/dommy/xml_http_request.rb +18 -4
- data/lib/dommy.rb +5 -0
- metadata +17 -3
data/lib/dommy/document.rb
CHANGED
|
@@ -141,6 +141,12 @@ module Dommy
|
|
|
141
141
|
end
|
|
142
142
|
private :synthetic_insert
|
|
143
143
|
|
|
144
|
+
# A doctype is connected when its tree is a document's; a synthetic one is
|
|
145
|
+
# never in a tree at all.
|
|
146
|
+
def is_connected?
|
|
147
|
+
get_root_node.is_a?(Dommy::Document)
|
|
148
|
+
end
|
|
149
|
+
|
|
144
150
|
def __js_get__(key)
|
|
145
151
|
case key
|
|
146
152
|
when "name"
|
|
@@ -168,6 +174,8 @@ module Dommy
|
|
|
168
174
|
NodeList.new
|
|
169
175
|
when "firstChild", "lastChild"
|
|
170
176
|
nil
|
|
177
|
+
when "isConnected"
|
|
178
|
+
is_connected?
|
|
171
179
|
end
|
|
172
180
|
end
|
|
173
181
|
|
|
@@ -802,11 +810,11 @@ module Dommy
|
|
|
802
810
|
href = base_el["href"].to_s
|
|
803
811
|
return doc_url if href.empty?
|
|
804
812
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
813
|
+
# HTML "document base URL": the frozen base URL is the href parsed
|
|
814
|
+
# against the document's URL; one that does not parse falls back.
|
|
815
|
+
URL.new(href, doc_url.to_s.empty? ? nil : doc_url).href
|
|
816
|
+
rescue Bridge::TypeError
|
|
817
|
+
doc_url
|
|
810
818
|
end
|
|
811
819
|
|
|
812
820
|
# `document.domain` — host portion of the URL. Real browsers
|
|
@@ -954,15 +962,7 @@ module Dommy
|
|
|
954
962
|
# Ruby Proc, a JS-bridge callable, or an object with
|
|
955
963
|
# `accept_node` / `acceptNode`.
|
|
956
964
|
def create_tree_walker(root, what_to_show = NodeFilter::SHOW_ALL, filter = nil)
|
|
957
|
-
TreeWalker.new(
|
|
958
|
-
end
|
|
959
|
-
|
|
960
|
-
# The `root` of a TreeWalker / NodeIterator is a non-nullable WebIDL `Node`:
|
|
961
|
-
# a null or non-Node argument is a TypeError before construction.
|
|
962
|
-
def require_node_root(root)
|
|
963
|
-
return root if root.is_a?(Dommy::Node)
|
|
964
|
-
|
|
965
|
-
raise Bridge::TypeError, "createTreeWalker/createNodeIterator root must be a Node"
|
|
965
|
+
TreeWalker.new(Internal::WebIDL.node!(root), what_to_show, filter)
|
|
966
966
|
end
|
|
967
967
|
|
|
968
968
|
# WebIDL `unsigned long whatToShow = 0xFFFFFFFF`: an omitted or `undefined`
|
|
@@ -988,6 +988,12 @@ module Dommy
|
|
|
988
988
|
# wrapper is owned by `this`. Per spec, the source node is left
|
|
989
989
|
# in place. `deep: true` copies the entire subtree.
|
|
990
990
|
def import_node(node, deep = false)
|
|
991
|
+
# Step 1: "If node is a document or shadow root, throw a
|
|
992
|
+
# NotSupportedError." Neither has anywhere to go in another document.
|
|
993
|
+
if node.is_a?(Document) || node.is_a?(ShadowRoot)
|
|
994
|
+
raise DOMException::NotSupportedError, "a #{node.is_a?(Document) ? "document" : "shadow root"} cannot be imported"
|
|
995
|
+
end
|
|
996
|
+
|
|
991
997
|
# An Attr is a Node but not a backend-tree node: it is copied by rebuilding
|
|
992
998
|
# it here with the same qualified name, namespace, prefix and value, owned
|
|
993
999
|
# by no element (importNode never attaches the copy to anything).
|
|
@@ -1105,7 +1111,11 @@ module Dommy
|
|
|
1105
1111
|
|
|
1106
1112
|
alias has_focus has_focus?
|
|
1107
1113
|
|
|
1114
|
+
# A document without a browsing context (one built by DOMParser or
|
|
1115
|
+
# createHTMLDocument) has no selection.
|
|
1108
1116
|
def get_selection
|
|
1117
|
+
return nil unless @default_view
|
|
1118
|
+
|
|
1109
1119
|
@__selection ||= Selection.new(self)
|
|
1110
1120
|
end
|
|
1111
1121
|
|
|
@@ -1147,7 +1157,7 @@ module Dommy
|
|
|
1147
1157
|
# `document.createNodeIterator(root, whatToShow?, filter?)` —
|
|
1148
1158
|
# flat depth-first iteration.
|
|
1149
1159
|
def create_node_iterator(root, what_to_show = NodeFilter::SHOW_ALL, filter = nil)
|
|
1150
|
-
root =
|
|
1160
|
+
root = Internal::WebIDL.node!(root)
|
|
1151
1161
|
iterator = NodeIterator.new(root, what_to_show, filter)
|
|
1152
1162
|
# The "NodeIterator pre-removing steps" run for iterators whose root's node
|
|
1153
1163
|
# document is the removed node's document. Track the iterator on the root's
|
|
@@ -1348,6 +1358,8 @@ module Dommy
|
|
|
1348
1358
|
#
|
|
1349
1359
|
# Spec: https://dom.spec.whatwg.org/#dom-parentnode-movebefore
|
|
1350
1360
|
def move_before(node, child = nil)
|
|
1361
|
+
Internal::WebIDL.node!(node)
|
|
1362
|
+
Internal::WebIDL.nullable_node!(child)
|
|
1351
1363
|
bn = move_backend_node(node)
|
|
1352
1364
|
ref_bn = move_backend_node(child)
|
|
1353
1365
|
ref_bn = ref_bn.next_sibling if ref_bn && bn && ref_bn == bn
|
|
@@ -1356,7 +1368,7 @@ module Dommy
|
|
|
1356
1368
|
old_parent = bn.parent
|
|
1357
1369
|
old_previous = bn.previous_sibling
|
|
1358
1370
|
old_next = bn.next_sibling
|
|
1359
|
-
detach_node(bn)
|
|
1371
|
+
detach_node(bn, moving: true) # steps 10-11, 14
|
|
1360
1372
|
ref_bn = nil if ref_bn && ref_bn.parent != @backend_doc
|
|
1361
1373
|
__internal_ranges_will_insert__(@backend_doc, ref_bn, 1) # step 16
|
|
1362
1374
|
new_previous = ref_bn ? ref_bn.previous_sibling : @backend_doc.children.to_a.last
|
|
@@ -1365,11 +1377,12 @@ module Dommy
|
|
|
1365
1377
|
notify_child_list_mutation(
|
|
1366
1378
|
target_node: old_parent, added_nodes: [], removed_nodes: [bn],
|
|
1367
1379
|
previous_sibling: old_previous && wrap_node(old_previous),
|
|
1368
|
-
next_sibling: old_next && wrap_node(old_next)
|
|
1380
|
+
next_sibling: old_next && wrap_node(old_next), moving: true
|
|
1369
1381
|
)
|
|
1370
1382
|
end
|
|
1371
1383
|
notify_document_child_list(added: [bn], previous_sibling: new_previous && wrap_node(new_previous),
|
|
1372
|
-
next_sibling: ref_bn && wrap_node(ref_bn))
|
|
1384
|
+
next_sibling: ref_bn && wrap_node(ref_bn), moving: true)
|
|
1385
|
+
__internal_notify_moved_subtree__(bn) # step 19.3, into a document
|
|
1373
1386
|
nil
|
|
1374
1387
|
end
|
|
1375
1388
|
|
|
@@ -1582,11 +1595,18 @@ module Dommy
|
|
|
1582
1595
|
nil
|
|
1583
1596
|
end
|
|
1584
1597
|
|
|
1585
|
-
# `document.cloneNode(deep)` → a fresh Document
|
|
1586
|
-
#
|
|
1598
|
+
# `document.cloneNode(deep)` → a fresh Document of the same kind, keeping
|
|
1599
|
+
# the content type. "Clone a single node" creates a document EMPTY, and a
|
|
1600
|
+
# deep clone then copies the original's children into it — so nothing
|
|
1601
|
+
# sprouts an html/head/body of its own, and a document with no children
|
|
1602
|
+
# clones to one with no children.
|
|
1587
1603
|
def clone_node(deep)
|
|
1588
|
-
copy =
|
|
1589
|
-
|
|
1604
|
+
copy = Document.new(nil, backend_doc: Backend.empty_document_like(@backend_doc))
|
|
1605
|
+
copy.content_type = @content_type
|
|
1606
|
+
return copy unless deep
|
|
1607
|
+
|
|
1608
|
+
child_nodes.each { |child| copy.append_child(copy.import_node(child, true)) }
|
|
1609
|
+
copy
|
|
1590
1610
|
end
|
|
1591
1611
|
|
|
1592
1612
|
def backend_node(node)
|
|
@@ -1987,9 +2007,7 @@ module Dommy
|
|
|
1987
2007
|
document_remove_child(args[0])
|
|
1988
2008
|
when "insertBefore"
|
|
1989
2009
|
raise Bridge::TypeError, "insertBefore requires 2 arguments." if args.length < 2
|
|
1990
|
-
|
|
1991
|
-
raise Bridge::TypeError, "The reference child is not a Node."
|
|
1992
|
-
end
|
|
2010
|
+
Internal::WebIDL.nullable_node!(args[1])
|
|
1993
2011
|
|
|
1994
2012
|
document_insert_before(args[0], args[1])
|
|
1995
2013
|
when "replaceChild"
|
|
@@ -2002,7 +2020,7 @@ module Dommy
|
|
|
2002
2020
|
when "cloneNode"
|
|
2003
2021
|
clone_node(args[0])
|
|
2004
2022
|
when "normalize"
|
|
2005
|
-
|
|
2023
|
+
normalize
|
|
2006
2024
|
when "writeln"
|
|
2007
2025
|
write(*(args + ["\n"]))
|
|
2008
2026
|
when "exitFullscreen"
|
|
@@ -2208,6 +2226,33 @@ module Dommy
|
|
|
2208
2226
|
@shadow_registry.find_for_fragment(fragment_node)
|
|
2209
2227
|
end
|
|
2210
2228
|
|
|
2229
|
+
def __internal_shadow_root_for_host__(host_node)
|
|
2230
|
+
@shadow_registry.find_for_host(host_node)
|
|
2231
|
+
end
|
|
2232
|
+
|
|
2233
|
+
# Every element among `root`'s shadow-including inclusive descendants, as
|
|
2234
|
+
# backend nodes, in shadow-including tree order: an element, then the shadow
|
|
2235
|
+
# tree it hosts, then its children. The whole list, shadow trees included, is
|
|
2236
|
+
# taken before the first element is yielded. Callbacks run synchronously here,
|
|
2237
|
+
# and one that attaches a shadow root or inserts elements triggers those
|
|
2238
|
+
# changes' own reactions; walking into them as well would run them twice.
|
|
2239
|
+
def __internal_each_shadow_including_element__(root, &block)
|
|
2240
|
+
shadow_including_elements(root).each(&block)
|
|
2241
|
+
nil
|
|
2242
|
+
end
|
|
2243
|
+
|
|
2244
|
+
def shadow_including_elements(root, list = [])
|
|
2245
|
+
elements = root.respond_to?(:element?) && root.element? ? [root] : []
|
|
2246
|
+
elements.concat(root.css("*").to_a) if root.respond_to?(:css)
|
|
2247
|
+
elements.each do |element|
|
|
2248
|
+
list << element
|
|
2249
|
+
shadow = @shadow_registry.find_for_host(element)
|
|
2250
|
+
shadow_including_elements(shadow.__dommy_backend_node__, list) if shadow
|
|
2251
|
+
end
|
|
2252
|
+
list
|
|
2253
|
+
end
|
|
2254
|
+
private :shadow_including_elements
|
|
2255
|
+
|
|
2211
2256
|
def __internal_shadow_root_containing__(node)
|
|
2212
2257
|
@shadow_registry.find_enclosing(node)
|
|
2213
2258
|
end
|
|
@@ -2235,6 +2280,12 @@ module Dommy
|
|
|
2235
2280
|
@mutation_coordinator.notify_connected_subtree(nk)
|
|
2236
2281
|
end
|
|
2237
2282
|
|
|
2283
|
+
# The custom element reactions of a move (moveBefore), for a node that ended
|
|
2284
|
+
# up connected.
|
|
2285
|
+
def __internal_notify_moved_subtree__(nk)
|
|
2286
|
+
@mutation_coordinator.notify_moved_subtree(nk)
|
|
2287
|
+
end
|
|
2288
|
+
|
|
2238
2289
|
def __internal_notify_disconnected_subtree__(nk)
|
|
2239
2290
|
@mutation_coordinator.notify_disconnected_subtree(nk)
|
|
2240
2291
|
end
|
|
@@ -2260,14 +2311,16 @@ module Dommy
|
|
|
2260
2311
|
added_nodes:,
|
|
2261
2312
|
removed_nodes:,
|
|
2262
2313
|
previous_sibling: nil,
|
|
2263
|
-
next_sibling: nil
|
|
2314
|
+
next_sibling: nil,
|
|
2315
|
+
moving: false
|
|
2264
2316
|
)
|
|
2265
2317
|
@mutation_coordinator.notify_child_list_mutation(
|
|
2266
2318
|
target_node: target_node,
|
|
2267
2319
|
added_nodes: added_nodes,
|
|
2268
2320
|
removed_nodes: removed_nodes,
|
|
2269
2321
|
previous_sibling: previous_sibling,
|
|
2270
|
-
next_sibling: next_sibling
|
|
2322
|
+
next_sibling: next_sibling,
|
|
2323
|
+
moving: moving
|
|
2271
2324
|
)
|
|
2272
2325
|
end
|
|
2273
2326
|
|
|
@@ -2302,13 +2355,14 @@ module Dommy
|
|
|
2302
2355
|
node && wrap_node(node)
|
|
2303
2356
|
end
|
|
2304
2357
|
|
|
2305
|
-
def notify_document_child_list(added: [], removed: [], previous_sibling: nil, next_sibling: nil)
|
|
2358
|
+
def notify_document_child_list(added: [], removed: [], previous_sibling: nil, next_sibling: nil, moving: false)
|
|
2306
2359
|
notify_child_list_mutation(
|
|
2307
2360
|
target_node: @backend_doc,
|
|
2308
2361
|
added_nodes: added,
|
|
2309
2362
|
removed_nodes: removed,
|
|
2310
2363
|
previous_sibling: previous_sibling,
|
|
2311
|
-
next_sibling: next_sibling
|
|
2364
|
+
next_sibling: next_sibling,
|
|
2365
|
+
moving: moving
|
|
2312
2366
|
)
|
|
2313
2367
|
end
|
|
2314
2368
|
|
|
@@ -2316,10 +2370,18 @@ module Dommy
|
|
|
2316
2370
|
# batch several removals into one childList record (replaceChildren,
|
|
2317
2371
|
# textContent=, replaceChild) use this and queue the record themselves;
|
|
2318
2372
|
# `remove_node_with_notify` is this plus a per-node record.
|
|
2319
|
-
|
|
2373
|
+
#
|
|
2374
|
+
# `moving:` marks the first half of the "move" primitive (moveBefore). A
|
|
2375
|
+
# move takes the node out of its old parent's children only to put it
|
|
2376
|
+
# straight back into the same shadow-including tree: it never leaves the
|
|
2377
|
+
# document, so nothing that reacts to a removal from the document may run.
|
|
2378
|
+
def detach_node(node, moving: false)
|
|
2320
2379
|
pre_remove_node(node)
|
|
2321
2380
|
add_transient_observers_for(node)
|
|
2322
2381
|
node.unlink
|
|
2382
|
+
# A removal can take a shadow tree, and a selection range in it, out of
|
|
2383
|
+
# the document without moving the range; the selection lets go of it.
|
|
2384
|
+
@__selection&.__internal_node_removed__ unless moving
|
|
2323
2385
|
node
|
|
2324
2386
|
end
|
|
2325
2387
|
|
|
@@ -2455,6 +2517,73 @@ module Dommy
|
|
|
2455
2517
|
end
|
|
2456
2518
|
end
|
|
2457
2519
|
|
|
2520
|
+
# Node.normalize() on the document: every Text run in the tree.
|
|
2521
|
+
def normalize
|
|
2522
|
+
__internal_normalize__(@backend_doc)
|
|
2523
|
+
end
|
|
2524
|
+
|
|
2525
|
+
# Node#normalize — merge each run of adjacent exclusive Text descendants
|
|
2526
|
+
# into its first node (preserving that node's identity, so a JS reference
|
|
2527
|
+
# to it survives) and drop empty Text nodes. Recurses the whole subtree,
|
|
2528
|
+
# so it serves Element, DocumentFragment, ShadowRoot and the Document alike.
|
|
2529
|
+
#
|
|
2530
|
+
# A run is merged one sibling at a time: append the sibling's data to the
|
|
2531
|
+
# survivor (a characterData record), hand its live range boundaries over,
|
|
2532
|
+
# remove it (a childList record), then the next. Read literally, the spec
|
|
2533
|
+
# concatenates every sibling's data first (steps 3-4, one "replace data")
|
|
2534
|
+
# and removes them afterwards (step 7), which would queue ONE
|
|
2535
|
+
# characterData record per run. Every shipping engine merges pairwise
|
|
2536
|
+
# instead — Blink, WebCore and Gecko all answer [characterData, childList,
|
|
2537
|
+
# characterData, childList, …] for a run of four text nodes, confirmed by
|
|
2538
|
+
# running the same script in Chromium 141, WebKitGTK 2.52.6 and Firefox —
|
|
2539
|
+
# and the WPT suite fixes only the childList side, so the records follow
|
|
2540
|
+
# the engines. The tree and every live range boundary end up exactly where
|
|
2541
|
+
# the spec's steps put them: a boundary in a later sibling (or on the
|
|
2542
|
+
# parent, pointing at one) is shifted down by each earlier removal and
|
|
2543
|
+
# then handed over at the survivor's length of that moment, which is the
|
|
2544
|
+
# same offset the batch steps compute up front.
|
|
2545
|
+
# https://github.com/takahashim/dommy/issues/24
|
|
2546
|
+
def __internal_normalize__(root)
|
|
2547
|
+
text_nodes = []
|
|
2548
|
+
root.traverse { |node| text_nodes << node if node.respond_to?(:text?) && node.text? }
|
|
2549
|
+
|
|
2550
|
+
text_nodes.each do |node|
|
|
2551
|
+
next unless node.parent # already removed as part of an earlier run
|
|
2552
|
+
|
|
2553
|
+
if node.content.to_s.empty?
|
|
2554
|
+
remove_node_with_notify(node)
|
|
2555
|
+
next
|
|
2556
|
+
end
|
|
2557
|
+
|
|
2558
|
+
sib = node.next
|
|
2559
|
+
while sib.respond_to?(:text?) && sib.text?
|
|
2560
|
+
following = sib.next
|
|
2561
|
+
data = sib.content.to_s
|
|
2562
|
+
# The offset the sibling's data lands at inside the survivor — the
|
|
2563
|
+
# length of what it already holds, measured before the append.
|
|
2564
|
+
length = wrap_node(node).length
|
|
2565
|
+
# An empty sibling has nothing to append: the engines skip the data
|
|
2566
|
+
# step for it (Gecko checks the length, Blink and WebCore behave the
|
|
2567
|
+
# same), so it is removed without a characterData record. Its range
|
|
2568
|
+
# boundaries still move to the survivor's join.
|
|
2569
|
+
unless data.empty?
|
|
2570
|
+
old = node.content.to_s
|
|
2571
|
+
node.content = old + data
|
|
2572
|
+
notify_character_data_mutation(target_node: node, old_value: old)
|
|
2573
|
+
end
|
|
2574
|
+
# WHATWG normalize() step 6: the merged-away sibling hands its live
|
|
2575
|
+
# range boundaries to the survivor at that offset BEFORE it is
|
|
2576
|
+
# removed, or the plain removing steps would strand them on the
|
|
2577
|
+
# parent.
|
|
2578
|
+
__internal_ranges_normalize_merge__(node, sib, length)
|
|
2579
|
+
remove_node_with_notify(sib)
|
|
2580
|
+
sib = following
|
|
2581
|
+
end
|
|
2582
|
+
end
|
|
2583
|
+
|
|
2584
|
+
nil
|
|
2585
|
+
end
|
|
2586
|
+
|
|
2458
2587
|
def __internal_ranges_replaced_data__(node, offset, count, new_length)
|
|
2459
2588
|
return unless live_ranges?
|
|
2460
2589
|
|
|
@@ -2574,40 +2703,105 @@ module Dommy
|
|
|
2574
2703
|
# `deep: true` recurses into children. Used by importNode and
|
|
2575
2704
|
# adoptNode for cross-document transfer.
|
|
2576
2705
|
def clone_into_doc(source, deep, source_document = self)
|
|
2577
|
-
copy =
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2706
|
+
copy = clone_single_node_into_doc(source, source_document)
|
|
2707
|
+
|
|
2708
|
+
if source.element? && source.name == "template"
|
|
2709
|
+
# A <template>'s contents live in a separate content fragment, not its
|
|
2710
|
+
# child list, so the generic deep pass over `children` misses them.
|
|
2711
|
+
clone_template_content(source, copy, source_document) if deep
|
|
2712
|
+
elsif deep && source.respond_to?(:children)
|
|
2713
|
+
source.children.each do |child|
|
|
2714
|
+
copy.add_child(clone_into_doc(child, true, source_document))
|
|
2715
|
+
end
|
|
2716
|
+
end
|
|
2717
|
+
|
|
2718
|
+
copy
|
|
2719
|
+
end
|
|
2720
|
+
|
|
2721
|
+
# "Clone a single node" (§4.4): the copy implements the SAME interface as
|
|
2722
|
+
# the original — a ProcessingInstruction clones to one, not to the comment
|
|
2723
|
+
# its serialization looks like — and carries the same data. Elements are
|
|
2724
|
+
# cloned by #clone_element_into_doc; children are the caller's business.
|
|
2725
|
+
def clone_single_node_into_doc(source, source_document)
|
|
2726
|
+
if source.element?
|
|
2727
|
+
clone_element_into_doc(source, source_document)
|
|
2728
|
+
elsif (cdata = Backend.cdata_class) && source.is_a?(cdata)
|
|
2729
|
+
# CDATA is a Text subtype in the backend, so ask about it first.
|
|
2730
|
+
Backend.create_cdata(source.content, @backend_doc)
|
|
2581
2731
|
elsif source.text?
|
|
2582
2732
|
Backend.create_text(source.content, @backend_doc)
|
|
2583
2733
|
elsif source.is_a?(Backend.comment_class)
|
|
2584
2734
|
Backend.create_comment(source.content, @backend_doc)
|
|
2735
|
+
elsif source.is_a?(Backend.processing_instruction_class)
|
|
2736
|
+
Backend.create_processing_instruction(source.target, source.content, @backend_doc)
|
|
2585
2737
|
elsif source.is_a?(Backend.document_fragment_class)
|
|
2586
2738
|
# A DocumentFragment clones to a fragment (its children are appended by
|
|
2587
|
-
# the deep pass
|
|
2739
|
+
# the deep pass), NOT to its first child — `importNode(<template>
|
|
2588
2740
|
# .content, true)` must return a fragment so `.firstElementChild` works
|
|
2589
2741
|
# (Vue/Alpine x-for clone template content this way). Built via the
|
|
2590
2742
|
# document's own `fragment` (as TemplateContentRegistry does) rather than
|
|
2591
2743
|
# `document_fragment_class.new`, so it works on backends whose fragment
|
|
2592
2744
|
# class isn't directly instantiable (Makiri).
|
|
2593
2745
|
Parser.fragment("", owner_doc: @backend_doc)
|
|
2746
|
+
elsif (doctype = Backend.document_type_class) && source.is_a?(doctype)
|
|
2747
|
+
wrapper = source_document.wrap_node(source)
|
|
2748
|
+
Backend.create_document_type(wrapper.name, wrapper.public_id, wrapper.system_id, @backend_doc)
|
|
2594
2749
|
else
|
|
2595
|
-
|
|
2596
|
-
fragment = Parser.fragment(source.to_html, owner_doc: @backend_doc)
|
|
2597
|
-
fragment.children.first || Backend.create_text("", @backend_doc)
|
|
2750
|
+
Backend.create_text("", @backend_doc)
|
|
2598
2751
|
end
|
|
2752
|
+
end
|
|
2599
2753
|
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2754
|
+
# An element's copy keeps its namespace, its prefix and the exact spelling
|
|
2755
|
+
# of its local name. None of that is on the backend node — the HTML backend
|
|
2756
|
+
# tracks no namespace for an element script created, and createElementNS's
|
|
2757
|
+
# case lives on the wrapper — so the original is read through its wrapper
|
|
2758
|
+
# and the copy is given the same metadata.
|
|
2759
|
+
def clone_element_into_doc(source, source_document)
|
|
2760
|
+
wrapper = source_document.wrap_node(source)
|
|
2761
|
+
namespace, prefix, local, qualified = clone_name_parts(wrapper, source)
|
|
2762
|
+
copy = Backend.create_element_loose(qualified, prefix, local, namespace, @backend_doc) ||
|
|
2763
|
+
Backend.create_element(source.name, @backend_doc)
|
|
2764
|
+
copy_attributes_into(source, copy)
|
|
2765
|
+
note_cloned_element_namespace(copy, namespace, prefix, local, qualified)
|
|
2766
|
+
copy
|
|
2767
|
+
end
|
|
2768
|
+
|
|
2769
|
+
# The four parts of the original's name — namespace, prefix, local name and
|
|
2770
|
+
# qualified name. The wrapper's own metadata has them whenever there is
|
|
2771
|
+
# anything to carry (createElementNS's case and prefix, an XML document's,
|
|
2772
|
+
# the HTML parser's SVG names); otherwise the element is an unprefixed one
|
|
2773
|
+
# whose namespace the backend knows.
|
|
2774
|
+
def clone_name_parts(wrapper, source)
|
|
2775
|
+
meta = wrapper.__internal_namespace_metadata__ if wrapper.respond_to?(:__internal_namespace_metadata__)
|
|
2776
|
+
return meta if meta
|
|
2777
|
+
|
|
2778
|
+
local = wrapper ? wrapper.local_name.to_s : source.name
|
|
2779
|
+
[wrapper&.namespace_uri, nil, local, local]
|
|
2780
|
+
end
|
|
2781
|
+
|
|
2782
|
+
# "Clone a single node" step 2.1 clones the attributes one by one, so each
|
|
2783
|
+
# keeps its own namespace and prefix — an `xml:b` must not flatten into an
|
|
2784
|
+
# attribute whose local name is the qualified string "xml:b".
|
|
2785
|
+
def copy_attributes_into(source, copy)
|
|
2786
|
+
Backend.attribute_nodes(source).each do |attr|
|
|
2787
|
+
info = Backend.attribute_ns_info(attr)
|
|
2788
|
+
if info[:namespace_uri]
|
|
2789
|
+
Backend.set_attribute_ns(copy, info[:namespace_uri], info[:prefix], info[:local_name],
|
|
2790
|
+
info[:qualified_name], info[:value])
|
|
2791
|
+
else
|
|
2792
|
+
copy[info[:qualified_name]] = info[:value]
|
|
2607
2793
|
end
|
|
2608
2794
|
end
|
|
2795
|
+
nil
|
|
2796
|
+
end
|
|
2609
2797
|
|
|
2610
|
-
|
|
2798
|
+
# Give the copy's wrapper the namespace metadata whenever the backend node
|
|
2799
|
+
# alone would report something else (see #clone_element_into_doc).
|
|
2800
|
+
def note_cloned_element_namespace(copy, namespace, prefix, local, qualified)
|
|
2801
|
+
derived = Backend.namespace_of(copy)&.href || (html_document? ? Element::HTML_NAMESPACE : nil)
|
|
2802
|
+
return if derived == namespace && prefix.nil? && local == copy.name
|
|
2803
|
+
|
|
2804
|
+
wrap_cloned_element_ns(copy, namespace, prefix, local, qualified)
|
|
2611
2805
|
end
|
|
2612
2806
|
|
|
2613
2807
|
# Clone a <template>'s content into a fragment registered as `copy`'s
|
data/lib/dommy/element.rb
CHANGED
|
@@ -21,6 +21,19 @@ module Dommy
|
|
|
21
21
|
|
|
22
22
|
# Public Ruby API (DocumentFragment surface)
|
|
23
23
|
|
|
24
|
+
# Node.cloneNode: a fresh, empty fragment, and — when `deep` — a clone of
|
|
25
|
+
# each child appended in tree order ("clone a node" step 5). Cloning the
|
|
26
|
+
# children one by one rather than re-parsing the fragment's serialization
|
|
27
|
+
# keeps each of them its own interface: a comment stays a comment, and a
|
|
28
|
+
# processing instruction does not come back as one.
|
|
29
|
+
def clone_node(deep = false)
|
|
30
|
+
copy = @document.create_document_fragment
|
|
31
|
+
return copy unless deep
|
|
32
|
+
|
|
33
|
+
child_nodes.each { |child| copy.append_child(child.clone_node(true)) }
|
|
34
|
+
copy
|
|
35
|
+
end
|
|
36
|
+
|
|
24
37
|
def children
|
|
25
38
|
element_children
|
|
26
39
|
end
|
|
@@ -152,9 +165,7 @@ module Dommy
|
|
|
152
165
|
when "isDefaultNamespace"
|
|
153
166
|
is_default_namespace(args[0])
|
|
154
167
|
when "cloneNode"
|
|
155
|
-
|
|
156
|
-
deep ? @document.wrap_node(Parser.fragment(@__node__.to_html, owner_doc: @document.backend_doc)) : @document
|
|
157
|
-
.wrap_node(Parser.fragment("", owner_doc: @document.backend_doc))
|
|
168
|
+
clone_node(args.first)
|
|
158
169
|
when "querySelector"
|
|
159
170
|
query_selector(Internal.css_query_arg!(args))
|
|
160
171
|
when "querySelectorAll"
|
|
@@ -227,7 +238,7 @@ module Dommy
|
|
|
227
238
|
end
|
|
228
239
|
|
|
229
240
|
def insert_before(node, ref)
|
|
230
|
-
|
|
241
|
+
Internal::WebIDL.node!(node)
|
|
231
242
|
ensure_pre_insertion_validity!(node, ref)
|
|
232
243
|
ref_bn = ref.respond_to?(:__dommy_backend_node__) ? ref.__dommy_backend_node__ : nil
|
|
233
244
|
ref_bn = nil unless ref_bn && ref_bn.parent == @__node__
|
|
@@ -250,7 +261,7 @@ module Dommy
|
|
|
250
261
|
end
|
|
251
262
|
|
|
252
263
|
def replace_child(new_child, old_child)
|
|
253
|
-
|
|
264
|
+
Internal::WebIDL.node!(new_child)
|
|
254
265
|
# WHATWG "replace" step 1 runs the full ensure-pre-insertion-validity,
|
|
255
266
|
# whose step 2 (node is an inclusive ancestor of parent — a cycle) comes
|
|
256
267
|
# BEFORE step 3 (the reference child's parentage). So
|
|
@@ -726,12 +737,20 @@ module Dommy
|
|
|
726
737
|
super
|
|
727
738
|
end
|
|
728
739
|
|
|
740
|
+
# Node.cloneNode. CharacterData has no children, so `deep` decides nothing:
|
|
741
|
+
# the copy is a new node of the SAME interface carrying the same data, owned
|
|
742
|
+
# by this node's document. Each subclass builds its own kind — a CDATASection
|
|
743
|
+
# that cloned to a Text would be the wrong node.
|
|
744
|
+
def clone_node(_deep = false)
|
|
745
|
+
@document.create_text_node(@__node__.text)
|
|
746
|
+
end
|
|
747
|
+
|
|
729
748
|
# Own __js_call__ methods, on top of CharacterDataNode's.
|
|
730
749
|
js_methods %w[cloneNode]
|
|
731
750
|
def __js_call__(method, args)
|
|
732
751
|
case method
|
|
733
752
|
when "cloneNode"
|
|
734
|
-
|
|
753
|
+
clone_node(args.first)
|
|
735
754
|
else
|
|
736
755
|
super
|
|
737
756
|
end
|
|
@@ -744,6 +763,10 @@ module Dommy
|
|
|
744
763
|
def node_type
|
|
745
764
|
4
|
|
746
765
|
end
|
|
766
|
+
|
|
767
|
+
def clone_node(_deep = false)
|
|
768
|
+
@document.create_cdata_section(@__node__.content)
|
|
769
|
+
end
|
|
747
770
|
end
|
|
748
771
|
|
|
749
772
|
class CommentNode < CharacterDataNode
|
|
@@ -751,12 +774,16 @@ module Dommy
|
|
|
751
774
|
8
|
|
752
775
|
end
|
|
753
776
|
|
|
777
|
+
def clone_node(_deep = false)
|
|
778
|
+
@document.create_comment(@__node__.content)
|
|
779
|
+
end
|
|
780
|
+
|
|
754
781
|
# Own __js_call__ methods, on top of CharacterDataNode's.
|
|
755
782
|
js_methods %w[cloneNode]
|
|
756
783
|
def __js_call__(method, args)
|
|
757
784
|
case method
|
|
758
785
|
when "cloneNode"
|
|
759
|
-
|
|
786
|
+
clone_node(args.first)
|
|
760
787
|
else
|
|
761
788
|
super
|
|
762
789
|
end
|
|
@@ -791,12 +818,16 @@ module Dommy
|
|
|
791
818
|
end
|
|
792
819
|
end
|
|
793
820
|
|
|
821
|
+
def clone_node(_deep = false)
|
|
822
|
+
@document.create_processing_instruction(@__node__.target, @__node__.content)
|
|
823
|
+
end
|
|
824
|
+
|
|
794
825
|
# Own __js_call__ methods, on top of CharacterDataNode's.
|
|
795
826
|
js_methods %w[cloneNode]
|
|
796
827
|
def __js_call__(method, args)
|
|
797
828
|
case method
|
|
798
829
|
when "cloneNode"
|
|
799
|
-
|
|
830
|
+
clone_node(args.first)
|
|
800
831
|
else
|
|
801
832
|
super
|
|
802
833
|
end
|
|
@@ -1688,7 +1719,7 @@ module Dommy
|
|
|
1688
1719
|
# inside a shadow tree, returns that ShadowRoot. Otherwise walks
|
|
1689
1720
|
# until we hit the Nokogiri Document (then returns the Document).
|
|
1690
1721
|
def root_node(options = nil)
|
|
1691
|
-
composed =
|
|
1722
|
+
composed = Node.composed_option?(options)
|
|
1692
1723
|
sr = @document.__internal_shadow_root_containing__(@__node__)
|
|
1693
1724
|
if sr
|
|
1694
1725
|
# Default: the shadow root is the root. `composed: true` is
|
|
@@ -1786,8 +1817,24 @@ module Dommy
|
|
|
1786
1817
|
attributes.set_named_item(attr)
|
|
1787
1818
|
end
|
|
1788
1819
|
|
|
1820
|
+
# setAttributeNodeNS is defined as the very same steps as setAttributeNode
|
|
1821
|
+
# ("set an attribute"): the namespace is the Attr's own, so there is nothing
|
|
1822
|
+
# left for the NS form to do differently. The JS bridge already routed it
|
|
1823
|
+
# here; this is the Ruby caller's way in.
|
|
1824
|
+
def set_attribute_node_ns(attr)
|
|
1825
|
+
set_attribute_node(attr)
|
|
1826
|
+
end
|
|
1827
|
+
|
|
1828
|
+
# removeAttributeNode step 1: "If this's attribute list does not contain
|
|
1829
|
+
# attr, throw a NotFoundError." What counts is the Attr itself, not its
|
|
1830
|
+
# name — one that belongs to another element, or to none, is not in this
|
|
1831
|
+
# list even when this element has an attribute of the same name.
|
|
1789
1832
|
def remove_attribute_node(attr)
|
|
1790
|
-
|
|
1833
|
+
owner = attr.owner_element if attr.respond_to?(:owner_element)
|
|
1834
|
+
unless owner.equal?(self)
|
|
1835
|
+
raise DOMException::NotFoundError,
|
|
1836
|
+
"the attribute #{attr.respond_to?(:name) ? attr.name.inspect : attr.inspect} is not this element's"
|
|
1837
|
+
end
|
|
1791
1838
|
|
|
1792
1839
|
attributes.remove_named_item(attr.name)
|
|
1793
1840
|
end
|
|
@@ -1982,7 +2029,7 @@ module Dommy
|
|
|
1982
2029
|
raise DOMException::NotSupportedError, "<#{tag}> cannot host a shadow root"
|
|
1983
2030
|
end
|
|
1984
2031
|
|
|
1985
|
-
raise DOMException::NotSupportedError, "Shadow root already attached" if
|
|
2032
|
+
raise DOMException::NotSupportedError, "Shadow root already attached" if __internal_shadow_root__
|
|
1986
2033
|
|
|
1987
2034
|
opts = options.is_a?(Hash) ? options : {}
|
|
1988
2035
|
mode_raw = opts.key?("mode") ? opts["mode"] : opts[:mode]
|
|
@@ -2007,16 +2054,18 @@ module Dommy
|
|
|
2007
2054
|
# `el.shadowRoot` — returns the attached ShadowRoot only when
|
|
2008
2055
|
# mode is "open"; closed shadows are hidden from external code.
|
|
2009
2056
|
def shadow_root
|
|
2010
|
-
|
|
2011
|
-
return nil if
|
|
2057
|
+
root = __internal_shadow_root__
|
|
2058
|
+
return nil if root.nil? || root.mode == "closed"
|
|
2012
2059
|
|
|
2013
|
-
|
|
2060
|
+
root
|
|
2014
2061
|
end
|
|
2015
2062
|
|
|
2016
2063
|
# Internal — gives access to the shadow root regardless of mode.
|
|
2017
|
-
# Used by event composition / `composedPath()`.
|
|
2064
|
+
# Used by event composition / `composedPath()`. The document's registry
|
|
2065
|
+
# answers for a wrapper that is not the one attachShadow ran on: a custom
|
|
2066
|
+
# element upgrade replaces the host's wrapper, not its shadow root.
|
|
2018
2067
|
def __internal_shadow_root__
|
|
2019
|
-
@__shadow_root
|
|
2068
|
+
@__shadow_root ||= @document.__internal_shadow_root_for_host__(@__node__)
|
|
2020
2069
|
end
|
|
2021
2070
|
|
|
2022
2071
|
# `el.insertAdjacentElement(position, element)` — DOM spec positions:
|
|
@@ -2574,9 +2623,8 @@ module Dommy
|
|
|
2574
2623
|
# so `a.href = "http://example.org/?ä"` reads back percent-encoded — which is
|
|
2575
2624
|
# what the URL parser produces and what `URI.join` does not.
|
|
2576
2625
|
def resolve_url(raw)
|
|
2577
|
-
|
|
2578
|
-
base =
|
|
2579
|
-
base = nil if base.to_s.empty?
|
|
2626
|
+
base = @document.base_uri.to_s
|
|
2627
|
+
base = nil if base.empty?
|
|
2580
2628
|
Internal::UrlParser.serialize(Internal::UrlParser.parse(raw.to_s, base))
|
|
2581
2629
|
rescue Internal::UrlParser::Failure
|
|
2582
2630
|
raw.to_s
|
|
@@ -3261,7 +3309,7 @@ module Dommy
|
|
|
3261
3309
|
end
|
|
3262
3310
|
|
|
3263
3311
|
def insert_before(child, reference)
|
|
3264
|
-
|
|
3312
|
+
Internal::WebIDL.node!(child)
|
|
3265
3313
|
# WHATWG pre-insert validates the reference the CALLER gave (step 1), and
|
|
3266
3314
|
# only then, in step 3, replaces it with the node's next sibling when it is
|
|
3267
3315
|
# the node being inserted — so "insert x before x" doesn't move x. Doing
|
|
@@ -3299,7 +3347,7 @@ module Dommy
|
|
|
3299
3347
|
end
|
|
3300
3348
|
|
|
3301
3349
|
def remove_child(child)
|
|
3302
|
-
|
|
3350
|
+
Internal::WebIDL.node!(child)
|
|
3303
3351
|
node = unwrap_dom_node(child)
|
|
3304
3352
|
unless node&.parent == @__node__
|
|
3305
3353
|
raise DOMException::NotFoundError, "node is not a child of this element"
|
|
@@ -3315,8 +3363,8 @@ module Dommy
|
|
|
3315
3363
|
# MutationObserver of both changes in one record so observers
|
|
3316
3364
|
# see the swap atomically.
|
|
3317
3365
|
def replace_child(new_child, old_child)
|
|
3318
|
-
|
|
3319
|
-
|
|
3366
|
+
Internal::WebIDL.node!(new_child)
|
|
3367
|
+
Internal::WebIDL.node!(old_child)
|
|
3320
3368
|
# replaceChild shares the pre-insertion checks (ancestor, node type,
|
|
3321
3369
|
# doctype placement); the reference child here is old_child, so step 3
|
|
3322
3370
|
# also enforces that it is actually a child (NotFoundError otherwise).
|