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
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:
|
|
@@ -3261,7 +3310,7 @@ module Dommy
|
|
|
3261
3310
|
end
|
|
3262
3311
|
|
|
3263
3312
|
def insert_before(child, reference)
|
|
3264
|
-
|
|
3313
|
+
Internal::WebIDL.node!(child)
|
|
3265
3314
|
# WHATWG pre-insert validates the reference the CALLER gave (step 1), and
|
|
3266
3315
|
# only then, in step 3, replaces it with the node's next sibling when it is
|
|
3267
3316
|
# the node being inserted — so "insert x before x" doesn't move x. Doing
|
|
@@ -3299,7 +3348,7 @@ module Dommy
|
|
|
3299
3348
|
end
|
|
3300
3349
|
|
|
3301
3350
|
def remove_child(child)
|
|
3302
|
-
|
|
3351
|
+
Internal::WebIDL.node!(child)
|
|
3303
3352
|
node = unwrap_dom_node(child)
|
|
3304
3353
|
unless node&.parent == @__node__
|
|
3305
3354
|
raise DOMException::NotFoundError, "node is not a child of this element"
|
|
@@ -3315,8 +3364,8 @@ module Dommy
|
|
|
3315
3364
|
# MutationObserver of both changes in one record so observers
|
|
3316
3365
|
# see the swap atomically.
|
|
3317
3366
|
def replace_child(new_child, old_child)
|
|
3318
|
-
|
|
3319
|
-
|
|
3367
|
+
Internal::WebIDL.node!(new_child)
|
|
3368
|
+
Internal::WebIDL.node!(old_child)
|
|
3320
3369
|
# replaceChild shares the pre-insertion checks (ancestor, node type,
|
|
3321
3370
|
# doctype placement); the reference child here is old_child, so step 3
|
|
3322
3371
|
# also enforces that it is actually a child (NotFoundError otherwise).
|
data/lib/dommy/event.rb
CHANGED
|
@@ -377,8 +377,11 @@ module Dommy
|
|
|
377
377
|
node.is_a?(ShadowRoot) && node.mode == "closed"
|
|
378
378
|
end
|
|
379
379
|
|
|
380
|
+
# Every node answers getRootNode — a CharacterData node and a Document as
|
|
381
|
+
# much as an Element, whose override also finds its shadow root. A Window
|
|
382
|
+
# has no root.
|
|
380
383
|
def root_of(node)
|
|
381
|
-
node.respond_to?(:
|
|
384
|
+
node.respond_to?(:get_root_node) ? node.get_root_node : nil
|
|
382
385
|
end
|
|
383
386
|
|
|
384
387
|
# `phase` is :capture (capture listeners), :bubble (non-capture), or :both
|
|
@@ -1373,7 +1376,7 @@ module Dommy
|
|
|
1373
1376
|
# `InputEvent` — fired by `input` / `beforeinput`. Carries `data`
|
|
1374
1377
|
# (the inserted text, if any) and `inputType` (insertText,
|
|
1375
1378
|
# deleteContentBackward, etc.).
|
|
1376
|
-
class InputEvent <
|
|
1379
|
+
class InputEvent < UIEvent
|
|
1377
1380
|
def initialize(type, init = nil)
|
|
1378
1381
|
super
|
|
1379
1382
|
@data = read_init(init, "data")
|
|
@@ -1581,7 +1584,7 @@ module Dommy
|
|
|
1581
1584
|
# touchcancel. Carries three TouchLists plus modifier keys.
|
|
1582
1585
|
#
|
|
1583
1586
|
# Spec: https://w3c.github.io/touch-events/#touchevent-interface
|
|
1584
|
-
class TouchEvent <
|
|
1587
|
+
class TouchEvent < UIEvent
|
|
1585
1588
|
def initialize(type, init = nil)
|
|
1586
1589
|
super
|
|
1587
1590
|
@touches = TouchList.new(Array(read_init(init, "touches") || []))
|
|
@@ -137,11 +137,7 @@ module Dommy
|
|
|
137
137
|
def validate_insert_before_ref!(args)
|
|
138
138
|
raise Bridge::TypeError, "insertBefore requires 2 arguments." if args.length < 2
|
|
139
139
|
|
|
140
|
-
|
|
141
|
-
return if ref.nil? || (defined?(Bridge::UNDEFINED) && ref.equal?(Bridge::UNDEFINED))
|
|
142
|
-
return if ref.is_a?(Dommy::Node)
|
|
143
|
-
|
|
144
|
-
raise Bridge::TypeError, "The reference child is not a Node."
|
|
140
|
+
WebIDL.nullable_node!(args[1])
|
|
145
141
|
end
|
|
146
142
|
|
|
147
143
|
private
|
|
@@ -26,23 +26,17 @@ module Dommy
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
def remove_child(node)
|
|
29
|
-
|
|
29
|
+
WebIDL.node!(node)
|
|
30
30
|
raise DOMException::NotFoundError, "the node to be removed is not a child of this node"
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
private
|
|
34
34
|
|
|
35
35
|
def reject_leaf_insertion!(node)
|
|
36
|
-
|
|
36
|
+
WebIDL.node!(node)
|
|
37
37
|
raise DOMException::HierarchyRequestError, leaf_insertion_message
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
# WebIDL coerces the Node argument first: a null / non-Node value is a
|
|
41
|
-
# TypeError before any DOM step runs.
|
|
42
|
-
def coerce_leaf_node_argument!(value)
|
|
43
|
-
raise Bridge::TypeError, "Argument is not a Node." unless value.is_a?(Dommy::Node)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
40
|
def leaf_insertion_message
|
|
47
41
|
"this node type does not support children"
|
|
48
42
|
end
|
|
@@ -37,20 +37,24 @@ module Dommy
|
|
|
37
37
|
nil
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
#
|
|
40
|
+
# Connected callbacks, connected scripts and blank-iframe loads for every
|
|
41
|
+
# element among the subtree's shadow-including inclusive descendants, in
|
|
42
|
+
# shadow-including tree order — so the custom elements in an inserted
|
|
43
|
+
# host's shadow tree connect too.
|
|
41
44
|
def notify_connected_subtree(nk)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if wrapped
|
|
47
|
-
notify_connected(wrapped)
|
|
48
|
-
run_connected_script(wrapped)
|
|
49
|
-
fire_blank_iframe_load(wrapped)
|
|
50
|
-
end
|
|
45
|
+
each_shadow_including_element(nk) do |element|
|
|
46
|
+
notify_connected(element)
|
|
47
|
+
run_connected_script(element)
|
|
48
|
+
fire_blank_iframe_load(element)
|
|
51
49
|
end
|
|
50
|
+
end
|
|
52
51
|
|
|
53
|
-
|
|
52
|
+
# The wrapped elements of the document's shadow-including walk.
|
|
53
|
+
def each_shadow_including_element(nk)
|
|
54
|
+
@document.__internal_each_shadow_including_element__(nk) do |element_node|
|
|
55
|
+
wrapped = @document.wrap_node(element_node)
|
|
56
|
+
yield wrapped if wrapped
|
|
57
|
+
end
|
|
54
58
|
end
|
|
55
59
|
|
|
56
60
|
# A srcless ("blank"/about:blank) `<iframe>` connected to the document gets
|
|
@@ -133,15 +137,32 @@ module Dommy
|
|
|
133
137
|
scheduler ? scheduler.queue_microtask(run) : run.call
|
|
134
138
|
end
|
|
135
139
|
|
|
136
|
-
|
|
137
|
-
|
|
140
|
+
# WHATWG "move": each custom element among the moved node's
|
|
141
|
+
# shadow-including inclusive descendants, in shadow-including tree order,
|
|
142
|
+
# gets connectedMoveCallback. The caller checks that the new parent is
|
|
143
|
+
# connected.
|
|
144
|
+
def notify_moved_subtree(nk)
|
|
145
|
+
each_shadow_including_element(nk) { |element| notify_moved(element) }
|
|
146
|
+
end
|
|
138
147
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
148
|
+
# HTML "enqueue a custom element callback reaction": a definition without
|
|
149
|
+
# connectedMoveCallback runs disconnectedCallback and then connectedCallback
|
|
150
|
+
# in its place. (A JS-defined element always answers the Ruby method; the
|
|
151
|
+
# bridge falls back on the JS side.)
|
|
152
|
+
def notify_moved(element)
|
|
153
|
+
if element.respond_to?(:connected_move_callback)
|
|
154
|
+
element.connected_move_callback
|
|
155
|
+
else
|
|
156
|
+
notify_disconnected(element)
|
|
157
|
+
notify_connected(element)
|
|
142
158
|
end
|
|
159
|
+
rescue StandardError
|
|
160
|
+
nil
|
|
161
|
+
end
|
|
143
162
|
|
|
144
|
-
|
|
163
|
+
# Disconnected callbacks, over the same shadow-including walk.
|
|
164
|
+
def notify_disconnected_subtree(nk)
|
|
165
|
+
each_shadow_including_element(nk) { |element| notify_disconnected(element) }
|
|
145
166
|
end
|
|
146
167
|
|
|
147
168
|
def notify_attribute_changed(element, name, old_value, new_value, namespace = nil)
|
|
@@ -251,12 +272,18 @@ module Dommy
|
|
|
251
272
|
end
|
|
252
273
|
|
|
253
274
|
# Fire MutationObserver childList records
|
|
275
|
+
# `moving:` marks the records of a move (moveBefore). A move runs neither
|
|
276
|
+
# the insertion nor the removing steps, so the connected / disconnected
|
|
277
|
+
# walk below — lifecycle callbacks, script execution, blank-iframe load —
|
|
278
|
+
# is skipped for it; its custom element reactions are the move's own
|
|
279
|
+
# (notify_moved_subtree).
|
|
254
280
|
def notify_child_list_mutation(
|
|
255
281
|
target_node:,
|
|
256
282
|
added_nodes:,
|
|
257
283
|
removed_nodes:,
|
|
258
284
|
previous_sibling: nil,
|
|
259
|
-
next_sibling: nil
|
|
285
|
+
next_sibling: nil,
|
|
286
|
+
moving: false
|
|
260
287
|
)
|
|
261
288
|
@document.__internal_note_tree_mutation__
|
|
262
289
|
target = @document.wrap_node(target_node)
|
|
@@ -269,7 +296,7 @@ module Dommy
|
|
|
269
296
|
# connectedCallback fires only when connected). So skip the O(subtree)
|
|
270
297
|
# walk for mutations within a still-detached tree — the common case
|
|
271
298
|
# during bulk DOM construction, where nothing in the walk can fire.
|
|
272
|
-
if !target.respond_to?(:is_connected?) || target.is_connected?
|
|
299
|
+
if !moving && (!target.respond_to?(:is_connected?) || target.is_connected?)
|
|
273
300
|
added_nodes.each { |nk| notify_connected_subtree(nk) }
|
|
274
301
|
removed_nodes.each { |nk| notify_disconnected_subtree(nk) }
|
|
275
302
|
end
|
|
@@ -278,8 +278,15 @@ module Dommy
|
|
|
278
278
|
end
|
|
279
279
|
end
|
|
280
280
|
|
|
281
|
-
# Clear cached wrapper (used by customElements.define for upgrades)
|
|
281
|
+
# Clear cached wrapper (used by customElements.define for upgrades).
|
|
282
|
+
# An upgrade replaces a node's wrapper without mutating the tree, so
|
|
283
|
+
# nothing bumps `dom_generation` and the query cache would keep handing
|
|
284
|
+
# out the wrapper this just retired — a `querySelector` after the upgrade
|
|
285
|
+
# would answer with the element's pre-upgrade self. Drop the memoized
|
|
286
|
+
# results too. Upgrades are rare (a handful per page), so clearing the
|
|
287
|
+
# whole cache costs less than tracking which selectors matched the node.
|
|
282
288
|
def reset_wrapper(nokogiri_node)
|
|
289
|
+
@query_cache.clear
|
|
283
290
|
@wrappers.delete(identity_key(nokogiri_node))
|
|
284
291
|
end
|
|
285
292
|
|
|
@@ -420,6 +427,7 @@ module Dommy
|
|
|
420
427
|
ruby_custom = custom_klass if custom_klass.is_a?(::Class)
|
|
421
428
|
klass = ruby_custom || Dommy.element_class_for(local_name || node.name, ns)
|
|
422
429
|
instance = klass.new(@document, node)
|
|
430
|
+
adjust_svg_tag_name(instance, node, ns) if local_name.nil?
|
|
423
431
|
|
|
424
432
|
@wrappers[identity_key(node)] = instance
|
|
425
433
|
|
|
@@ -434,6 +442,20 @@ module Dommy
|
|
|
434
442
|
instance
|
|
435
443
|
end
|
|
436
444
|
|
|
445
|
+
# HTML's "adjust SVG tag name" (§13.2.6.5), the parser step that gives
|
|
446
|
+
# `<feMerge>` its camel case back after the tokenizer lower-cased it. The
|
|
447
|
+
# backend keeps the lower-case name, so — exactly as for the case
|
|
448
|
+
# createElementNS preserves — the adjusted name lives on the wrapper.
|
|
449
|
+
# Only names the parser produced are adjusted: createElementNS says what
|
|
450
|
+
# the local name is and passes it in, and an XML document is parsed
|
|
451
|
+
# verbatim to begin with.
|
|
452
|
+
def adjust_svg_tag_name(instance, node, namespace)
|
|
453
|
+
return unless namespace == Element::SVG_NAMESPACE && @document.html_document?
|
|
454
|
+
|
|
455
|
+
adjusted = Dommy::SVG_ADJUSTED_TAG_NAMES[node.name]
|
|
456
|
+
instance.__internal_set_namespace__(namespace, nil, adjusted, adjusted) if adjusted
|
|
457
|
+
end
|
|
458
|
+
|
|
437
459
|
def custom_element_class_for(tag_name)
|
|
438
460
|
# Custom elements are registered on window, not document.
|
|
439
461
|
# Access via default_view if available.
|
|
@@ -19,7 +19,7 @@ module Dommy
|
|
|
19
19
|
# `appendChild(child)` — detach the node(s) from any current parent
|
|
20
20
|
# and append to the end of this node's child list.
|
|
21
21
|
def append_child(child)
|
|
22
|
-
|
|
22
|
+
WebIDL.node!(child)
|
|
23
23
|
ensure_pre_insertion_validity!(child, nil)
|
|
24
24
|
# An append has a null reference child, so insert step 5 shifts nothing;
|
|
25
25
|
# convert_for_insert still routes through it so every insertion site
|
|
@@ -120,7 +120,8 @@ module Dommy
|
|
|
120
120
|
# primitive underneath it.
|
|
121
121
|
#
|
|
122
122
|
# A move is NOT remove + insert. It runs neither the removing steps nor
|
|
123
|
-
# the insertion steps
|
|
123
|
+
# the insertion steps: a custom element gets connectedMoveCallback (or, with
|
|
124
|
+
# none defined, disconnectedCallback then connectedCallback) instead, it
|
|
124
125
|
# never adopts — step 1 requires the same shadow-including root, so the
|
|
125
126
|
# node document cannot change — and it carries its own validity checks
|
|
126
127
|
# instead of "ensure pre-insertion validity". What it does share is the
|
|
@@ -129,7 +130,8 @@ module Dommy
|
|
|
129
130
|
#
|
|
130
131
|
# Spec: https://dom.spec.whatwg.org/#dom-parentnode-movebefore
|
|
131
132
|
def move_before(node, child = nil)
|
|
132
|
-
|
|
133
|
+
WebIDL.node!(node)
|
|
134
|
+
WebIDL.nullable_node!(child)
|
|
133
135
|
bn = insertion_backend_node(node)
|
|
134
136
|
ref_bn = insertion_backend_node(child)
|
|
135
137
|
# moveBefore step 2: a reference child that IS the node moves out of the
|
|
@@ -149,7 +151,7 @@ module Dommy
|
|
|
149
151
|
# Steps 10-11 and 14. detach_node runs the live range and NodeIterator
|
|
150
152
|
# pre-remove steps and then unlinks, without queuing a record — the move
|
|
151
153
|
# queues its own pair at the end (steps 23-24).
|
|
152
|
-
@document.detach_node(bn)
|
|
154
|
+
@document.detach_node(bn, moving: true)
|
|
153
155
|
|
|
154
156
|
ref_bn = nil if ref_bn && ref_bn.parent != @__node__
|
|
155
157
|
# Step 16 — measured after the removal, which step 14 has already done.
|
|
@@ -160,69 +162,16 @@ module Dommy
|
|
|
160
162
|
|
|
161
163
|
# Steps 23-24: one record for the old parent, one for the new.
|
|
162
164
|
notify_move_records(bn, old_parent, old_previous, old_next, new_previous, ref_bn)
|
|
165
|
+
# Step 19.3's custom element reactions, run as the call returns — only
|
|
166
|
+
# when the new parent is connected.
|
|
167
|
+
@document.__internal_notify_moved_subtree__(bn) if node.get_root_node({ "composed" => true }).is_a?(Dommy::Document)
|
|
163
168
|
node
|
|
164
169
|
end
|
|
165
170
|
|
|
166
|
-
# Node
|
|
167
|
-
#
|
|
168
|
-
# to it survives) and drop empty Text nodes. Recurses the whole subtree,
|
|
169
|
-
# so it works for Element, DocumentFragment, and ShadowRoot alike.
|
|
170
|
-
#
|
|
171
|
-
# A run is merged one sibling at a time: append the sibling's data to the
|
|
172
|
-
# survivor (a characterData record), hand its live range boundaries over,
|
|
173
|
-
# remove it (a childList record), then the next. Read literally, the spec
|
|
174
|
-
# concatenates every sibling's data first (steps 3-4, one "replace data")
|
|
175
|
-
# and removes them afterwards (step 7), which would queue ONE
|
|
176
|
-
# characterData record per run. Every shipping engine merges pairwise
|
|
177
|
-
# instead — Blink, WebCore and Gecko all answer [characterData, childList,
|
|
178
|
-
# characterData, childList, …] for a run of four text nodes, confirmed by
|
|
179
|
-
# running the same script in Chromium 141, WebKitGTK 2.52.6 and Firefox —
|
|
180
|
-
# and the WPT suite fixes only the childList side, so the records follow
|
|
181
|
-
# the engines. The tree and every live range boundary end up exactly where
|
|
182
|
-
# the spec's steps put them: a boundary in a later sibling (or on the
|
|
183
|
-
# parent, pointing at one) is shifted down by each earlier removal and
|
|
184
|
-
# then handed over at the survivor's length of that moment, which is the
|
|
185
|
-
# same offset the batch steps compute up front.
|
|
186
|
-
# https://github.com/takahashim/dommy/issues/24
|
|
171
|
+
# Node.normalize(). The algorithm lives on the Document, which is a
|
|
172
|
+
# receiver too.
|
|
187
173
|
def normalize
|
|
188
|
-
|
|
189
|
-
@__node__.traverse { |node| text_nodes << node if node.respond_to?(:text?) && node.text? }
|
|
190
|
-
|
|
191
|
-
text_nodes.each do |node|
|
|
192
|
-
next unless node.parent # already removed as part of an earlier run
|
|
193
|
-
|
|
194
|
-
if node.content.to_s.empty?
|
|
195
|
-
@document.remove_node_with_notify(node)
|
|
196
|
-
next
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
sib = node.next
|
|
200
|
-
while sib.respond_to?(:text?) && sib.text?
|
|
201
|
-
following = sib.next
|
|
202
|
-
data = sib.content.to_s
|
|
203
|
-
# The offset the sibling's data lands at inside the survivor — the
|
|
204
|
-
# length of what it already holds, measured before the append.
|
|
205
|
-
length = @document.wrap_node(node).length
|
|
206
|
-
# An empty sibling has nothing to append: the engines skip the data
|
|
207
|
-
# step for it (Gecko checks the length, Blink and WebCore behave the
|
|
208
|
-
# same), so it is removed without a characterData record. Its range
|
|
209
|
-
# boundaries still move to the survivor's join.
|
|
210
|
-
unless data.empty?
|
|
211
|
-
old = node.content.to_s
|
|
212
|
-
node.content = old + data
|
|
213
|
-
@document.notify_character_data_mutation(target_node: node, old_value: old)
|
|
214
|
-
end
|
|
215
|
-
# WHATWG normalize() step 6: the merged-away sibling hands its live
|
|
216
|
-
# range boundaries to the survivor at that offset BEFORE it is
|
|
217
|
-
# removed, or the plain removing steps would strand them on the
|
|
218
|
-
# parent.
|
|
219
|
-
@document.__internal_ranges_normalize_merge__(node, sib, length)
|
|
220
|
-
@document.remove_node_with_notify(sib)
|
|
221
|
-
sib = following
|
|
222
|
-
end
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
nil
|
|
174
|
+
@document.__internal_normalize__(@__node__)
|
|
226
175
|
end
|
|
227
176
|
|
|
228
177
|
private
|
|
@@ -342,12 +291,12 @@ module Dommy
|
|
|
342
291
|
if old_parent
|
|
343
292
|
@document.notify_child_list_mutation(
|
|
344
293
|
target_node: old_parent, added_nodes: [], removed_nodes: [bn],
|
|
345
|
-
previous_sibling: wrap.call(old_previous), next_sibling: wrap.call(old_next)
|
|
294
|
+
previous_sibling: wrap.call(old_previous), next_sibling: wrap.call(old_next), moving: true
|
|
346
295
|
)
|
|
347
296
|
end
|
|
348
297
|
@document.notify_child_list_mutation(
|
|
349
298
|
target_node: @__node__, added_nodes: [bn], removed_nodes: [],
|
|
350
|
-
previous_sibling: wrap.call(new_previous), next_sibling: wrap.call(ref_bn)
|
|
299
|
+
previous_sibling: wrap.call(new_previous), next_sibling: wrap.call(ref_bn), moving: true
|
|
351
300
|
)
|
|
352
301
|
end
|
|
353
302
|
|
|
@@ -406,15 +355,6 @@ module Dommy
|
|
|
406
355
|
false
|
|
407
356
|
end
|
|
408
357
|
|
|
409
|
-
# WebIDL coercion for an `appendChild`/`insertBefore`/`replaceChild`
|
|
410
|
-
# argument typed `Node`: a null / undefined / non-Node value is a
|
|
411
|
-
# TypeError before any DOM step runs.
|
|
412
|
-
def coerce_node_argument!(value)
|
|
413
|
-
return value if value.is_a?(Dommy::Node)
|
|
414
|
-
|
|
415
|
-
raise Bridge::TypeError, "Argument is not a Node."
|
|
416
|
-
end
|
|
417
|
-
|
|
418
358
|
# WHATWG "ensure pre-insertion validity" for an element-like parent
|
|
419
359
|
# (Element / DocumentFragment / ShadowRoot — never a Document, so the
|
|
420
360
|
# document-only constraints in step 6 don't apply here). Steps run in
|
|
@@ -65,8 +65,9 @@ module Dommy
|
|
|
65
65
|
end
|
|
66
66
|
end
|
|
67
67
|
|
|
68
|
+
# A Text node in the spec's sense, which a CDATASection is too.
|
|
68
69
|
def text_node?(node)
|
|
69
|
-
node.respond_to?(:node_type) && node.node_type
|
|
70
|
+
node.respond_to?(:node_type) && [3, 4].include?(node.node_type)
|
|
70
71
|
end
|
|
71
72
|
end
|
|
72
73
|
end
|
|
@@ -69,6 +69,23 @@ module Dommy
|
|
|
69
69
|
|
|
70
70
|
TypeSelector = Struct.new(:namespace, :name) do
|
|
71
71
|
def specificity = TYPE
|
|
72
|
+
|
|
73
|
+
# HTML matches a type selector against an HTML element by lowercasing
|
|
74
|
+
# the SELECTOR (see SelectorMatcher#matches_type?). Computed once per
|
|
75
|
+
# parsed selector — the matcher asks per element per query, and the AST
|
|
76
|
+
# it asks is the cached one.
|
|
77
|
+
def ascii_lowercased_name
|
|
78
|
+
@ascii_lowercased_name ||= name.to_s.downcase(:ascii)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# A name with no ASCII uppercase in it — nearly every selector ever
|
|
82
|
+
# written — is its own lowercased form, so HTML's rule and an exact
|
|
83
|
+
# comparison agree and the matcher need not ask what it is looking at.
|
|
84
|
+
def already_ascii_lowercase?
|
|
85
|
+
return @already_ascii_lowercase unless @already_ascii_lowercase.nil?
|
|
86
|
+
|
|
87
|
+
@already_ascii_lowercase = name.to_s == ascii_lowercased_name
|
|
88
|
+
end
|
|
72
89
|
end
|
|
73
90
|
|
|
74
91
|
UniversalSelector = Struct.new(:namespace) do
|