dommy 0.8.1 → 0.9.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/README.md +3 -3
- data/lib/dommy/animation.rb +4 -0
- data/lib/dommy/attr.rb +11 -5
- data/lib/dommy/backend/makiri_adapter.rb +330 -0
- data/lib/dommy/backend.rb +114 -33
- data/lib/dommy/blob.rb +2 -0
- data/lib/dommy/bridge.rb +11 -0
- data/lib/dommy/browser.rb +217 -0
- data/lib/dommy/compression_streams.rb +4 -0
- data/lib/dommy/crypto.rb +4 -0
- data/lib/dommy/css.rb +487 -50
- data/lib/dommy/custom_elements.rb +2 -2
- data/lib/dommy/data_transfer.rb +2 -0
- data/lib/dommy/data_uri.rb +35 -0
- data/lib/dommy/deferred_response.rb +59 -0
- data/lib/dommy/document.rb +386 -228
- data/lib/dommy/dom_exception.rb +2 -0
- data/lib/dommy/dom_parser.rb +7 -17
- data/lib/dommy/element.rb +502 -155
- data/lib/dommy/event.rb +240 -9
- data/lib/dommy/fetch.rb +152 -34
- data/lib/dommy/form_data.rb +2 -0
- data/lib/dommy/history.rb +2 -0
- data/lib/dommy/html_canvas_element.rb +230 -0
- data/lib/dommy/html_collection.rb +5 -6
- data/lib/dommy/html_elements.rb +304 -27
- data/lib/dommy/interaction/debug.rb +35 -0
- data/lib/dommy/interaction/dom_summary.rb +131 -0
- data/lib/dommy/interaction/driver.rb +244 -0
- data/lib/dommy/interaction/event_synthesis.rb +56 -0
- data/lib/dommy/interaction/field_interactor.rb +117 -0
- data/lib/dommy/interaction/form_submission.rb +268 -0
- data/lib/dommy/interaction/locator.rb +158 -0
- data/lib/dommy/interaction/role_query.rb +58 -0
- data/lib/dommy/interaction.rb +32 -0
- data/lib/dommy/internal/accessibility_tree.rb +215 -0
- data/lib/dommy/internal/accessible_description.rb +38 -0
- data/lib/dommy/internal/accessible_name.rb +301 -0
- data/lib/dommy/internal/aria_role.rb +252 -0
- data/lib/dommy/internal/aria_snapshot.rb +64 -0
- data/lib/dommy/internal/aria_state.rb +151 -0
- data/lib/dommy/internal/css/calc.rb +242 -0
- data/lib/dommy/internal/css/cascade.rb +430 -0
- data/lib/dommy/internal/css/color.rb +381 -0
- data/lib/dommy/internal/css/computed_style_declaration.rb +130 -0
- data/lib/dommy/internal/css/counters.rb +227 -0
- data/lib/dommy/internal/css/custom_properties.rb +183 -0
- data/lib/dommy/internal/css/media_query.rb +302 -0
- data/lib/dommy/internal/css/parser.rb +265 -0
- data/lib/dommy/internal/css/property_registry.rb +512 -0
- data/lib/dommy/internal/css/rule_index.rb +494 -0
- data/lib/dommy/internal/css/supports.rb +158 -0
- data/lib/dommy/internal/css/ua_stylesheet.rb +53 -0
- data/lib/dommy/internal/css_pseudo_handlers.rb +283 -42
- data/lib/dommy/internal/css_rule_text.rb +160 -0
- data/lib/dommy/internal/dom_matching.rb +80 -9
- data/lib/dommy/internal/element_matching.rb +109 -0
- data/lib/dommy/internal/global_functions.rb +33 -0
- data/lib/dommy/internal/mutation_coordinator.rb +95 -4
- data/lib/dommy/internal/namespaces.rb +49 -5
- data/lib/dommy/internal/node_wrapper_cache.rb +163 -26
- data/lib/dommy/internal/parent_node.rb +82 -5
- data/lib/dommy/internal/selector_ast.rb +124 -0
- data/lib/dommy/internal/selector_index.rb +146 -0
- data/lib/dommy/internal/selector_matcher.rb +756 -0
- data/lib/dommy/internal/selector_parser.rb +283 -131
- data/lib/dommy/internal/shadow_root_registry.rb +9 -2
- data/lib/dommy/internal/template_content_registry.rb +26 -18
- data/lib/dommy/internal/xml_serialization.rb +344 -0
- data/lib/dommy/intersection_observer.rb +2 -0
- data/lib/dommy/js/bridge_conformance.rb +80 -0
- data/lib/dommy/js/constructor_resolver.rb +44 -0
- data/lib/dommy/js/custom_element_bridge.rb +90 -0
- data/lib/dommy/js/dom_interfaces.rb +162 -0
- data/lib/dommy/js/handle_table.rb +60 -0
- data/lib/dommy/js/host_bridge.rb +517 -0
- data/lib/dommy/js/host_runtime.js +1495 -0
- data/lib/dommy/js/import_map.rb +58 -0
- data/lib/dommy/js/marshaller.rb +240 -0
- data/lib/dommy/js/module_loader.rb +99 -0
- data/lib/dommy/js/observable_runtime.js +742 -0
- data/lib/dommy/js/runtime.rb +115 -0
- data/lib/dommy/js/script_boot.rb +221 -0
- data/lib/dommy/js/wire_tags.rb +62 -0
- data/lib/dommy/location.rb +2 -0
- data/lib/dommy/media_query_list.rb +50 -14
- data/lib/dommy/message_channel.rb +22 -6
- data/lib/dommy/minitest/assertions.rb +27 -0
- data/lib/dommy/mutation_observer.rb +89 -4
- data/lib/dommy/navigator.rb +34 -2
- data/lib/dommy/node.rb +24 -14
- data/lib/dommy/notification.rb +2 -0
- data/lib/dommy/parser.rb +1 -1
- data/lib/dommy/performance.rb +21 -1
- data/lib/dommy/promise.rb +94 -10
- data/lib/dommy/range.rb +173 -31
- data/lib/dommy/resources.rb +178 -0
- data/lib/dommy/rspec/capy_style_matchers.rb +126 -0
- data/lib/dommy/scheduler.rb +149 -13
- data/lib/dommy/screen.rb +91 -0
- data/lib/dommy/shadow_root.rb +76 -13
- data/lib/dommy/storage.rb +2 -1
- data/lib/dommy/streams.rb +6 -0
- data/lib/dommy/text_codec.rb +7 -1
- data/lib/dommy/tree_walker.rb +33 -10
- data/lib/dommy/url.rb +13 -1
- data/lib/dommy/version.rb +1 -1
- data/lib/dommy/window.rb +199 -11
- data/lib/dommy/worker.rb +8 -4
- data/lib/dommy/xml_http_request.rb +47 -6
- data/lib/dommy.rb +36 -1
- metadata +86 -14
- data/lib/dommy/backend/nokogiri_adapter.rb +0 -127
- data/lib/dommy/backend/nokolexbor_adapter.rb +0 -117
data/lib/dommy/css.rb
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "internal/css/supports"
|
|
4
|
+
|
|
3
5
|
module Dommy
|
|
4
|
-
# `CSSStyleSheet` —
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
6
|
+
# `CSSStyleSheet` — the sheet itself doesn't interpret rule text; it
|
|
7
|
+
# acts as an ordered list of opaque `CSSRule`-like wrappers. The
|
|
8
|
+
# cascade consumes the sheet through `cascade_text` (all rule texts
|
|
9
|
+
# in document order), so insertRule/deleteRule/replaceSync and
|
|
10
|
+
# `disabled` are reflected in computed styles for `<style>`-owned
|
|
11
|
+
# sheets (Internal::CSS::RuleIndex re-parses on the next lookup).
|
|
9
12
|
#
|
|
10
13
|
# sheet.insertRule("p { color: red }", 0);
|
|
11
14
|
# for (const r of sheet.cssRules) console.log(r.cssText);
|
|
12
15
|
#
|
|
13
|
-
# `
|
|
14
|
-
#
|
|
16
|
+
# `href`, `media`, `title`, `type` mirror the owner node's
|
|
17
|
+
# attributes when present.
|
|
15
18
|
class CSSStyleSheet
|
|
16
19
|
attr_reader :owner_node, :css_rules
|
|
17
20
|
|
|
18
|
-
def initialize(owner_node:, href: nil, media: nil, title: nil, type: "text/css")
|
|
21
|
+
def initialize(owner_node:, href: nil, media: nil, title: nil, type: "text/css", source_text: nil)
|
|
19
22
|
@owner_node = owner_node
|
|
20
23
|
@href = href
|
|
21
24
|
@media = media
|
|
@@ -23,6 +26,13 @@ module Dommy
|
|
|
23
26
|
@type = type
|
|
24
27
|
@disabled = false
|
|
25
28
|
@css_rules = CSSRuleList.new
|
|
29
|
+
# The owner node's CSS text at sheet creation, split into one CSSRule
|
|
30
|
+
# per top-level rule (source order) so cssRules mirrors the parsed
|
|
31
|
+
# sheet — selectorText / style / nested cssRules read off each slice.
|
|
32
|
+
# insertRule(0) lands before them, appends after, as a real sheet would.
|
|
33
|
+
Internal::CSSRuleText.split_rules(source_text).each_with_index do |slice, i|
|
|
34
|
+
@css_rules.__internal_insert__(i, CSSRule.new(slice, self))
|
|
35
|
+
end
|
|
26
36
|
end
|
|
27
37
|
|
|
28
38
|
def disabled
|
|
@@ -30,7 +40,18 @@ module Dommy
|
|
|
30
40
|
end
|
|
31
41
|
|
|
32
42
|
def disabled=(v)
|
|
33
|
-
|
|
43
|
+
v = !!v
|
|
44
|
+
changed = @disabled != v
|
|
45
|
+
@disabled = v
|
|
46
|
+
__internal_bump_owner_style_generation__ if changed
|
|
47
|
+
v
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The sheet's full CSS text in document order — what the cascade
|
|
51
|
+
# parses in place of the owner `<style>`'s raw text once the sheet
|
|
52
|
+
# has been mutated through the CSSOM.
|
|
53
|
+
def cascade_text
|
|
54
|
+
@css_rules.map(&:css_text).join("\n")
|
|
34
55
|
end
|
|
35
56
|
|
|
36
57
|
def href
|
|
@@ -57,31 +78,55 @@ module Dommy
|
|
|
57
78
|
nil
|
|
58
79
|
end
|
|
59
80
|
|
|
60
|
-
# `insertRule(
|
|
61
|
-
# given position (
|
|
81
|
+
# `insertRule(rule, index)` — `rule` is required; inserts a CSSRule at the
|
|
82
|
+
# given position (Dommy defaults to the end). Returns the index used.
|
|
62
83
|
def insert_rule(rule_text, index = nil)
|
|
84
|
+
raise Bridge::TypeError, "insertRule requires a rule" if rule_text.nil?
|
|
85
|
+
|
|
63
86
|
idx = index.nil? ? @css_rules.length : index.to_i
|
|
64
87
|
raise DOMException::IndexSizeError, "out of range" if idx < 0 || idx > @css_rules.length
|
|
65
88
|
|
|
66
89
|
@css_rules.__internal_insert__(idx, CSSRule.new(rule_text.to_s, self))
|
|
90
|
+
__internal_bump_owner_style_generation__
|
|
67
91
|
idx
|
|
68
92
|
end
|
|
69
93
|
|
|
94
|
+
# `deleteRule(index)` — `index` is required.
|
|
70
95
|
def delete_rule(index)
|
|
96
|
+
raise Bridge::TypeError, "deleteRule requires an index" if index.nil?
|
|
97
|
+
|
|
71
98
|
idx = index.to_i
|
|
72
99
|
raise DOMException::IndexSizeError, "out of range" if idx < 0 || idx >= @css_rules.length
|
|
73
100
|
|
|
74
101
|
@css_rules.__internal_delete_at__(idx)
|
|
102
|
+
__internal_bump_owner_style_generation__
|
|
75
103
|
nil
|
|
76
104
|
end
|
|
77
105
|
|
|
106
|
+
# `addRule(selector, style, index)` — the legacy IE-era editing API (still
|
|
107
|
+
# in CSSOM). Builds `selector { style }`, inserts it (default: at the end),
|
|
108
|
+
# and returns -1. Omitted selector/style stringify to "undefined", matching
|
|
109
|
+
# the spec's coercion.
|
|
110
|
+
def add_rule(selector = nil, style = nil, index = nil)
|
|
111
|
+
selector = selector.nil? ? "undefined" : selector.to_s
|
|
112
|
+
style = style.nil? ? "undefined" : style.to_s
|
|
113
|
+
idx = index.nil? ? @css_rules.length : index.to_i
|
|
114
|
+
insert_rule("#{selector} { #{style} }", idx)
|
|
115
|
+
-1
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# `removeRule(index = 0)` — the legacy alias for deleteRule (its index
|
|
119
|
+
# defaults to 0, unlike deleteRule's required argument).
|
|
120
|
+
def remove_rule(index = 0)
|
|
121
|
+
delete_rule(index.nil? ? 0 : index)
|
|
122
|
+
end
|
|
123
|
+
|
|
78
124
|
# `replaceSync(text)` — replace all rules with a single rule blob
|
|
79
125
|
# (no parsing — we keep it as one opaque entry).
|
|
80
126
|
def replace_sync(text)
|
|
81
127
|
@css_rules.__internal_clear__
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
@css_rules.__internal_insert__(0, CSSRule.new(text.to_s, self))
|
|
128
|
+
@css_rules.__internal_insert__(0, CSSRule.new(text.to_s, self)) unless text.to_s.empty?
|
|
129
|
+
__internal_bump_owner_style_generation__
|
|
85
130
|
nil
|
|
86
131
|
end
|
|
87
132
|
|
|
@@ -113,6 +158,8 @@ module Dommy
|
|
|
113
158
|
parent_style_sheet
|
|
114
159
|
when "ownerRule"
|
|
115
160
|
owner_rule
|
|
161
|
+
else
|
|
162
|
+
Bridge::ABSENT
|
|
116
163
|
end
|
|
117
164
|
end
|
|
118
165
|
|
|
@@ -120,25 +167,48 @@ module Dommy
|
|
|
120
167
|
case key
|
|
121
168
|
when "disabled"
|
|
122
169
|
self.disabled = value
|
|
170
|
+
else
|
|
171
|
+
return Bridge::UNHANDLED
|
|
123
172
|
end
|
|
124
173
|
|
|
125
174
|
nil
|
|
126
175
|
end
|
|
127
176
|
|
|
128
177
|
include Bridge::Methods
|
|
129
|
-
js_methods %w[insertRule deleteRule replaceSync replace]
|
|
178
|
+
js_methods %w[insertRule deleteRule addRule removeRule replaceSync replace]
|
|
130
179
|
def __js_call__(method, args)
|
|
131
180
|
case method
|
|
132
181
|
when "insertRule"
|
|
133
182
|
insert_rule(args[0], args[1])
|
|
134
183
|
when "deleteRule"
|
|
135
184
|
delete_rule(args[0])
|
|
185
|
+
when "addRule"
|
|
186
|
+
add_rule(args[0], args[1], args[2])
|
|
187
|
+
when "removeRule"
|
|
188
|
+
remove_rule(args[0])
|
|
136
189
|
when "replaceSync"
|
|
137
190
|
replace_sync(args[0])
|
|
138
191
|
when "replace"
|
|
139
192
|
replace(args[0])
|
|
140
193
|
end
|
|
141
194
|
end
|
|
195
|
+
|
|
196
|
+
# A child CSSRule whose text changed (e.g. `rule.style.color = ...`)
|
|
197
|
+
# invalidates the owner document's computed style the same way
|
|
198
|
+
# insertRule/deleteRule do — its rebuilt cssText feeds `cascade_text`.
|
|
199
|
+
def __internal_notify_rule_changed__
|
|
200
|
+
__internal_bump_owner_style_generation__
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
private
|
|
204
|
+
|
|
205
|
+
# CSSOM mutations must invalidate the owner document's computed-style
|
|
206
|
+
# cache — the rule index re-reads `cascade_text` on the next lookup.
|
|
207
|
+
def __internal_bump_owner_style_generation__
|
|
208
|
+
doc = @owner_node.respond_to?(:owner_document) ? @owner_node.owner_document : nil
|
|
209
|
+
doc.__internal_bump_style_generation__ if doc.respond_to?(:__internal_bump_style_generation__)
|
|
210
|
+
nil
|
|
211
|
+
end
|
|
142
212
|
end
|
|
143
213
|
|
|
144
214
|
# `CSSRuleList` — indexed list of CSSRule, returned by
|
|
@@ -208,10 +278,160 @@ module Dommy
|
|
|
208
278
|
end
|
|
209
279
|
end
|
|
210
280
|
|
|
211
|
-
# `
|
|
212
|
-
#
|
|
213
|
-
#
|
|
214
|
-
# the
|
|
281
|
+
# `CSSStyleRule#style` — a live, mutable CSSStyleDeclaration backed by a
|
|
282
|
+
# rule's declaration block. Reads come from the parsed block; every write
|
|
283
|
+
# reserializes the block and hands it back to the owning CSSRule, which
|
|
284
|
+
# rebuilds its cssText and invalidates the document's computed-style cache.
|
|
285
|
+
class RuleStyleDeclaration
|
|
286
|
+
include Enumerable
|
|
287
|
+
|
|
288
|
+
def initialize(rule, body_text)
|
|
289
|
+
@rule = rule
|
|
290
|
+
@props = parse(body_text)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def get_property_value(name)
|
|
294
|
+
entry = @props[name.to_s]
|
|
295
|
+
entry ? entry[:value] : ""
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def get_property_priority(name)
|
|
299
|
+
entry = @props[name.to_s]
|
|
300
|
+
entry ? entry[:priority] : ""
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def set_property(name, value, priority = nil)
|
|
304
|
+
key = name.to_s
|
|
305
|
+
if value.nil? || value.to_s.empty?
|
|
306
|
+
@props.delete(key)
|
|
307
|
+
else
|
|
308
|
+
important = priority.to_s.downcase == "important" ? "important" : ""
|
|
309
|
+
@props[key] = {value: value.to_s, priority: important}
|
|
310
|
+
end
|
|
311
|
+
flush!
|
|
312
|
+
nil
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def remove_property(name)
|
|
316
|
+
removed = @props.delete(name.to_s)
|
|
317
|
+
flush!
|
|
318
|
+
removed ? removed[:value] : ""
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def [](key)
|
|
322
|
+
key.is_a?(Integer) ? @props.keys[key].to_s : get_property_value(key)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def []=(name, value)
|
|
326
|
+
set_property(name, value)
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def length
|
|
330
|
+
@props.size
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def item(index)
|
|
334
|
+
@props.keys[index.to_i].to_s
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def css_text
|
|
338
|
+
@props.map do |name, entry|
|
|
339
|
+
important = entry[:priority] == "important" ? " !important" : ""
|
|
340
|
+
"#{name}: #{entry[:value]}#{important};"
|
|
341
|
+
end.join(" ")
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def css_text=(text)
|
|
345
|
+
@props = parse(text)
|
|
346
|
+
flush!
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def each(&blk)
|
|
350
|
+
@props.keys.each(&blk)
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# camelCase / snake_case property accessors (`style.backgroundColor`).
|
|
354
|
+
def method_missing(name, *args)
|
|
355
|
+
str = name.to_s
|
|
356
|
+
if str.end_with?("=")
|
|
357
|
+
set_property(css_name(str[0..-2]), args.first)
|
|
358
|
+
elsif args.empty?
|
|
359
|
+
get_property_value(css_name(str))
|
|
360
|
+
else
|
|
361
|
+
super
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def respond_to_missing?(_name, _include_private = false)
|
|
366
|
+
true
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def __js_get__(key)
|
|
370
|
+
case key
|
|
371
|
+
when "cssText" then css_text
|
|
372
|
+
when "length" then length
|
|
373
|
+
when "parentRule" then @rule
|
|
374
|
+
else
|
|
375
|
+
if key.is_a?(Integer) || key.to_s.match?(/\A\d+\z/)
|
|
376
|
+
self[key.to_i]
|
|
377
|
+
else
|
|
378
|
+
get_property_value(css_name(key.to_s))
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def __js_set__(key, value)
|
|
384
|
+
case key
|
|
385
|
+
when "cssText" then self.css_text = value
|
|
386
|
+
else set_property(css_name(key.to_s), value)
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
nil
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
include Bridge::Methods
|
|
393
|
+
js_methods %w[getPropertyValue getPropertyPriority setProperty removeProperty item]
|
|
394
|
+
def __js_call__(method, args)
|
|
395
|
+
case method
|
|
396
|
+
when "getPropertyValue" then get_property_value(args[0])
|
|
397
|
+
when "getPropertyPriority" then get_property_priority(args[0])
|
|
398
|
+
when "setProperty" then set_property(args[0], args[1], args[2])
|
|
399
|
+
when "removeProperty" then remove_property(args[0])
|
|
400
|
+
when "item" then item(args[0].to_i)
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
private
|
|
405
|
+
|
|
406
|
+
def css_name(name)
|
|
407
|
+
str = name.to_s
|
|
408
|
+
return str if str.start_with?("--")
|
|
409
|
+
|
|
410
|
+
str.include?("_") ? str.tr("_", "-") : str.gsub(/[A-Z]/) { "-#{Regexp.last_match(0).downcase}" }
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
# Parse a declaration block into ordered { name => {value:, priority:} },
|
|
414
|
+
# reusing the cascade's declaration parser (same normalization the cascade
|
|
415
|
+
# sees) so reads agree with computed style.
|
|
416
|
+
def parse(body_text)
|
|
417
|
+
Internal::CSS::Parser.parse_declarations(body_text.to_s).each_with_object({}) do |decl, out|
|
|
418
|
+
out[decl.name] = {value: decl.value, priority: decl.important ? "important" : ""}
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def flush!
|
|
423
|
+
@rule.__internal_rebuild_from_style__(css_text)
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
# `CSSRule` — one parsed stylesheet rule. `cssText` round-trips the source
|
|
428
|
+
# slice verbatim until the rule is mutated. The CSSOM subclass hierarchy
|
|
429
|
+
# (CSSStyleRule / CSSMediaRule / …) is collapsed into this one class, which
|
|
430
|
+
# exposes the accessors per `type`: `selectorText` + `style` for style
|
|
431
|
+
# rules, `conditionText` + `cssRules` for grouping rules (@media/@supports).
|
|
432
|
+
# The selector text and declaration block are derived lazily with
|
|
433
|
+
# Internal::CSSRuleText (a light scanner; the cascade's correctness still
|
|
434
|
+
# comes from lexbor).
|
|
215
435
|
class CSSRule
|
|
216
436
|
STYLE_RULE = 1
|
|
217
437
|
CHARSET_RULE = 2
|
|
@@ -221,6 +441,15 @@ module Dommy
|
|
|
221
441
|
PAGE_RULE = 6
|
|
222
442
|
KEYFRAMES_RULE = 7
|
|
223
443
|
KEYFRAME_RULE = 8
|
|
444
|
+
SUPPORTS_RULE = 12
|
|
445
|
+
|
|
446
|
+
AT_RULE_TYPES = {
|
|
447
|
+
"media" => MEDIA_RULE, "supports" => SUPPORTS_RULE, "import" => IMPORT_RULE,
|
|
448
|
+
"charset" => CHARSET_RULE, "font-face" => FONT_FACE_RULE, "page" => PAGE_RULE,
|
|
449
|
+
"keyframes" => KEYFRAMES_RULE
|
|
450
|
+
}.freeze
|
|
451
|
+
|
|
452
|
+
GROUPING_TYPES = [MEDIA_RULE, SUPPORTS_RULE].freeze
|
|
224
453
|
|
|
225
454
|
attr_reader :parent_style_sheet
|
|
226
455
|
|
|
@@ -235,60 +464,257 @@ module Dommy
|
|
|
235
464
|
|
|
236
465
|
def css_text=(v)
|
|
237
466
|
@css_text = v.to_s
|
|
467
|
+
invalidate!
|
|
238
468
|
end
|
|
239
469
|
|
|
240
|
-
# We don't parse, so report the generic STYLE_RULE type.
|
|
241
470
|
def type
|
|
242
|
-
|
|
471
|
+
keyword = Internal::CSSRuleText.at_keyword(prelude)
|
|
472
|
+
keyword ? AT_RULE_TYPES.fetch(keyword, STYLE_RULE) : STYLE_RULE
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
def style_rule?
|
|
476
|
+
type == STYLE_RULE
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def grouping?
|
|
480
|
+
GROUPING_TYPES.include?(type)
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
# CSSStyleRule#selectorText — the rule's selector list ("" for at-rules).
|
|
484
|
+
def selector_text
|
|
485
|
+
style_rule? ? prelude : ""
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
def selector_text=(value)
|
|
489
|
+
return unless style_rule?
|
|
490
|
+
|
|
491
|
+
@selector = value.to_s
|
|
492
|
+
rebuild_css_text!
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
# CSSStyleRule#style — a live, mutable declaration block. nil for at-rules
|
|
496
|
+
# (matching the absent `style` member on CSSMediaRule etc.). Writes
|
|
497
|
+
# reserialize cssText and invalidate the document's computed-style cache.
|
|
498
|
+
def style
|
|
499
|
+
return nil unless style_rule?
|
|
500
|
+
|
|
501
|
+
@style ||= RuleStyleDeclaration.new(self, Internal::CSSRuleText.split_rule(@css_text).last)
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
# CSSGroupingRule#cssRules — the nested rules of an @media/@supports block.
|
|
505
|
+
def css_rules
|
|
506
|
+
return nil unless grouping?
|
|
507
|
+
|
|
508
|
+
@css_rules ||= begin
|
|
509
|
+
list = CSSRuleList.new
|
|
510
|
+
Internal::CSSRuleText.split_rules(body).each_with_index do |slice, i|
|
|
511
|
+
list.__internal_insert__(i, CSSRule.new(slice, @parent_style_sheet))
|
|
512
|
+
end
|
|
513
|
+
list
|
|
514
|
+
end
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
# CSSConditionRule#conditionText — the condition text after the at-keyword.
|
|
518
|
+
def condition_text
|
|
519
|
+
grouping? ? prelude.sub(/\A@[-a-z]+/i, "").strip : ""
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
# CSSMediaRule#media — a live MediaList over the @media condition. nil for
|
|
523
|
+
# non-media rules. Mutations rebuild the rule's prelude and reflow the
|
|
524
|
+
# cascade.
|
|
525
|
+
def media
|
|
526
|
+
return nil unless type == MEDIA_RULE
|
|
527
|
+
|
|
528
|
+
@media_list ||= MediaList.new(condition_text, on_change: method(:__internal_set_media__))
|
|
243
529
|
end
|
|
244
530
|
|
|
245
531
|
def parent_rule
|
|
246
532
|
nil
|
|
247
533
|
end
|
|
248
534
|
|
|
535
|
+
# Called by RuleStyleDeclaration after a property write: rebuild cssText
|
|
536
|
+
# from the (possibly new) selector and declaration block.
|
|
537
|
+
def __internal_rebuild_from_style__(block_text)
|
|
538
|
+
@selector ||= prelude
|
|
539
|
+
@css_text = block_text.empty? ? "#{@selector} {}" : "#{@selector} { #{block_text} }"
|
|
540
|
+
@parent_style_sheet&.__internal_notify_rule_changed__
|
|
541
|
+
nil
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
# Called by the MediaList when its media text changes: rebuild the @media
|
|
545
|
+
# prelude (keeping the block body) and reflow the cascade.
|
|
546
|
+
def __internal_set_media__(media_text)
|
|
547
|
+
@css_text = "@media #{media_text} { #{body} }"
|
|
548
|
+
@prelude = nil
|
|
549
|
+
@media_list = nil
|
|
550
|
+
@parent_style_sheet&.__internal_notify_rule_changed__
|
|
551
|
+
nil
|
|
552
|
+
end
|
|
553
|
+
|
|
249
554
|
def __js_get__(key)
|
|
250
555
|
case key
|
|
251
|
-
when "cssText"
|
|
252
|
-
|
|
253
|
-
when "
|
|
254
|
-
|
|
255
|
-
when "
|
|
256
|
-
|
|
257
|
-
when "
|
|
258
|
-
|
|
259
|
-
when "
|
|
260
|
-
|
|
261
|
-
when "MEDIA_RULE"
|
|
262
|
-
|
|
263
|
-
when "
|
|
264
|
-
|
|
265
|
-
when "
|
|
266
|
-
|
|
267
|
-
when "
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
when "KEYFRAME_RULE"
|
|
272
|
-
KEYFRAME_RULE
|
|
273
|
-
when "CHARSET_RULE"
|
|
274
|
-
CHARSET_RULE
|
|
556
|
+
when "cssText" then @css_text
|
|
557
|
+
when "type" then type
|
|
558
|
+
when "selectorText" then selector_text
|
|
559
|
+
when "style" then style
|
|
560
|
+
when "cssRules" then css_rules
|
|
561
|
+
when "conditionText" then grouping? ? condition_text : nil
|
|
562
|
+
when "media" then media
|
|
563
|
+
when "parentStyleSheet" then @parent_style_sheet
|
|
564
|
+
when "parentRule" then parent_rule
|
|
565
|
+
when "STYLE_RULE" then STYLE_RULE
|
|
566
|
+
when "MEDIA_RULE" then MEDIA_RULE
|
|
567
|
+
when "IMPORT_RULE" then IMPORT_RULE
|
|
568
|
+
when "FONT_FACE_RULE" then FONT_FACE_RULE
|
|
569
|
+
when "PAGE_RULE" then PAGE_RULE
|
|
570
|
+
when "KEYFRAMES_RULE" then KEYFRAMES_RULE
|
|
571
|
+
when "KEYFRAME_RULE" then KEYFRAME_RULE
|
|
572
|
+
when "SUPPORTS_RULE" then SUPPORTS_RULE
|
|
573
|
+
when "CHARSET_RULE" then CHARSET_RULE
|
|
574
|
+
else
|
|
575
|
+
Bridge::ABSENT
|
|
275
576
|
end
|
|
276
577
|
end
|
|
277
578
|
|
|
278
579
|
def __js_set__(key, value)
|
|
279
580
|
case key
|
|
280
|
-
when "cssText"
|
|
281
|
-
|
|
581
|
+
when "cssText" then self.css_text = value
|
|
582
|
+
when "selectorText" then self.selector_text = value
|
|
583
|
+
when "media"
|
|
584
|
+
# CSSMediaRule#media is settable with a media-text string.
|
|
585
|
+
__internal_set_media__(value.to_s) if type == MEDIA_RULE
|
|
586
|
+
else
|
|
587
|
+
# Signal "not a host property" so the bridge keeps the assignment as a
|
|
588
|
+
# JS-side expando (WebIDL platform objects allow expandos; WPT's
|
|
589
|
+
# [SameObject] tests stash a marker on cssRules[i] and read it back).
|
|
590
|
+
return Bridge::UNHANDLED
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
nil
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
private
|
|
597
|
+
|
|
598
|
+
# The rule prelude (selector list or at-rule keyword + condition),
|
|
599
|
+
# memoized; recomputed after css_text changes.
|
|
600
|
+
def prelude
|
|
601
|
+
@prelude ||= Internal::CSSRuleText.split_rule(@css_text).first
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
# The block body (text between the outermost braces), or "" when absent.
|
|
605
|
+
def body
|
|
606
|
+
Internal::CSSRuleText.split_rule(@css_text).last.to_s
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
def rebuild_css_text!
|
|
610
|
+
@css_text = body.strip.empty? ? "#{@selector} {}" : "#{@selector} { #{body.strip} }"
|
|
611
|
+
@prelude = nil
|
|
612
|
+
@parent_style_sheet&.__internal_notify_rule_changed__
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
# Drop derived state after cssText is replaced wholesale.
|
|
616
|
+
def invalidate!
|
|
617
|
+
@prelude = nil
|
|
618
|
+
@selector = nil
|
|
619
|
+
@style = nil
|
|
620
|
+
@css_rules = nil
|
|
621
|
+
@media_list = nil
|
|
622
|
+
end
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
# `MediaList` — the CSSOM list of comma-separated media queries behind
|
|
626
|
+
# `CSSMediaRule#media` (and `<style>`/`<link>`/`CSSStyleSheet#media`).
|
|
627
|
+
# Indexed, with mediaText/append/delete editing; `on_change` (optional) is
|
|
628
|
+
# called with the new media text after any mutation so the owner can persist
|
|
629
|
+
# it.
|
|
630
|
+
class MediaList
|
|
631
|
+
def initialize(media_text = "", on_change: nil)
|
|
632
|
+
@items = parse(media_text)
|
|
633
|
+
@on_change = on_change
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
def length
|
|
637
|
+
@items.length
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
def item(index)
|
|
641
|
+
i = index.to_i
|
|
642
|
+
i.negative? || i >= @items.length ? nil : @items[i]
|
|
643
|
+
end
|
|
644
|
+
|
|
645
|
+
def media_text
|
|
646
|
+
@items.join(", ")
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
def media_text=(text)
|
|
650
|
+
@items = parse(text)
|
|
651
|
+
notify
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
# css-mediaqueries: appendMedium is a no-op when the medium is already
|
|
655
|
+
# present (case-insensitively); deleteMedium removes every match.
|
|
656
|
+
def append_medium(medium)
|
|
657
|
+
medium = medium.to_s.strip
|
|
658
|
+
return if medium.empty? || @items.any? { |existing| existing.casecmp(medium).zero? }
|
|
659
|
+
|
|
660
|
+
@items << medium
|
|
661
|
+
notify
|
|
662
|
+
end
|
|
663
|
+
|
|
664
|
+
def delete_medium(medium)
|
|
665
|
+
medium = medium.to_s.strip
|
|
666
|
+
@items.reject! { |existing| existing.casecmp(medium).zero? }
|
|
667
|
+
notify
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
def to_s
|
|
671
|
+
media_text
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
def __js_get__(key)
|
|
675
|
+
case key
|
|
676
|
+
when "length" then length
|
|
677
|
+
when "mediaText" then media_text
|
|
678
|
+
else
|
|
679
|
+
item(key.to_i) if key.is_a?(Integer) || key.to_s.match?(/\A\d+\z/)
|
|
282
680
|
end
|
|
681
|
+
end
|
|
283
682
|
|
|
683
|
+
def __js_set__(key, value)
|
|
684
|
+
return Bridge::UNHANDLED unless key == "mediaText"
|
|
685
|
+
|
|
686
|
+
# [LegacyNullToEmptyString]: `media.mediaText = null` clears it.
|
|
687
|
+
self.media_text = value.nil? ? "" : value
|
|
284
688
|
nil
|
|
285
689
|
end
|
|
690
|
+
|
|
691
|
+
include Bridge::Methods
|
|
692
|
+
js_methods %w[item appendMedium deleteMedium toString]
|
|
693
|
+
def __js_call__(method, args)
|
|
694
|
+
case method
|
|
695
|
+
when "item" then item(args[0])
|
|
696
|
+
when "appendMedium" then append_medium(args[0])
|
|
697
|
+
when "deleteMedium" then delete_medium(args[0])
|
|
698
|
+
when "toString" then to_s
|
|
699
|
+
end
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
private
|
|
703
|
+
|
|
704
|
+
def parse(text)
|
|
705
|
+
text.to_s.split(",").map(&:strip).reject(&:empty?)
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
def notify
|
|
709
|
+
@on_change&.call(media_text)
|
|
710
|
+
end
|
|
286
711
|
end
|
|
287
712
|
|
|
288
713
|
# `window.CSS` namespace object — `escape()` for safe selector building
|
|
289
|
-
# (used by Turbo and friends) and
|
|
714
|
+
# (used by Turbo and friends) and `supports()` backed by the @supports
|
|
715
|
+
# condition evaluator.
|
|
290
716
|
class CSSNamespace
|
|
291
|
-
def __js_get__(_key) =
|
|
717
|
+
def __js_get__(_key) = Bridge::ABSENT # method-only; any property read is absent
|
|
292
718
|
def __js_set__(_key, _value) = Bridge::UNHANDLED
|
|
293
719
|
|
|
294
720
|
include Bridge::Methods
|
|
@@ -298,7 +724,18 @@ module Dommy
|
|
|
298
724
|
when "escape"
|
|
299
725
|
self.class.escape(args[0])
|
|
300
726
|
when "supports"
|
|
301
|
-
|
|
727
|
+
supports?(args)
|
|
728
|
+
end
|
|
729
|
+
end
|
|
730
|
+
|
|
731
|
+
# CSS.supports(property, value) checks one declaration; CSS.supports(
|
|
732
|
+
# conditionText) evaluates a full <supports-condition>. Both go through the
|
|
733
|
+
# same optimistic evaluator the cascade uses for @supports.
|
|
734
|
+
def supports?(args)
|
|
735
|
+
if args.length >= 2 && !args[1].nil?
|
|
736
|
+
Internal::CSS::Supports.supports_declaration?(args[0], args[1])
|
|
737
|
+
else
|
|
738
|
+
Internal::CSS::Supports.match?(args[0])
|
|
302
739
|
end
|
|
303
740
|
end
|
|
304
741
|
|
|
@@ -97,7 +97,7 @@ module Dommy
|
|
|
97
97
|
end
|
|
98
98
|
|
|
99
99
|
def __js_get__(_key)
|
|
100
|
-
|
|
100
|
+
Bridge::ABSENT # method-only registry; any property read is absent
|
|
101
101
|
end
|
|
102
102
|
|
|
103
103
|
include Bridge::Methods
|
|
@@ -130,7 +130,7 @@ module Dommy
|
|
|
130
130
|
# Match by tag name rather than interpolating `name` into a CSS selector:
|
|
131
131
|
# a spec-valid custom element name may contain "." (a CSS class selector
|
|
132
132
|
# char) or other metacharacters, which would corrupt the query.
|
|
133
|
-
doc.
|
|
133
|
+
doc.backend_doc.css("*").each do |nk|
|
|
134
134
|
next unless nk.name == name
|
|
135
135
|
|
|
136
136
|
doc.__internal_reset_wrapper__(nk)
|