dommy 0.11.0 → 0.12.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/custom_elements.rb +22 -16
- data/lib/dommy/document.rb +236 -42
- data/lib/dommy/element.rb +69 -20
- data/lib/dommy/event.rb +6 -3
- data/lib/dommy/internal/child_node.rb +1 -5
- 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/web_idl.rb +57 -0
- data/lib/dommy/js/custom_element_bridge.rb +10 -0
- data/lib/dommy/js/dom_interfaces.rb +14 -4
- data/lib/dommy/js/handle_table.rb +15 -0
- data/lib/dommy/js/host_bridge.rb +14 -0
- data/lib/dommy/js/host_runtime.js +79 -13
- data/lib/dommy/js/marshaller.rb +1 -0
- data/lib/dommy/node.rb +19 -3
- data/lib/dommy/range.rb +116 -183
- 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/svg_elements.rb +45 -0
- data/lib/dommy/tree_walker.rb +9 -17
- 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 +2 -1
- data/lib/dommy.rb +3 -0
- metadata +13 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6306b86751026b1b48072c497fd34cd008c5b0bfe1838ae62ab3896d89a2aa3f
|
|
4
|
+
data.tar.gz: 817d56deed527b5f2718242937bb1a30a7d22738ca8f09276750505cf9368b5a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bfce9a98c80d522db534045b6ee378d0bdfb8e1f03646173e4fc74fd5e8ed46e35e723fe4ce5f7828cdab9d71b6e567c2022ad63d01f2f70af8a0baf0acd34cf
|
|
7
|
+
data.tar.gz: 6b0aa54054201df72970198f18450268d53cbe434b3da15bd9b49cd68eb98b9d94ae1acf80d40b668706052385fa8597e4580b683c472760ee970a10ee13d2f4
|
|
@@ -43,18 +43,6 @@ module Dommy
|
|
|
43
43
|
node.clone_node(deep)
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
-
# Makiri documents have no node-level clone; re-parsing the serialized
|
|
47
|
-
# document reproduces the full tree. Dispatch on the document kind so an XML
|
|
48
|
-
# document round-trips through the XML parser (case/namespaces/CDATA) and an
|
|
49
|
-
# HTML document through the HTML parser.
|
|
50
|
-
def clone_document(doc)
|
|
51
|
-
if doc.is_a?(::Makiri::XML::Document)
|
|
52
|
-
::Makiri::XML::Document.parse(doc.to_xml)
|
|
53
|
-
else
|
|
54
|
-
::Makiri::HTML::Document.parse(doc.to_html)
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
|
|
58
46
|
# A fresh, empty HTML-backed document (children dropped so it starts with no
|
|
59
47
|
# documentElement). The backing for a shallow clone of an HTML document.
|
|
60
48
|
def empty_document
|
|
@@ -233,8 +221,8 @@ module Dommy
|
|
|
233
221
|
end
|
|
234
222
|
|
|
235
223
|
# Both Makiri document families (HTML and XML) mint a real PI node and
|
|
236
|
-
# serialize it
|
|
237
|
-
#
|
|
224
|
+
# serialize it as `<?target data?>`, so PIs — unlike CDATA — need no
|
|
225
|
+
# HTML-document fallback.
|
|
238
226
|
def create_processing_instruction(target, data, doc)
|
|
239
227
|
doc.create_processing_instruction(target, data)
|
|
240
228
|
end
|
data/lib/dommy/backend.rb
CHANGED
|
@@ -53,11 +53,6 @@ module Dommy
|
|
|
53
53
|
current.clone_node(node, deep: deep)
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
-
# Deep copy of a whole document (DOM cloneNode on the document).
|
|
57
|
-
def clone_document(doc)
|
|
58
|
-
current.clone_document(doc)
|
|
59
|
-
end
|
|
60
|
-
|
|
61
56
|
# A fresh, empty HTML-backed document.
|
|
62
57
|
def empty_document
|
|
63
58
|
current.empty_document
|
|
@@ -86,19 +86,16 @@ module Dommy
|
|
|
86
86
|
promise
|
|
87
87
|
end
|
|
88
88
|
|
|
89
|
-
# Walk `root`'s subtree and re-wrap any nodes whose tag is now
|
|
89
|
+
# Walk `root`'s shadow-including subtree and re-wrap any nodes whose tag is now
|
|
90
90
|
# registered; fires `connectedCallback` for each upgraded node
|
|
91
91
|
# that's currently attached to a document tree.
|
|
92
92
|
def upgrade(root)
|
|
93
93
|
return nil unless root.respond_to?(:__dommy_backend_node__)
|
|
94
94
|
|
|
95
|
-
|
|
96
|
-
next unless nk.element?
|
|
95
|
+
@window.document.__internal_each_shadow_including_element__(root.__dommy_backend_node__) do |nk|
|
|
97
96
|
next unless @definitions.key?(nk.name)
|
|
98
97
|
|
|
99
|
-
|
|
100
|
-
@window.document.__internal_reset_wrapper__(nk)
|
|
101
|
-
wrapped = @window.document.wrap_node(nk)
|
|
98
|
+
wrapped = rewrap_for_upgrade(@window.document, nk)
|
|
102
99
|
next unless wrapped
|
|
103
100
|
|
|
104
101
|
replay_observed_attributes(wrapped)
|
|
@@ -137,6 +134,22 @@ module Dommy
|
|
|
137
134
|
list&.each { |p| p.fulfill(klass) }
|
|
138
135
|
end
|
|
139
136
|
|
|
137
|
+
# Re-wrap `nk` as its definition's class, or nil when there is nothing to
|
|
138
|
+
# upgrade. An element that is already an instance of the class is left alone:
|
|
139
|
+
# upgrading an element that is already custom does nothing. The new wrapper
|
|
140
|
+
# is told which one it replaces, because a script may hold the element — a
|
|
141
|
+
# JS-defined element upgrades that reference in place.
|
|
142
|
+
def rewrap_for_upgrade(doc, nk)
|
|
143
|
+
previous = doc.__internal_peek_wrapper__(nk)
|
|
144
|
+
klass = @definitions[nk.name]
|
|
145
|
+
return nil if klass.is_a?(Module) && previous.is_a?(klass)
|
|
146
|
+
|
|
147
|
+
doc.__internal_reset_wrapper__(nk)
|
|
148
|
+
wrapped = doc.wrap_node(nk)
|
|
149
|
+
wrapped.__internal_upgraded_from__(previous) if previous && wrapped.respond_to?(:__internal_upgraded_from__)
|
|
150
|
+
wrapped
|
|
151
|
+
end
|
|
152
|
+
|
|
140
153
|
# When define() lands after the matching element is already in
|
|
141
154
|
# the document, those nodes need upgrading: re-wrap them with the
|
|
142
155
|
# new class and fire connectedCallback.
|
|
@@ -145,11 +158,11 @@ module Dommy
|
|
|
145
158
|
# Match by tag name rather than interpolating `name` into a CSS selector:
|
|
146
159
|
# a spec-valid custom element name may contain "." (a CSS class selector
|
|
147
160
|
# char) or other metacharacters, which would corrupt the query.
|
|
148
|
-
|
|
161
|
+
# Shadow trees included, in shadow-including tree order (define step 18).
|
|
162
|
+
doc.__internal_each_shadow_including_element__(doc.backend_doc) do |nk|
|
|
149
163
|
next unless nk.name == name
|
|
150
164
|
|
|
151
|
-
doc
|
|
152
|
-
wrapped = doc.wrap_node(nk)
|
|
165
|
+
wrapped = rewrap_for_upgrade(doc, nk)
|
|
153
166
|
next unless wrapped
|
|
154
167
|
|
|
155
168
|
replay_observed_attributes(wrapped)
|
|
@@ -178,12 +191,5 @@ module Dommy
|
|
|
178
191
|
)
|
|
179
192
|
end
|
|
180
193
|
end
|
|
181
|
-
|
|
182
|
-
def walk_descendants(node, &blk)
|
|
183
|
-
yield node
|
|
184
|
-
return unless node.respond_to?(:children)
|
|
185
|
-
|
|
186
|
-
node.children.each { |c| walk_descendants(c, &blk) }
|
|
187
|
-
end
|
|
188
194
|
end
|
|
189
195
|
end
|
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
|
|
|
@@ -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
|