dommy 0.13.0 → 0.14.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 +63 -29
- data/lib/dommy/backend.rb +32 -9
- data/lib/dommy/bridge/constructor.rb +72 -0
- data/lib/dommy/bridge/errors.rb +42 -0
- data/lib/dommy/bridge/methods.rb +21 -14
- data/lib/dommy/bridge/sentinels.rb +39 -0
- data/lib/dommy/bridge/values.rb +78 -0
- data/lib/dommy/{js → bridge}/wire_tags.rb +14 -11
- data/lib/dommy/bridge.rb +22 -250
- data/lib/dommy/browser.rb +112 -11
- data/lib/dommy/character_data.rb +539 -0
- data/lib/dommy/class_list.rb +311 -0
- data/lib/dommy/crypto.rb +12 -1
- data/lib/dommy/css.rb +12 -0
- data/lib/dommy/data_transfer.rb +106 -0
- data/lib/dommy/document.rb +307 -369
- data/lib/dommy/dom_parser.rb +3 -0
- data/lib/dommy/element.rb +199 -1817
- data/lib/dommy/encodings/tables.rb +4 -4
- data/lib/dommy/encodings.rb +58 -27
- data/lib/dommy/event.rb +227 -1
- data/lib/dommy/event_source.rb +41 -0
- data/lib/dommy/form_data.rb +6 -64
- data/lib/dommy/form_entry_list.rb +227 -0
- data/lib/dommy/fragment.rb +320 -0
- data/lib/dommy/history.rb +3 -0
- data/lib/dommy/html_collection.rb +18 -8
- data/lib/dommy/html_elements/base.rb +338 -0
- data/lib/dommy/html_elements/embedded.rb +237 -0
- data/lib/dommy/html_elements/forms/form.rb +1088 -0
- data/lib/dommy/html_elements/forms/input.rb +642 -0
- data/lib/dommy/html_elements/forms/select.rb +536 -0
- data/lib/dommy/html_elements/forms.rb +8 -0
- data/lib/dommy/html_elements/interactive.rb +382 -0
- data/lib/dommy/html_elements/media.rb +329 -0
- data/lib/dommy/html_elements/metadata.rb +353 -0
- data/lib/dommy/html_elements/miscellaneous.rb +77 -0
- data/lib/dommy/html_elements/tables.rb +468 -0
- data/lib/dommy/html_elements.rb +11 -5322
- data/lib/dommy/hyperlink_utils.rb +171 -0
- data/lib/dommy/interaction/debug.rb +7 -6
- data/lib/dommy/interaction/dom_summary.rb +65 -40
- data/lib/dommy/interaction/driver.rb +29 -115
- data/lib/dommy/interaction/event_synthesis.rb +97 -4
- data/lib/dommy/interaction/field_interactor.rb +1 -13
- data/lib/dommy/interaction/form_submission.rb +89 -167
- data/lib/dommy/interaction/key_sender.rb +121 -0
- data/lib/dommy/interaction/mime_types.rb +25 -0
- data/lib/dommy/interaction.rb +2 -0
- data/lib/dommy/internal/accessibility_tree.rb +32 -9
- data/lib/dommy/internal/accessibility_visibility.rb +44 -0
- data/lib/dommy/internal/accessible_name.rb +85 -65
- data/lib/dommy/internal/aria_role.rb +21 -6
- data/lib/dommy/internal/aria_snapshot.rb +5 -0
- data/lib/dommy/internal/aria_state.rb +24 -5
- data/lib/dommy/internal/autofill.rb +141 -0
- data/lib/dommy/internal/backend_prefilter.rb +170 -0
- data/lib/dommy/internal/bounded_cache.rb +50 -0
- data/lib/dommy/internal/child_node.rb +9 -34
- data/lib/dommy/internal/css/calc.rb +143 -145
- data/lib/dommy/internal/css/cascade.rb +38 -393
- data/lib/dommy/internal/css/cascaded_declarations.rb +245 -0
- data/lib/dommy/internal/css/color.rb +61 -34
- data/lib/dommy/internal/css/computed_style_builder.rb +205 -0
- data/lib/dommy/internal/css/computed_style_declaration.rb +20 -4
- data/lib/dommy/internal/css/counters.rb +60 -37
- data/lib/dommy/internal/css/custom_properties.rb +87 -39
- data/lib/dommy/internal/css/media_query.rb +228 -203
- data/lib/dommy/internal/css/parser.rb +29 -12
- data/lib/dommy/internal/css/property_registry.rb +32 -56
- data/lib/dommy/internal/css/renderability.rb +61 -0
- data/lib/dommy/internal/css/rule_index.rb +17 -140
- data/lib/dommy/internal/css/selector_dependencies.rb +162 -0
- data/lib/dommy/internal/css/style_cache.rb +61 -0
- data/lib/dommy/internal/css/ua_stylesheet.rb +40 -1
- data/lib/dommy/internal/css/value_tokens.rb +60 -0
- data/lib/dommy/internal/directionality.rb +188 -0
- data/lib/dommy/internal/document_generations.rb +169 -0
- data/lib/dommy/internal/document_interaction_state.rb +47 -0
- data/lib/dommy/internal/document_live_ranges.rb +97 -0
- data/lib/dommy/internal/dom_matching.rb +28 -12
- data/lib/dommy/internal/element_aria.rb +163 -0
- data/lib/dommy/internal/element_geometry.rb +69 -0
- data/lib/dommy/internal/element_matching.rb +5 -12
- data/lib/dommy/internal/element_shadow.rb +117 -0
- data/lib/dommy/internal/element_state.rb +232 -0
- data/lib/dommy/internal/element_tasks.rb +19 -0
- data/lib/dommy/internal/element_top_layer.rb +63 -0
- data/lib/dommy/internal/enumerated_keyword_sets.rb +53 -0
- data/lib/dommy/internal/exception_report.rb +1 -1
- data/lib/dommy/internal/input_type.rb +280 -0
- data/lib/dommy/internal/insertion_point.rb +62 -0
- data/lib/dommy/internal/mime_type.rb +74 -0
- data/lib/dommy/internal/mutation_coordinator.rb +57 -198
- data/lib/dommy/internal/namespaces.rb +41 -43
- data/lib/dommy/internal/node_adopter.rb +84 -77
- data/lib/dommy/internal/node_factory.rb +157 -0
- data/lib/dommy/internal/node_identity.rb +31 -0
- data/lib/dommy/internal/node_wrapper_cache.rb +61 -207
- data/lib/dommy/internal/observer_options.rb +93 -0
- data/lib/dommy/internal/parent_node.rb +27 -27
- data/lib/dommy/internal/post_insertion_steps.rb +213 -0
- data/lib/dommy/internal/reflected_attributes.rb +425 -13
- data/lib/dommy/internal/rendered_text/collector.rb +221 -0
- data/lib/dommy/internal/rendered_text/computed_box.rb +39 -0
- data/lib/dommy/internal/rendered_text/fragment.rb +55 -0
- data/lib/dommy/internal/rendered_text/items.rb +57 -0
- data/lib/dommy/internal/rendered_text/whitespace_collapser.rb +132 -0
- data/lib/dommy/internal/selector_arguments.rb +94 -0
- data/lib/dommy/internal/selector_index.rb +3 -1
- data/lib/dommy/internal/selector_matcher.rb +23 -383
- data/lib/dommy/internal/selector_parser.rb +2 -37
- data/lib/dommy/internal/template_content_registry.rb +112 -66
- data/lib/dommy/internal/text_flattening.rb +20 -0
- data/lib/dommy/internal/text_selection.rb +104 -0
- data/lib/dommy/internal/toggle_events.rb +56 -0
- data/lib/dommy/internal/toggle_task_tracker.rb +57 -0
- data/lib/dommy/internal/url_parser.rb +722 -569
- data/lib/dommy/internal/url_record_accessors.rb +90 -0
- data/lib/dommy/internal/window_constructors.rb +190 -0
- data/lib/dommy/internal/xml_serialization.rb +168 -80
- data/lib/dommy/js/crossing_profile.rb +75 -0
- data/lib/dommy/js/dom_interfaces.rb +22 -2
- data/lib/dommy/js/host_bridge.rb +43 -187
- data/lib/dommy/js/host_callbacks.rb +96 -0
- data/lib/dommy/js/host_runtime.js +840 -763
- data/lib/dommy/js/import_map.rb +10 -1
- data/lib/dommy/js/marshaller.rb +94 -65
- data/lib/dommy/js/observable_runtime.js +125 -146
- data/lib/dommy/js/runtime.rb +10 -43
- data/lib/dommy/js/runtime_registry.rb +46 -0
- data/lib/dommy/js/script_boot.rb +38 -73
- data/lib/dommy/js/webidl_tables.js +619 -0
- data/lib/dommy/js/window_wiring.rb +69 -0
- data/lib/dommy/location.rb +152 -103
- data/lib/dommy/mutation_observer.rb +3 -57
- data/lib/dommy/navigation.rb +65 -12
- data/lib/dommy/node.rb +46 -20
- data/lib/dommy/promise.rb +58 -4
- data/lib/dommy/resources.rb +19 -1
- data/lib/dommy/shadow_root.rb +11 -5
- data/lib/dommy/style_declaration.rb +284 -0
- data/lib/dommy/svg_elements.rb +1 -45
- data/lib/dommy/url.rb +20 -69
- data/lib/dommy/url_pattern/regexp_translator.rb +1 -6
- data/lib/dommy/url_pattern/tokenizer.rb +4 -2
- data/lib/dommy/validity_state.rb +383 -0
- data/lib/dommy/version.rb +1 -1
- data/lib/dommy/window.rb +30 -166
- data/lib/dommy/xml_http_request.rb +35 -4
- data/lib/dommy.rb +23 -3
- metadata +74 -7
- data/lib/dommy/bridge/constructor_registry.rb +0 -28
- data/lib/dommy/internal/css_pseudo_handlers.rb +0 -378
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
# DOMTokenList (`classList`, `relList`) and DOMStringMap (`dataset`) —
|
|
5
|
+
# the two live views over one attribute's text.
|
|
6
|
+
#
|
|
7
|
+
# Lived in element.rb, which is for Element.
|
|
8
|
+
class ClassList
|
|
9
|
+
include Enumerable
|
|
10
|
+
|
|
11
|
+
# `attribute` is the content attribute this token list reflects ("class" for
|
|
12
|
+
# `classList`, "rel" for `relList`, "sandbox", "sizes", "for", …).
|
|
13
|
+
def initialize(element, attribute = "class")
|
|
14
|
+
@element = element
|
|
15
|
+
@attribute = attribute
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def length
|
|
19
|
+
class_tokens.length
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
alias size length
|
|
23
|
+
|
|
24
|
+
def item(index)
|
|
25
|
+
i = index.to_i
|
|
26
|
+
return nil if i.negative?
|
|
27
|
+
|
|
28
|
+
class_tokens[i]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def value
|
|
32
|
+
@element.__dommy_backend_node__[@attribute].to_s
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def value=(new_value)
|
|
36
|
+
@element.set_attribute(@attribute, new_value.to_s)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Spec: contains() does NOT validate (no SyntaxError on empty).
|
|
40
|
+
def contains?(token)
|
|
41
|
+
class_tokens.include?(token.to_s)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# DOMTokenList membership. Defined explicitly (rather than inheriting
|
|
45
|
+
# Enumerable#include?, which re-iterates via #each) so a class-selector match
|
|
46
|
+
# is a single Array#include? over the cached tokens — the hot path under a
|
|
47
|
+
# querySelector-heavy SPA.
|
|
48
|
+
def include?(token)
|
|
49
|
+
class_tokens.include?(token.to_s)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def add(*tokens)
|
|
53
|
+
update_tokens { |existing| existing | normalize_tokens(tokens) }
|
|
54
|
+
nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def remove(*tokens)
|
|
58
|
+
update_tokens { |existing| existing - normalize_tokens(tokens) }
|
|
59
|
+
nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def replace(old_token, new_token)
|
|
63
|
+
# Spec order: both tokens' empty checks (SyntaxError) precede both
|
|
64
|
+
# whitespace checks (InvalidCharacterError) — so replace(" ", "") is a
|
|
65
|
+
# SyntaxError (the empty newToken), not an InvalidCharacterError.
|
|
66
|
+
old_s = stringify_token(old_token)
|
|
67
|
+
new_s = stringify_token(new_token)
|
|
68
|
+
raise DOMException::SyntaxError, "token is empty" if old_s.empty? || new_s.empty?
|
|
69
|
+
if old_s.match?(/[ \t\n\f\r]/) || new_s.match?(/[ \t\n\f\r]/)
|
|
70
|
+
raise DOMException::InvalidCharacterError, "token contains whitespace"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
tokens = class_tokens
|
|
74
|
+
idx = tokens.index(old_s)
|
|
75
|
+
return false unless idx
|
|
76
|
+
|
|
77
|
+
# class_tokens returns the cached token array; dup before mutating so the
|
|
78
|
+
# in-place assignment can't corrupt the cache (whose key is still the old
|
|
79
|
+
# raw attribute string, which would then hand stale tokens to later reads).
|
|
80
|
+
updated = tokens.dup
|
|
81
|
+
updated[idx] = new_s
|
|
82
|
+
@element.set_attribute(@attribute, updated.uniq.join(" "))
|
|
83
|
+
true
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def [](index)
|
|
87
|
+
item(index)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def each(&blk)
|
|
91
|
+
class_tokens.each(&blk)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def to_a
|
|
95
|
+
class_tokens.dup
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def to_s
|
|
99
|
+
value
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def __js_get__(key)
|
|
103
|
+
case key
|
|
104
|
+
when "length"
|
|
105
|
+
length
|
|
106
|
+
when "value"
|
|
107
|
+
value
|
|
108
|
+
else
|
|
109
|
+
# Indexed getter: `classList[i]` is an undefined-returning indexed
|
|
110
|
+
# property — out-of-range or negative indices yield JS `undefined`
|
|
111
|
+
# (unlike `item(i)`, which returns null). Returning Ruby nil here would
|
|
112
|
+
# marshal as JS null, so use the UNDEFINED sentinel.
|
|
113
|
+
if key.is_a?(Integer) || key.to_s.match?(/\A-?\d+\z/)
|
|
114
|
+
i = key.to_i
|
|
115
|
+
token = i.negative? ? nil : class_tokens[i]
|
|
116
|
+
token.nil? ? Bridge::UNDEFINED : token
|
|
117
|
+
else
|
|
118
|
+
Bridge::ABSENT # unknown non-index property
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def __js_set__(key, val)
|
|
124
|
+
case key
|
|
125
|
+
when "value"
|
|
126
|
+
self.value = val
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
nil
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
include Bridge::Methods
|
|
133
|
+
# NOTE: `supports` is intentionally absent — for the class attribute's token
|
|
134
|
+
# list it must throw a TypeError, which `list.supports(...)` (not a function)
|
|
135
|
+
# already does.
|
|
136
|
+
js_methods %w[add remove contains toggle replace item toString]
|
|
137
|
+
def __js_call__(method, args)
|
|
138
|
+
case method
|
|
139
|
+
when "add"
|
|
140
|
+
update_tokens { |tokens| tokens | normalize_tokens(args) }
|
|
141
|
+
Bridge::UNDEFINED
|
|
142
|
+
when "remove"
|
|
143
|
+
update_tokens { |tokens| tokens - normalize_tokens(args) }
|
|
144
|
+
Bridge::UNDEFINED
|
|
145
|
+
when "contains"
|
|
146
|
+
# contains() does not validate; null coerces to the string "null".
|
|
147
|
+
class_tokens.include?(stringify_token(args[0]))
|
|
148
|
+
when "toggle"
|
|
149
|
+
toggle(args[0], args[1])
|
|
150
|
+
when "replace"
|
|
151
|
+
replace(args[0], args[1])
|
|
152
|
+
when "item"
|
|
153
|
+
item(args[0])
|
|
154
|
+
when "toString"
|
|
155
|
+
value
|
|
156
|
+
else
|
|
157
|
+
nil
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
private
|
|
162
|
+
|
|
163
|
+
def toggle(token, force)
|
|
164
|
+
name = validate_token(token)
|
|
165
|
+
present = class_tokens.include?(name)
|
|
166
|
+
force_given = !(force.nil? || force.equal?(Bridge::UNDEFINED))
|
|
167
|
+
|
|
168
|
+
# Spec: toggle runs the update steps only when it actually adds or removes.
|
|
169
|
+
# With an explicit force that already matches the current state it's a
|
|
170
|
+
# no-op — the attribute is left byte-for-byte untouched (no re-serialize).
|
|
171
|
+
if force_given
|
|
172
|
+
want = !!force
|
|
173
|
+
return want if want == present
|
|
174
|
+
|
|
175
|
+
update_tokens { |tokens| want ? tokens | [name] : tokens - [name] }
|
|
176
|
+
return want
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
desired = !present
|
|
180
|
+
update_tokens { |tokens| desired ? tokens | [name] : tokens - [name] }
|
|
181
|
+
desired
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# USVString coercion of a token argument: JS `null` becomes the string
|
|
185
|
+
# "null" (so `add(null)` adds the token "null"), not the empty string.
|
|
186
|
+
def stringify_token(token)
|
|
187
|
+
token.nil? ? "null" : token.to_s
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Spec: any empty-string argument throws SyntaxError; any token
|
|
191
|
+
# containing ASCII whitespace throws InvalidCharacterError. Applies
|
|
192
|
+
# to add / remove / replace / toggle.
|
|
193
|
+
def normalize_tokens(args)
|
|
194
|
+
args.map { |t| validate_token(t) }
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def validate_token(token)
|
|
198
|
+
s = stringify_token(token)
|
|
199
|
+
raise DOMException::SyntaxError, "token is empty" if s.empty?
|
|
200
|
+
raise DOMException::InvalidCharacterError, "token contains whitespace: #{s.inspect}" if s.match?(/\s/)
|
|
201
|
+
|
|
202
|
+
s
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# The DOMTokenList token set: the class attribute parsed as an *ordered set*
|
|
206
|
+
# (whitespace-split, duplicates removed preserving first-seen order). length,
|
|
207
|
+
# item, iteration, and contains all operate on this set; `value`/`toString`
|
|
208
|
+
# return the raw attribute. ASCII whitespace per the spec is space/tab/LF/FF/CR.
|
|
209
|
+
def class_tokens
|
|
210
|
+
raw = @element.__dommy_backend_node__[@attribute].to_s
|
|
211
|
+
# Cache the parsed token list keyed by the raw attribute string: a class
|
|
212
|
+
# selector match re-reads this for every element on every querySelector,
|
|
213
|
+
# and the split/reject/uniq dominated heavy-SPA load profiles. The key is
|
|
214
|
+
# the raw value itself, so any change (add/remove/className=, or a direct
|
|
215
|
+
# backend mutation) yields a different key and transparently recomputes.
|
|
216
|
+
cached = @token_cache
|
|
217
|
+
return cached[1] if cached && cached[0] == raw
|
|
218
|
+
|
|
219
|
+
tokens = raw.split(/[ \t\n\f\r]+/).reject(&:empty?).uniq
|
|
220
|
+
@token_cache = [raw, tokens]
|
|
221
|
+
tokens
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# DOMTokenList "update steps": serialize the (deduplicated) token set back to
|
|
225
|
+
# the class attribute. add/remove/replace always run this, so duplicates
|
|
226
|
+
# collapse and whitespace normalizes even on a no-op token. The one carve-out
|
|
227
|
+
# (per spec) is an empty set with no existing attribute — don't create one.
|
|
228
|
+
def update_tokens
|
|
229
|
+
tokens = yield(class_tokens)
|
|
230
|
+
return if tokens.empty? && !@element.__dommy_backend_node__.key?(@attribute)
|
|
231
|
+
|
|
232
|
+
@element.set_attribute(@attribute, tokens.join(" "))
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# `Element#dataset` proxy. `el.dataset.fooBar` reads / writes
|
|
237
|
+
# `data-foo-bar` per the HTMLOrForeignElement.dataset spec
|
|
238
|
+
# (camelCase ↔ kebab-case round-trip).
|
|
239
|
+
class DatasetMap
|
|
240
|
+
def initialize(element)
|
|
241
|
+
@element = element
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def __js_get__(key)
|
|
245
|
+
name = key.to_s
|
|
246
|
+
return Bridge::ABSENT unless supported_name?(name)
|
|
247
|
+
|
|
248
|
+
# A missing data-* attribute reads as JS `undefined` (and `"foo" in dataset`
|
|
249
|
+
# is false), per DOMStringMap semantics.
|
|
250
|
+
value = @element.__dommy_backend_node__[attr_name(name)]
|
|
251
|
+
value.nil? ? Bridge::ABSENT : value
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def __js_set__(key, value)
|
|
255
|
+
name = key.to_s
|
|
256
|
+
raise DOMException::SyntaxError, "#{name.inspect} is not a valid dataset name" unless supported_name?(name)
|
|
257
|
+
|
|
258
|
+
attribute = attr_name(name)
|
|
259
|
+
unless Internal::Namespaces.valid_attribute_local_name?(attribute)
|
|
260
|
+
raise DOMException::InvalidCharacterError, "#{attribute.inspect} is not a valid attribute name"
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
@element.set_attribute(attribute, value.to_s)
|
|
264
|
+
nil
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Named deleter (`delete el.dataset.foo`): removes the data-* attribute. A
|
|
268
|
+
# name that is not supported is silently left alone (it names nothing to
|
|
269
|
+
# delete).
|
|
270
|
+
def __js_delete__(key)
|
|
271
|
+
name = key.to_s
|
|
272
|
+
return true unless supported_name?(name)
|
|
273
|
+
|
|
274
|
+
@element.remove_attribute(attr_name(name))
|
|
275
|
+
true
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def __js_call__(_method, _args)
|
|
279
|
+
nil
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# WebIDL "supported property names" for DOMStringMap: each `data-*`
|
|
283
|
+
# attribute's name with the `data-` prefix stripped and `-x` sequences
|
|
284
|
+
# camel-cased (`data-date-of-birth` → `dateOfBirth`, `data-` → ``).
|
|
285
|
+
def __js_named_props__
|
|
286
|
+
Backend.attribute_nodes(@element.__dommy_backend_node__).filter_map do |a|
|
|
287
|
+
name = Backend.attribute_ns_info(a)[:qualified_name]
|
|
288
|
+
next unless name.start_with?("data-")
|
|
289
|
+
|
|
290
|
+
name.sub(/\Adata-/, "").gsub(/-([a-z])/) { ::Regexp.last_match(1).upcase }
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
private
|
|
295
|
+
|
|
296
|
+
# Whether `name` can be a supported property name. The camel-casing of an
|
|
297
|
+
# attribute name never leaves a `-` before an ASCII lowercase letter
|
|
298
|
+
# (`data--foo` is `Foo`, never `-foo`), so such a name is none: the getter
|
|
299
|
+
# finds nothing, the setter throws, and the deleter does nothing.
|
|
300
|
+
def supported_name?(name) = !name.match?(/-[a-z]/)
|
|
301
|
+
|
|
302
|
+
def attr_name(key)
|
|
303
|
+
"data-#{key.to_s.gsub(/[A-Z]/) { |m| "-#{m.downcase}" }}"
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Stub `DOMRect` for `getBoundingClientRect` — no layout engine,
|
|
308
|
+
# so all values are 0. Consumer code that uses these for *relative*
|
|
309
|
+
# positioning sees zeroed values; absolute layout assertions need
|
|
310
|
+
# the real browser.
|
|
311
|
+
end
|
data/lib/dommy/crypto.rb
CHANGED
|
@@ -51,6 +51,15 @@ module Dommy
|
|
|
51
51
|
|
|
52
52
|
alias getRandomValues get_random_values
|
|
53
53
|
|
|
54
|
+
# `length` random bytes, for the JS `getRandomValues` stub: it validates the
|
|
55
|
+
# caller's typed array and copies these bytes into it, so the array's type
|
|
56
|
+
# and identity survive (see host_runtime.js's randomValuesStub). Not a WebIDL
|
|
57
|
+
# method — the stub calls it as `__internal_random_bytes__`, a name kept
|
|
58
|
+
# apart from the WebIDL `getRandomValues`.
|
|
59
|
+
def random_bytes(length)
|
|
60
|
+
Bridge::Bytes.new(SecureRandom.bytes(length).bytes)
|
|
61
|
+
end
|
|
62
|
+
|
|
54
63
|
def subtle
|
|
55
64
|
@subtle ||= SubtleCrypto.new(@window)
|
|
56
65
|
end
|
|
@@ -65,13 +74,15 @@ module Dommy
|
|
|
65
74
|
end
|
|
66
75
|
|
|
67
76
|
include Bridge::Methods
|
|
68
|
-
js_methods %w[randomUUID getRandomValues]
|
|
77
|
+
js_methods %w[randomUUID getRandomValues __internal_random_bytes__]
|
|
69
78
|
def __js_call__(method, args)
|
|
70
79
|
case method
|
|
71
80
|
when "randomUUID"
|
|
72
81
|
random_uuid
|
|
73
82
|
when "getRandomValues"
|
|
74
83
|
get_random_values(args[0])
|
|
84
|
+
when "__internal_random_bytes__"
|
|
85
|
+
random_bytes(args[0])
|
|
75
86
|
end
|
|
76
87
|
end
|
|
77
88
|
end
|
data/lib/dommy/css.rb
CHANGED
|
@@ -280,6 +280,14 @@ module Dommy
|
|
|
280
280
|
end
|
|
281
281
|
end
|
|
282
282
|
|
|
283
|
+
# `StyleSheetList` — the CSSStyleSheets of a document's or shadow root's
|
|
284
|
+
# <style> / <link rel=stylesheet> elements, live. Its IDL has an indexed
|
|
285
|
+
# getter and `length` but no `iterable<>`, so it gets @@iterator and NOT the
|
|
286
|
+
# values/entries/forEach that NodeList has — reporting it as a NodeList (as it
|
|
287
|
+
# once was) leaked Array.prototype's pair methods.
|
|
288
|
+
class StyleSheetList < LiveList
|
|
289
|
+
end
|
|
290
|
+
|
|
283
291
|
# `CSSStyleRule#style` — a live, mutable CSSStyleDeclaration backed by a
|
|
284
292
|
# rule's declaration block. Reads come from the parsed block; every write
|
|
285
293
|
# reserializes the block and hands it back to the owning CSSRule, which
|
|
@@ -603,6 +611,10 @@ module Dommy
|
|
|
603
611
|
case key
|
|
604
612
|
when "cssText" then self.css_text = value
|
|
605
613
|
when "selectorText" then self.selector_text = value
|
|
614
|
+
when "style"
|
|
615
|
+
# CSSStyleRule.style is [PutForwards=cssText]: assigning to it writes
|
|
616
|
+
# the declaration block (`rule.style = "color: red"`). nil on at-rules.
|
|
617
|
+
style&.css_text = value
|
|
606
618
|
when "media"
|
|
607
619
|
# CSSMediaRule#media is settable with a media-text string.
|
|
608
620
|
__internal_set_media__(value.to_s) if type == MEDIA_RULE
|
data/lib/dommy/data_transfer.rb
CHANGED
|
@@ -45,10 +45,23 @@ module Dommy
|
|
|
45
45
|
|
|
46
46
|
attr_accessor :drop_effect, :effect_allowed
|
|
47
47
|
|
|
48
|
+
def items
|
|
49
|
+
@items ||= DataTransferItemList.new(self)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Called by the item list when a File/Blob is added through `items.add`.
|
|
53
|
+
# FileList is immutable, so rebuild it with the new file appended.
|
|
54
|
+
def __internal_add_file__(file)
|
|
55
|
+
@files = FileList.new(@files.to_a + [file])
|
|
56
|
+
nil
|
|
57
|
+
end
|
|
58
|
+
|
|
48
59
|
def __js_get__(key)
|
|
49
60
|
case key
|
|
50
61
|
when "files"
|
|
51
62
|
@files
|
|
63
|
+
when "items"
|
|
64
|
+
items
|
|
52
65
|
when "types"
|
|
53
66
|
types
|
|
54
67
|
when "dropEffect"
|
|
@@ -101,4 +114,97 @@ module Dommy
|
|
|
101
114
|
end
|
|
102
115
|
end
|
|
103
116
|
end
|
|
117
|
+
|
|
118
|
+
# `DataTransferItemList` — `dataTransfer.items`. The tests build one to seed a
|
|
119
|
+
# file input: `dt.items.add(file); input.files = dt.files`. A Blob/File adds a
|
|
120
|
+
# file item (and appends to `files`); anything else adds a string item.
|
|
121
|
+
class DataTransferItemList
|
|
122
|
+
include Bridge::Methods
|
|
123
|
+
js_methods %w[add remove clear]
|
|
124
|
+
|
|
125
|
+
def initialize(owner)
|
|
126
|
+
@owner = owner
|
|
127
|
+
@items = []
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def add(data, type = nil)
|
|
131
|
+
item = DataTransferItem.new(data, type)
|
|
132
|
+
@items << item
|
|
133
|
+
if data.is_a?(Blob)
|
|
134
|
+
@owner.__internal_add_file__(data)
|
|
135
|
+
else
|
|
136
|
+
@owner.set_data(type.to_s.empty? ? "text/plain" : type.to_s, data.to_s)
|
|
137
|
+
end
|
|
138
|
+
item
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def remove(index)
|
|
142
|
+
@items.delete_at(index.to_i)
|
|
143
|
+
nil
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def clear
|
|
147
|
+
@items.clear
|
|
148
|
+
nil
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def length = @items.length
|
|
152
|
+
|
|
153
|
+
def __js_get__(key)
|
|
154
|
+
return length if key == "length"
|
|
155
|
+
|
|
156
|
+
index = Integer(key, exception: false)
|
|
157
|
+
return Bridge::ABSENT unless index&.between?(0, @items.length - 1)
|
|
158
|
+
|
|
159
|
+
@items[index]
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def __js_call__(method, args)
|
|
163
|
+
case method
|
|
164
|
+
when "add" then add(args[0], args[1])
|
|
165
|
+
when "remove" then remove(args[0])
|
|
166
|
+
when "clear" then clear
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# `DataTransferItem` — one entry in the list, a file or a string item.
|
|
172
|
+
class DataTransferItem
|
|
173
|
+
include Bridge::Methods
|
|
174
|
+
js_methods %w[getAsFile getAsString]
|
|
175
|
+
|
|
176
|
+
def initialize(data, type = nil)
|
|
177
|
+
@data = data
|
|
178
|
+
@type = if type.to_s.empty?
|
|
179
|
+
data.is_a?(Blob) ? data.type.to_s : "text/plain"
|
|
180
|
+
else
|
|
181
|
+
type.to_s
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def kind = @data.is_a?(Blob) ? "file" : "string"
|
|
186
|
+
def type = @type
|
|
187
|
+
|
|
188
|
+
def get_as_file = @data.is_a?(Blob) ? @data : nil
|
|
189
|
+
|
|
190
|
+
def get_as_string(callback)
|
|
191
|
+
callback&.call(@data.to_s)
|
|
192
|
+
nil
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def __js_get__(key)
|
|
196
|
+
case key
|
|
197
|
+
when "kind" then kind
|
|
198
|
+
when "type" then type
|
|
199
|
+
else Bridge::ABSENT
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def __js_call__(method, args)
|
|
204
|
+
case method
|
|
205
|
+
when "getAsFile" then get_as_file
|
|
206
|
+
when "getAsString" then get_as_string(args[0])
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
104
210
|
end
|