dommy 0.7.0 → 0.8.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/animation.rb +9 -1
- data/lib/dommy/attr.rb +192 -39
- data/lib/dommy/backend/nokogiri_adapter.rb +76 -0
- data/lib/dommy/backend/nokolexbor_adapter.rb +37 -0
- data/lib/dommy/backend.rb +46 -0
- data/lib/dommy/blob.rb +28 -9
- data/lib/dommy/bridge/constructor_registry.rb +28 -0
- data/lib/dommy/bridge/methods.rb +57 -0
- data/lib/dommy/bridge.rb +97 -0
- data/lib/dommy/callable_invoker.rb +36 -0
- data/lib/dommy/cookie_store.rb +3 -1
- data/lib/dommy/crypto.rb +7 -1
- data/lib/dommy/css.rb +46 -0
- data/lib/dommy/custom_elements.rb +27 -3
- data/lib/dommy/data_transfer.rb +4 -0
- data/lib/dommy/document.rb +615 -48
- data/lib/dommy/dom_parser.rb +28 -15
- data/lib/dommy/element.rb +999 -471
- data/lib/dommy/event.rb +260 -96
- data/lib/dommy/event_source.rb +6 -2
- data/lib/dommy/fetch.rb +505 -43
- data/lib/dommy/file_reader.rb +11 -3
- data/lib/dommy/form_data.rb +2 -0
- data/lib/dommy/history.rb +43 -8
- data/lib/dommy/html_collection.rb +55 -2
- data/lib/dommy/html_elements.rb +102 -1519
- data/lib/dommy/internal/css_pseudo_handlers.rb +109 -0
- data/lib/dommy/internal/global_functions.rb +26 -0
- data/lib/dommy/internal/idna.rb +16 -7
- data/lib/dommy/internal/ipv4_parser.rb +22 -7
- data/lib/dommy/internal/mutation_coordinator.rb +11 -2
- data/lib/dommy/internal/namespaces.rb +70 -0
- data/lib/dommy/internal/node_equality.rb +86 -0
- data/lib/dommy/internal/node_wrapper_cache.rb +62 -27
- data/lib/dommy/internal/observable_callback.rb +1 -5
- data/lib/dommy/internal/parent_node.rb +126 -0
- data/lib/dommy/internal/reflected_attributes.rb +103 -13
- data/lib/dommy/internal/selector_parser.rb +664 -0
- data/lib/dommy/internal/url_parser.rb +677 -0
- data/lib/dommy/intersection_observer.rb +2 -0
- data/lib/dommy/location.rb +2 -0
- data/lib/dommy/media_query_list.rb +7 -1
- data/lib/dommy/message_channel.rb +32 -2
- data/lib/dommy/mutation_observer.rb +55 -12
- data/lib/dommy/navigator.rb +26 -12
- data/lib/dommy/node.rb +158 -28
- data/lib/dommy/notification.rb +3 -1
- data/lib/dommy/performance.rb +4 -0
- data/lib/dommy/performance_observer.rb +2 -0
- data/lib/dommy/promise.rb +14 -14
- data/lib/dommy/range.rb +74 -5
- data/lib/dommy/resize_observer.rb +2 -0
- data/lib/dommy/scheduler.rb +34 -13
- data/lib/dommy/shadow_root.rb +23 -54
- data/lib/dommy/storage.rb +2 -0
- data/lib/dommy/streams.rb +18 -27
- data/lib/dommy/svg_elements.rb +204 -3606
- data/lib/dommy/text_codec.rb +174 -21
- data/lib/dommy/tree_walker.rb +255 -66
- data/lib/dommy/url.rb +287 -449
- data/lib/dommy/url_pattern.rb +2 -0
- data/lib/dommy/version.rb +1 -1
- data/lib/dommy/web_socket.rb +37 -7
- data/lib/dommy/window.rb +202 -213
- data/lib/dommy/worker.rb +7 -7
- data/lib/dommy/xml_http_request.rb +15 -5
- data/lib/dommy.rb +7 -0
- metadata +12 -3
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
module Internal
|
|
5
|
+
# Shared ParentNode tree-mutation surface for Element, Fragment, and
|
|
6
|
+
# ShadowRoot. Includers must expose `@__node__` (the backing Nokogiri
|
|
7
|
+
# node) and `@document` (the owning Dommy::Document).
|
|
8
|
+
#
|
|
9
|
+
# All child-list mutations funnel through `notify_child_list`, which
|
|
10
|
+
# forwards to MutationCoordinator#notify_child_list_mutation. That
|
|
11
|
+
# coordinator already no-ops on empty added/removed sets and on an
|
|
12
|
+
# unwrappable target, so callers may invoke it unconditionally.
|
|
13
|
+
module ParentNode
|
|
14
|
+
# `appendChild(child)` — detach the node(s) from any current parent
|
|
15
|
+
# and append to the end of this node's child list.
|
|
16
|
+
def append_child(child)
|
|
17
|
+
check_insertion!(child)
|
|
18
|
+
nodes = detach_dom_nodes(child)
|
|
19
|
+
nodes.each { |n| @__node__.add_child(n) }
|
|
20
|
+
notify_child_list(added: nodes)
|
|
21
|
+
child
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# ParentNode#append — mixed Node/String args appended in order.
|
|
25
|
+
def append(*args)
|
|
26
|
+
nodes = args.flat_map { |arg| detach_dom_nodes(arg) }
|
|
27
|
+
nodes.each { |n| @__node__.add_child(n) }
|
|
28
|
+
notify_child_list(added: nodes)
|
|
29
|
+
nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# ParentNode#prepend — insert before the current first child.
|
|
33
|
+
def prepend(*args)
|
|
34
|
+
nodes = args.flat_map { |arg| detach_dom_nodes(arg) }
|
|
35
|
+
anchor = @__node__.children.first
|
|
36
|
+
if anchor
|
|
37
|
+
nodes.reverse_each { |n| anchor.add_previous_sibling(n) }
|
|
38
|
+
else
|
|
39
|
+
nodes.each { |n| @__node__.add_child(n) }
|
|
40
|
+
end
|
|
41
|
+
notify_child_list(added: nodes)
|
|
42
|
+
nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# ParentNode#replaceChildren — remove all existing children, then
|
|
46
|
+
# append the new set. One mutation record carries both sides.
|
|
47
|
+
def replace_children(*args)
|
|
48
|
+
removed = @__node__.children.to_a
|
|
49
|
+
removed.each(&:unlink)
|
|
50
|
+
nodes = args.flat_map { |arg| detach_dom_nodes(arg) }
|
|
51
|
+
nodes.each { |n| @__node__.add_child(n) }
|
|
52
|
+
notify_child_list(added: nodes, removed: removed)
|
|
53
|
+
nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
# Centralized MutationObserver childList notification. Defaults the
|
|
59
|
+
# target to this node; beforebegin/afterend/replaceWith/outerHTML
|
|
60
|
+
# callers pass the parent explicitly. The coordinator filters out
|
|
61
|
+
# empty added/removed sets, so this is always safe to call.
|
|
62
|
+
def notify_child_list(added: [], removed: [], target: @__node__)
|
|
63
|
+
@document.notify_child_list_mutation(
|
|
64
|
+
target_node: target,
|
|
65
|
+
added_nodes: added,
|
|
66
|
+
removed_nodes: removed
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Coerce an append/prepend/replaceChildren argument into raw Nokogiri
|
|
71
|
+
# node(s), detached from any current parent:
|
|
72
|
+
# - Element / TextNode / CommentNode → its backing node (unlinked)
|
|
73
|
+
# - Fragment → its extracted children
|
|
74
|
+
# - String → a fresh text node
|
|
75
|
+
# - anything else with a backing node → that node (unlinked)
|
|
76
|
+
#
|
|
77
|
+
# The class constants resolve at call time, so the mixin only needs to
|
|
78
|
+
# be defined before the including class bodies run.
|
|
79
|
+
def detach_dom_nodes(value)
|
|
80
|
+
case value
|
|
81
|
+
when Element, TextNode, CommentNode
|
|
82
|
+
[detach_with_notify(value.__dommy_backend_node__)]
|
|
83
|
+
when Fragment
|
|
84
|
+
value.extract_children
|
|
85
|
+
when String
|
|
86
|
+
[@document.create_text_node(value).__dommy_backend_node__]
|
|
87
|
+
else
|
|
88
|
+
node = value.respond_to?(:__dommy_backend_node__) ? value.__dommy_backend_node__ : nil
|
|
89
|
+
return [] unless node
|
|
90
|
+
|
|
91
|
+
[detach_with_notify(node)]
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Detach a node from its current parent, queuing a childList removal
|
|
96
|
+
# record on that old parent first (WHATWG "remove" runs before the
|
|
97
|
+
# subsequent insert, so moving a node yields a removal record + an addition
|
|
98
|
+
# record). Returns the raw node, ready to be re-linked.
|
|
99
|
+
def detach_with_notify(node)
|
|
100
|
+
old_parent = node.parent
|
|
101
|
+
return node unless old_parent
|
|
102
|
+
|
|
103
|
+
# Capture the position (as wrapped nodes — the coordinator records
|
|
104
|
+
# explicit siblings verbatim) before unlinking.
|
|
105
|
+
prev_sib = node.previous_sibling && @document.wrap_node(node.previous_sibling)
|
|
106
|
+
next_sib = node.next_sibling && @document.wrap_node(node.next_sibling)
|
|
107
|
+
node.unlink
|
|
108
|
+
@document.notify_child_list_mutation(
|
|
109
|
+
target_node: old_parent,
|
|
110
|
+
added_nodes: [],
|
|
111
|
+
removed_nodes: [node],
|
|
112
|
+
previous_sibling: prev_sib,
|
|
113
|
+
next_sibling: next_sib
|
|
114
|
+
)
|
|
115
|
+
node
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Hierarchy guard hook. Default no-op (Fragment / ShadowRoot stay
|
|
119
|
+
# permissive, matching current behavior). Element overrides this to
|
|
120
|
+
# call its `check_hierarchy!`.
|
|
121
|
+
def check_insertion!(_child)
|
|
122
|
+
nil
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -2,23 +2,113 @@
|
|
|
2
2
|
|
|
3
3
|
module Dommy
|
|
4
4
|
module Internal
|
|
5
|
-
# IDL-attribute reflection
|
|
6
|
-
# subclasses. Each helper delegates to the host element's standard
|
|
7
|
-
# attribute API (`get_attribute` / `set_attribute` / `has_attribute?`
|
|
8
|
-
# / `remove_attribute`), so case-sensitivity is naturally inherited
|
|
9
|
-
# from the host element's namespace — HTML lowercases, SVG keeps
|
|
10
|
-
# the spec name (`viewBox`).
|
|
5
|
+
# IDL-attribute reflection for `HTMLElement` and `SVGElement` subclasses.
|
|
11
6
|
#
|
|
12
|
-
# Two
|
|
7
|
+
# Two layers:
|
|
13
8
|
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
9
|
+
# 1. **Instance helpers** (`reflected_string` / `set_reflected_string` /
|
|
10
|
+
# `reflected_boolean` / `set_reflected_boolean`) delegate to the host
|
|
11
|
+
# element's standard attribute API (`get_attribute` / `set_attribute` /
|
|
12
|
+
# `has_attribute?` / `remove_attribute`), so case-sensitivity is
|
|
13
|
+
# inherited from the host's namespace — HTML lowercases, SVG keeps the
|
|
14
|
+
# spec name (`viewBox`).
|
|
17
15
|
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
16
|
+
# - **String**: property mirrors the attribute value. Missing → `""`.
|
|
17
|
+
# - **Boolean**: property is true iff the attribute is present. Setting
|
|
18
|
+
# true writes `""`; setting false removes the attribute.
|
|
19
|
+
#
|
|
20
|
+
# 2. **A class-level DSL** (`reflect_string` / `reflect_boolean`) that
|
|
21
|
+
# declares reflected attributes once and generates BOTH the snake_case
|
|
22
|
+
# getter/setter pair AND a `js_key => ruby_name` registry entry. A shared
|
|
23
|
+
# `__js_get__` / `__js_set__` consults that registry, so bridge property
|
|
24
|
+
# access needs no hand-written `case` arm. This keeps the Ruby accessor,
|
|
25
|
+
# the JS getter, and the JS setter from drifting apart (the same class of
|
|
26
|
+
# bug the `js_methods` macro prevents for `__js_call__`).
|
|
27
|
+
#
|
|
28
|
+
# reflect_string :cx, :cy, :r
|
|
29
|
+
# reflect_string view_box: "viewBox", class_name: { attr: "class" }
|
|
30
|
+
# reflect_boolean :disabled, :required
|
|
31
|
+
#
|
|
32
|
+
# Identifier defaults (override via a String or Hash value):
|
|
33
|
+
# - js_key (camelCase IDL name) = camelize(ruby_name)
|
|
34
|
+
# - attr (content attribute) = camelize(ruby_name)
|
|
35
|
+
# - String value overrides attr only: text_anchor: "text-anchor"
|
|
36
|
+
# - Hash value overrides either: tabindex: { js: "tabIndex" }
|
|
21
37
|
module ReflectedAttributes
|
|
38
|
+
def self.included(base)
|
|
39
|
+
base.extend(ClassMethods)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
module ClassMethods
|
|
43
|
+
def reflect_string(*names, **mapped)
|
|
44
|
+
_reflect(:string, names, mapped)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def reflect_boolean(*names, **mapped)
|
|
48
|
+
_reflect(:boolean, names, mapped)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Merged `js_key => ruby_name` map across the class ancestry (memoized).
|
|
52
|
+
# Recomputed lazily; `_reflect` invalidates the cache when called.
|
|
53
|
+
def reflected_property_map
|
|
54
|
+
@__reflected_map__ ||= begin
|
|
55
|
+
inherited = superclass.respond_to?(:reflected_property_map) ? superclass.reflected_property_map : {}
|
|
56
|
+
inherited.merge(@__reflected_props__ || {})
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def _reflect(type, names, mapped)
|
|
63
|
+
@__reflected_props__ ||= {}
|
|
64
|
+
@__reflected_map__ = nil # invalidate memoized merge
|
|
65
|
+
|
|
66
|
+
getter = type == :boolean ? :reflected_boolean : :reflected_string
|
|
67
|
+
setter = type == :boolean ? :set_reflected_boolean : :set_reflected_string
|
|
68
|
+
|
|
69
|
+
(names.map { |n| [n, nil] } + mapped.to_a).each do |ruby_name, override|
|
|
70
|
+
attr, js = _resolve_identifiers(ruby_name, override)
|
|
71
|
+
define_method(ruby_name) { __send__(getter, attr) }
|
|
72
|
+
define_method(:"#{ruby_name}=") { |value| __send__(setter, attr, value) }
|
|
73
|
+
@__reflected_props__[js] = ruby_name
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def _resolve_identifiers(ruby_name, override)
|
|
78
|
+
default = _camelize(ruby_name)
|
|
79
|
+
case override
|
|
80
|
+
when nil
|
|
81
|
+
[default, default]
|
|
82
|
+
when String
|
|
83
|
+
[override, default]
|
|
84
|
+
when Hash
|
|
85
|
+
[override[:attr] || default, override[:js] || default]
|
|
86
|
+
else
|
|
87
|
+
raise ArgumentError, "reflect_*: unsupported mapping for #{ruby_name.inspect}: #{override.inspect}"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def _camelize(name)
|
|
92
|
+
name.to_s.gsub(/_([a-z0-9])/) { ::Regexp.last_match(1).upcase }
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Bridge property read: route reflected keys to their accessor (which a
|
|
97
|
+
# subclass may have overridden with coercion), else up the super chain.
|
|
98
|
+
def __js_get__(key)
|
|
99
|
+
prop = self.class.reflected_property_map[key]
|
|
100
|
+
return __send__(prop) if prop
|
|
101
|
+
|
|
102
|
+
super
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def __js_set__(key, value)
|
|
106
|
+
prop = self.class.reflected_property_map[key]
|
|
107
|
+
return __send__(:"#{prop}=", value) if prop
|
|
108
|
+
|
|
109
|
+
super
|
|
110
|
+
end
|
|
111
|
+
|
|
22
112
|
private
|
|
23
113
|
|
|
24
114
|
def reflected_string(name)
|