dommy 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/dommy/attr.rb +4 -5
- data/lib/dommy/backend/makiri_adapter.rb +22 -0
- data/lib/dommy/backend.rb +18 -0
- data/lib/dommy/browser.rb +8 -1
- data/lib/dommy/callable_invoker.rb +13 -5
- data/lib/dommy/css.rb +33 -10
- data/lib/dommy/custom_elements.rb +42 -2
- data/lib/dommy/document.rb +868 -189
- data/lib/dommy/dom_parser.rb +12 -1
- data/lib/dommy/element.rb +444 -273
- data/lib/dommy/event.rb +457 -109
- data/lib/dommy/html_collection.rb +7 -4
- data/lib/dommy/html_elements.rb +763 -182
- data/lib/dommy/interaction/driver.rb +23 -1
- data/lib/dommy/interaction/event_synthesis.rb +4 -7
- data/lib/dommy/interaction/field_interactor.rb +13 -7
- data/lib/dommy/internal/accessible_name.rb +45 -8
- data/lib/dommy/internal/aria_role.rb +5 -3
- data/lib/dommy/internal/child_node.rb +150 -36
- data/lib/dommy/internal/css/cascade.rb +3 -1
- data/lib/dommy/internal/css/parser.rb +97 -14
- data/lib/dommy/internal/css/rule_index.rb +288 -18
- data/lib/dommy/internal/css_priority.rb +33 -0
- data/lib/dommy/internal/dom_matching.rb +1 -1
- data/lib/dommy/internal/leaf_node.rb +51 -0
- data/lib/dommy/internal/mutation_coordinator.rb +134 -27
- data/lib/dommy/internal/node_adopter.rb +190 -0
- data/lib/dommy/internal/node_traversal.rb +10 -0
- data/lib/dommy/internal/node_wrapper_cache.rb +53 -12
- data/lib/dommy/internal/observer_manager.rb +18 -0
- data/lib/dommy/internal/observer_matcher.rb +37 -4
- data/lib/dommy/internal/parent_node.rb +321 -22
- data/lib/dommy/internal/range_text_serializer.rb +5 -4
- data/lib/dommy/internal/selector_index.rb +24 -8
- data/lib/dommy/internal/selector_matcher.rb +132 -16
- data/lib/dommy/internal/template_content_registry.rb +23 -7
- data/lib/dommy/internal/utf16.rb +45 -0
- data/lib/dommy/internal/xml_serialization.rb +18 -5
- data/lib/dommy/js/dom_interfaces.rb +72 -9
- data/lib/dommy/js/host_bridge.rb +65 -15
- data/lib/dommy/js/host_runtime.js +529 -45
- data/lib/dommy/js/marshaller.rb +12 -7
- data/lib/dommy/js/script_boot.rb +30 -29
- data/lib/dommy/mutation_observer.rb +109 -19
- data/lib/dommy/node.rb +27 -0
- data/lib/dommy/parser.rb +14 -0
- data/lib/dommy/promise.rb +4 -4
- data/lib/dommy/range.rb +358 -62
- data/lib/dommy/shadow_root.rb +32 -17
- data/lib/dommy/tree_walker.rb +7 -3
- data/lib/dommy/version.rb +1 -1
- data/lib/dommy/window.rb +74 -4
- data/lib/dommy.rb +4 -1
- metadata +8 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 47c59ce209a6e63313db2b6685126910463abfc26020ec5f24a7b7494b67b10f
|
|
4
|
+
data.tar.gz: 43980f4f5eb7b2c5bfe76e41bd2786e9c8cad571df1eb882e0e6d5107fbe04c6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b2627fa8969554f7ecb7a04ed16c331a77e604b44a9ca9e95412a89e48412744a3dd606892255e93eb56988cfda5214000a15821bbc5eb6ed7d18701740b3262
|
|
7
|
+
data.tar.gz: 632bddbea02093111566d46ba9fc44af35ed17879bf9921c4ad477a138d44b83e2c2761337d34f5bdfa31cc150353a4b7afe47b0cca8d817e79eda95d7b556ad
|
data/lib/dommy/attr.rb
CHANGED
|
@@ -71,11 +71,10 @@ module Dommy
|
|
|
71
71
|
|
|
72
72
|
def value
|
|
73
73
|
if @owner
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
end
|
|
74
|
+
# Both branches go through the namespace-aware read: the backend's
|
|
75
|
+
# `node[name]` indexes by local name, so a null-namespace `b` would read
|
|
76
|
+
# back the value of a prefixed `xml:b` sitting on the same element.
|
|
77
|
+
Backend.get_attribute_ns(@owner.__dommy_backend_node__, @namespace_uri, @local_name).to_s
|
|
79
78
|
else
|
|
80
79
|
@detached_value
|
|
81
80
|
end
|
|
@@ -276,6 +276,12 @@ module Dommy
|
|
|
276
276
|
|
|
277
277
|
node["xmlns:#{prefix}"] = href.to_s
|
|
278
278
|
nil
|
|
279
|
+
rescue ArgumentError
|
|
280
|
+
# DOM validates a qualified name against the Name production, which
|
|
281
|
+
# admits prefixes an XML backend cannot spell as an `xmlns:` attribute
|
|
282
|
+
# ("0:a", ";:a"). The element is still valid — its prefix and namespace
|
|
283
|
+
# live on the wrapper — so the declaration is simply not written.
|
|
284
|
+
nil
|
|
279
285
|
end
|
|
280
286
|
|
|
281
287
|
def namespace_definitions(_node)
|
|
@@ -330,6 +336,22 @@ module Dommy
|
|
|
330
336
|
node.attribute_nodes
|
|
331
337
|
end
|
|
332
338
|
|
|
339
|
+
# Attribute node matching the qualified name exactly. `node[name]` would
|
|
340
|
+
# also answer for a prefixed attribute with that local name.
|
|
341
|
+
#
|
|
342
|
+
# This sits under `getAttribute` / `hasAttribute` / `setAttribute` /
|
|
343
|
+
# `removeAttribute`, so it runs on the hottest path in the library — every
|
|
344
|
+
# CSS match, every reflected IDL attribute — and Makiri does the scan
|
|
345
|
+
# natively (0.9.0). The value-returning spelling is what the two readers
|
|
346
|
+
# that only want the value ask for: it wraps no Attr at all.
|
|
347
|
+
def attr_by_qualified_name(node, qualified_name)
|
|
348
|
+
node.attribute_by_qualified_name(qualified_name.to_s)
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def attr_value_by_qualified_name(node, qualified_name)
|
|
352
|
+
node.attribute_value_by_qualified_name(qualified_name.to_s)
|
|
353
|
+
end
|
|
354
|
+
|
|
333
355
|
# Attribute node matching (namespace, local name) case-sensitively; a
|
|
334
356
|
# null/empty namespace matches a null-namespace attribute.
|
|
335
357
|
def attr_by_ns(node, namespace, local_name)
|
data/lib/dommy/backend.rb
CHANGED
|
@@ -233,6 +233,24 @@ module Dommy
|
|
|
233
233
|
current.attribute_ns_info(attr_node)
|
|
234
234
|
end
|
|
235
235
|
|
|
236
|
+
# The attribute node whose QUALIFIED name is `qualified_name`, or nil.
|
|
237
|
+
#
|
|
238
|
+
# WHATWG's by-name family ("get an attribute by name", `setAttribute`,
|
|
239
|
+
# `removeAttribute`) matches on the qualified name. A lookup by local name
|
|
240
|
+
# confuses `b` with a prefixed `xml:b`, so those paths must come through
|
|
241
|
+
# here rather than through `node[name]`.
|
|
242
|
+
def attr_by_qualified_name(node, qualified_name)
|
|
243
|
+
current.attr_by_qualified_name(node, qualified_name)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# That attribute's VALUE, or nil when there is no such attribute. The same
|
|
247
|
+
# match without an attribute node in hand, for the two readers that only
|
|
248
|
+
# ever wanted the value — `getAttribute` and `hasAttribute` — which run on
|
|
249
|
+
# every CSS match and every reflected IDL attribute.
|
|
250
|
+
def attr_value_by_qualified_name(node, qualified_name)
|
|
251
|
+
current.attr_value_by_qualified_name(node, qualified_name)
|
|
252
|
+
end
|
|
253
|
+
|
|
236
254
|
# The element's attribute nodes (each readable via attribute_ns_info).
|
|
237
255
|
# The single choke point so DOM code doesn't touch parser internals.
|
|
238
256
|
def attribute_nodes(node)
|
data/lib/dommy/browser.rb
CHANGED
|
@@ -279,9 +279,16 @@ module Dommy
|
|
|
279
279
|
runtime.install_wasm_memory_shim if @wasm_memory_shim && runtime.respond_to?(:install_wasm_memory_shim)
|
|
280
280
|
window.globals["__fetch_handler__"] = Resources::FetchHandler.new(@resources) if @resources
|
|
281
281
|
@runtime = runtime
|
|
282
|
+
doc = window.document
|
|
283
|
+
# An `on*` attribute that arrived after boot (a cloned template, an
|
|
284
|
+
# innerHTML fragment) is compiled on first dispatch, which replays the scan.
|
|
285
|
+
# Installed whenever a runtime is attached — an embedder that drives script
|
|
286
|
+
# boot itself (`execute_scripts: false`) still needs inline handlers wired.
|
|
287
|
+
doc.inline_handler_wirer = lambda do
|
|
288
|
+
Js::ScriptBoot.wire_inline_handlers(runtime, on_error: ->(e) { @js_errors << e })
|
|
289
|
+
end
|
|
282
290
|
return unless @execute_scripts
|
|
283
291
|
|
|
284
|
-
doc = window.document
|
|
285
292
|
# Dynamically-inserted `<script src>` (webpack/Vite on-demand chunks)
|
|
286
293
|
# fetch + run through the same resources adapter, after boot.
|
|
287
294
|
doc.external_script_runner = lambda do |element, src|
|
|
@@ -25,15 +25,23 @@ module Dommy
|
|
|
25
25
|
# that order). A JS function listener's `this` must be the event's
|
|
26
26
|
# currentTarget (the node the listener is attached to), so pass it through
|
|
27
27
|
# when the bridge supports an explicit receiver.
|
|
28
|
-
|
|
28
|
+
# `args:` overrides the single-event argument list — the special error
|
|
29
|
+
# event handler (`window.onerror`) is called with (message, filename,
|
|
30
|
+
# lineno, colno, error) instead of the event. An EventListener object's
|
|
31
|
+
# handleEvent always receives the event.
|
|
32
|
+
def invoke_listener(listener, event, current_target = nil, args: nil)
|
|
33
|
+
args ||= [event]
|
|
29
34
|
if listener.respond_to?(:handle_event)
|
|
30
35
|
listener.handle_event(event)
|
|
31
36
|
elsif listener.respond_to?(:call) && !listener.is_a?(Module)
|
|
32
|
-
listener.call(
|
|
33
|
-
elsif listener.respond_to?(:
|
|
34
|
-
listener
|
|
37
|
+
listener.call(*args)
|
|
38
|
+
elsif listener.respond_to?(:__js_invoke__)
|
|
39
|
+
# A JS function listener: `this` is the currentTarget, and a thrown value
|
|
40
|
+
# surfaces (as a ThrowValue) so the dispatch reports it as a window
|
|
41
|
+
# `error` event instead of swallowing it.
|
|
42
|
+
listener.__js_invoke__(args, this: current_target, raising: true)
|
|
35
43
|
elsif listener.respond_to?(:__js_call__)
|
|
36
|
-
listener.__js_call__("call",
|
|
44
|
+
listener.__js_call__("call", args)
|
|
37
45
|
end
|
|
38
46
|
end
|
|
39
47
|
end
|
data/lib/dommy/css.rb
CHANGED
|
@@ -18,7 +18,9 @@ module Dommy
|
|
|
18
18
|
class CSSStyleSheet
|
|
19
19
|
attr_reader :owner_node, :css_rules
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
# A stylesheet constructed by `new CSSStyleSheet()` has no owner node.
|
|
22
|
+
# Element-owned sheets still pass their <style> / <link> node explicitly.
|
|
23
|
+
def initialize(owner_node: nil, href: nil, media: nil, title: nil, type: "text/css", source_text: nil)
|
|
22
24
|
@owner_node = owner_node
|
|
23
25
|
@href = href
|
|
24
26
|
@media = media
|
|
@@ -291,21 +293,27 @@ module Dommy
|
|
|
291
293
|
end
|
|
292
294
|
|
|
293
295
|
def get_property_value(name)
|
|
294
|
-
entry = @props[name
|
|
296
|
+
entry = @props[property_key(name)]
|
|
295
297
|
entry ? entry[:value] : ""
|
|
296
298
|
end
|
|
297
299
|
|
|
298
300
|
def get_property_priority(name)
|
|
299
|
-
entry = @props[name
|
|
301
|
+
entry = @props[property_key(name)]
|
|
300
302
|
entry ? entry[:priority] : ""
|
|
301
303
|
end
|
|
302
304
|
|
|
303
305
|
def set_property(name, value, priority = nil)
|
|
304
|
-
key = name
|
|
306
|
+
key = property_key(name)
|
|
305
307
|
if value.nil? || value.to_s.empty?
|
|
308
|
+
# CSSOM step 3 — an empty value removes the declaration, and it runs
|
|
309
|
+
# before the priority check, so the priority is irrelevant here.
|
|
306
310
|
@props.delete(key)
|
|
307
311
|
else
|
|
308
|
-
|
|
312
|
+
# CSSOM step 4 — an invalid priority abandons the call, leaving the
|
|
313
|
+
# declaration block exactly as it was.
|
|
314
|
+
important = Internal::CssPriority.normalize(priority)
|
|
315
|
+
return nil if important.nil?
|
|
316
|
+
|
|
309
317
|
@props[key] = {value: value.to_s, priority: important}
|
|
310
318
|
end
|
|
311
319
|
flush!
|
|
@@ -313,7 +321,7 @@ module Dommy
|
|
|
313
321
|
end
|
|
314
322
|
|
|
315
323
|
def remove_property(name)
|
|
316
|
-
removed = @props.delete(name
|
|
324
|
+
removed = @props.delete(property_key(name))
|
|
317
325
|
flush!
|
|
318
326
|
removed ? removed[:value] : ""
|
|
319
327
|
end
|
|
@@ -403,6 +411,13 @@ module Dommy
|
|
|
403
411
|
|
|
404
412
|
private
|
|
405
413
|
|
|
414
|
+
# CSSOM normalizes every property name it is handed (see
|
|
415
|
+
# Internal::CSS::Parser.property_name); `css_name` below is the separate
|
|
416
|
+
# camelCase / snake_case → kebab conversion for the IDL accessors.
|
|
417
|
+
def property_key(name)
|
|
418
|
+
Internal::CSS::Parser.property_name(name)
|
|
419
|
+
end
|
|
420
|
+
|
|
406
421
|
def css_name(name)
|
|
407
422
|
str = name.to_s
|
|
408
423
|
return str if str.start_with?("--")
|
|
@@ -414,8 +429,8 @@ module Dommy
|
|
|
414
429
|
# reusing the cascade's declaration parser (same normalization the cascade
|
|
415
430
|
# sees) so reads agree with computed style.
|
|
416
431
|
def parse(body_text)
|
|
417
|
-
Internal::CSS::Parser.
|
|
418
|
-
|
|
432
|
+
Internal::CSS::Parser.parse_block(body_text).transform_values do |decl|
|
|
433
|
+
{value: decl.value, priority: decl.important ? "important" : ""}
|
|
419
434
|
end
|
|
420
435
|
end
|
|
421
436
|
|
|
@@ -458,8 +473,16 @@ module Dommy
|
|
|
458
473
|
@parent_style_sheet = parent_style_sheet
|
|
459
474
|
end
|
|
460
475
|
|
|
476
|
+
# CSSOM serializes a style rule as its selector plus its declaration block,
|
|
477
|
+
# so the text is rebuilt from the parsed declarations rather than echoed
|
|
478
|
+
# back: `#foo { color: red }` reads as `#foo { color: red; }`, and a block
|
|
479
|
+
# whose contents are not declarations at all serializes empty. An at-rule
|
|
480
|
+
# keeps its text as given.
|
|
461
481
|
def css_text
|
|
462
|
-
@css_text
|
|
482
|
+
return @css_text unless style_rule?
|
|
483
|
+
|
|
484
|
+
declarations = style&.css_text.to_s
|
|
485
|
+
declarations.empty? ? "#{selector_text} { }" : "#{selector_text} { #{declarations} }"
|
|
463
486
|
end
|
|
464
487
|
|
|
465
488
|
def css_text=(v)
|
|
@@ -553,7 +576,7 @@ module Dommy
|
|
|
553
576
|
|
|
554
577
|
def __js_get__(key)
|
|
555
578
|
case key
|
|
556
|
-
when "cssText" then
|
|
579
|
+
when "cssText" then css_text
|
|
557
580
|
when "type" then type
|
|
558
581
|
when "selectorText" then selector_text
|
|
559
582
|
when "style" then style
|
|
@@ -34,6 +34,15 @@ module Dommy
|
|
|
34
34
|
@pending_promises = {}
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
# Whether `name` is a valid custom element name. Also consulted by
|
|
38
|
+
# `element_class_for`: an unrecognized HTML-namespace name that is valid here
|
|
39
|
+
# is an *undefined custom element* (interface HTMLElement), not an unknown
|
|
40
|
+
# element (HTMLUnknownElement).
|
|
41
|
+
def self.valid_name?(name)
|
|
42
|
+
key = name.to_s
|
|
43
|
+
key.match?(NAME_RE) && !RESERVED_NAMES.include?(key)
|
|
44
|
+
end
|
|
45
|
+
|
|
37
46
|
def define(name, klass, _options = nil)
|
|
38
47
|
key = name.to_s
|
|
39
48
|
unless key.match?(NAME_RE)
|
|
@@ -90,7 +99,13 @@ module Dommy
|
|
|
90
99
|
# Force re-wrap by clearing the document's cached wrapper.
|
|
91
100
|
@window.document.__internal_reset_wrapper__(nk)
|
|
92
101
|
wrapped = @window.document.wrap_node(nk)
|
|
93
|
-
|
|
102
|
+
next unless wrapped
|
|
103
|
+
|
|
104
|
+
replay_observed_attributes(wrapped)
|
|
105
|
+
# connectedCallback is enqueued only for an element that is actually in
|
|
106
|
+
# a document tree — `customElements.upgrade()` on a detached subtree
|
|
107
|
+
# upgrades it without connecting it.
|
|
108
|
+
@window.document.__internal_notify_connected__(wrapped) if wrapped.is_connected?
|
|
94
109
|
end
|
|
95
110
|
|
|
96
111
|
nil
|
|
@@ -135,7 +150,32 @@ module Dommy
|
|
|
135
150
|
|
|
136
151
|
doc.__internal_reset_wrapper__(nk)
|
|
137
152
|
wrapped = doc.wrap_node(nk)
|
|
138
|
-
|
|
153
|
+
next unless wrapped
|
|
154
|
+
|
|
155
|
+
replay_observed_attributes(wrapped)
|
|
156
|
+
doc.__internal_notify_connected__(wrapped)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# "Upgrade an element" step 6: the element's *existing* attributes are
|
|
161
|
+
# replayed through attributeChangedCallback (oldValue null) before
|
|
162
|
+
# connectedCallback, so a definition registered after the markup was parsed
|
|
163
|
+
# still sees the attributes that were already there.
|
|
164
|
+
def replay_observed_attributes(element)
|
|
165
|
+
klass = element.class
|
|
166
|
+
return unless klass.respond_to?(:observed_attributes)
|
|
167
|
+
|
|
168
|
+
observed = Array(klass.observed_attributes).map { |a| a.to_s.downcase }
|
|
169
|
+
return if observed.empty?
|
|
170
|
+
|
|
171
|
+
element.get_attribute_names.each do |name|
|
|
172
|
+
next unless observed.include?(name.to_s.downcase)
|
|
173
|
+
|
|
174
|
+
# Routed through the coordinator so the callback's arity handling (the
|
|
175
|
+
# optional 4th namespace argument) stays in one place.
|
|
176
|
+
@window.document.__internal_notify_attribute_changed__(
|
|
177
|
+
element, name, nil, element.get_attribute(name)
|
|
178
|
+
)
|
|
139
179
|
end
|
|
140
180
|
end
|
|
141
181
|
|