dommy 0.8.0 → 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 +96 -10
- data/lib/dommy/backend/nokogiri_adapter.rb +0 -127
- data/lib/dommy/backend/nokolexbor_adapter.rb +0 -117
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
module Internal
|
|
5
|
+
module CSS
|
|
6
|
+
# Normalizes CSS color values into the serialization browsers use for
|
|
7
|
+
# `getComputedStyle` — `rgb(r, g, b)` / `rgba(r, g, b, a)`.
|
|
8
|
+
#
|
|
9
|
+
# Supported inputs: the 148 CSS Color Module Level 4 named colors,
|
|
10
|
+
# `transparent`, `#rgb` / `#rgba` / `#rrggbb` / `#rrggbbaa` hex
|
|
11
|
+
# notations, and `rgb()` / `rgba()` functional notation. Anything else
|
|
12
|
+
# (hsl(), var(), …) is passed through unchanged.
|
|
13
|
+
module Color
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
# CSS Color Module Level 4 named colors (148 keywords) => [r, g, b].
|
|
17
|
+
NAMED = {
|
|
18
|
+
"aliceblue" => [240, 248, 255],
|
|
19
|
+
"antiquewhite" => [250, 235, 215],
|
|
20
|
+
"aqua" => [0, 255, 255],
|
|
21
|
+
"aquamarine" => [127, 255, 212],
|
|
22
|
+
"azure" => [240, 255, 255],
|
|
23
|
+
"beige" => [245, 245, 220],
|
|
24
|
+
"bisque" => [255, 228, 196],
|
|
25
|
+
"black" => [0, 0, 0],
|
|
26
|
+
"blanchedalmond" => [255, 235, 205],
|
|
27
|
+
"blue" => [0, 0, 255],
|
|
28
|
+
"blueviolet" => [138, 43, 226],
|
|
29
|
+
"brown" => [165, 42, 42],
|
|
30
|
+
"burlywood" => [222, 184, 135],
|
|
31
|
+
"cadetblue" => [95, 158, 160],
|
|
32
|
+
"chartreuse" => [127, 255, 0],
|
|
33
|
+
"chocolate" => [210, 105, 30],
|
|
34
|
+
"coral" => [255, 127, 80],
|
|
35
|
+
"cornflowerblue" => [100, 149, 237],
|
|
36
|
+
"cornsilk" => [255, 248, 220],
|
|
37
|
+
"crimson" => [220, 20, 60],
|
|
38
|
+
"cyan" => [0, 255, 255],
|
|
39
|
+
"darkblue" => [0, 0, 139],
|
|
40
|
+
"darkcyan" => [0, 139, 139],
|
|
41
|
+
"darkgoldenrod" => [184, 134, 11],
|
|
42
|
+
"darkgray" => [169, 169, 169],
|
|
43
|
+
"darkgreen" => [0, 100, 0],
|
|
44
|
+
"darkgrey" => [169, 169, 169],
|
|
45
|
+
"darkkhaki" => [189, 183, 107],
|
|
46
|
+
"darkmagenta" => [139, 0, 139],
|
|
47
|
+
"darkolivegreen" => [85, 107, 47],
|
|
48
|
+
"darkorange" => [255, 140, 0],
|
|
49
|
+
"darkorchid" => [153, 50, 204],
|
|
50
|
+
"darkred" => [139, 0, 0],
|
|
51
|
+
"darksalmon" => [233, 150, 122],
|
|
52
|
+
"darkseagreen" => [143, 188, 143],
|
|
53
|
+
"darkslateblue" => [72, 61, 139],
|
|
54
|
+
"darkslategray" => [47, 79, 79],
|
|
55
|
+
"darkslategrey" => [47, 79, 79],
|
|
56
|
+
"darkturquoise" => [0, 206, 209],
|
|
57
|
+
"darkviolet" => [148, 0, 211],
|
|
58
|
+
"deeppink" => [255, 20, 147],
|
|
59
|
+
"deepskyblue" => [0, 191, 255],
|
|
60
|
+
"dimgray" => [105, 105, 105],
|
|
61
|
+
"dimgrey" => [105, 105, 105],
|
|
62
|
+
"dodgerblue" => [30, 144, 255],
|
|
63
|
+
"firebrick" => [178, 34, 34],
|
|
64
|
+
"floralwhite" => [255, 250, 240],
|
|
65
|
+
"forestgreen" => [34, 139, 34],
|
|
66
|
+
"fuchsia" => [255, 0, 255],
|
|
67
|
+
"gainsboro" => [220, 220, 220],
|
|
68
|
+
"ghostwhite" => [248, 248, 255],
|
|
69
|
+
"gold" => [255, 215, 0],
|
|
70
|
+
"goldenrod" => [218, 165, 32],
|
|
71
|
+
"gray" => [128, 128, 128],
|
|
72
|
+
"green" => [0, 128, 0],
|
|
73
|
+
"greenyellow" => [173, 255, 47],
|
|
74
|
+
"grey" => [128, 128, 128],
|
|
75
|
+
"honeydew" => [240, 255, 240],
|
|
76
|
+
"hotpink" => [255, 105, 180],
|
|
77
|
+
"indianred" => [205, 92, 92],
|
|
78
|
+
"indigo" => [75, 0, 130],
|
|
79
|
+
"ivory" => [255, 255, 240],
|
|
80
|
+
"khaki" => [240, 230, 140],
|
|
81
|
+
"lavender" => [230, 230, 250],
|
|
82
|
+
"lavenderblush" => [255, 240, 245],
|
|
83
|
+
"lawngreen" => [124, 252, 0],
|
|
84
|
+
"lemonchiffon" => [255, 250, 205],
|
|
85
|
+
"lightblue" => [173, 216, 230],
|
|
86
|
+
"lightcoral" => [240, 128, 128],
|
|
87
|
+
"lightcyan" => [224, 255, 255],
|
|
88
|
+
"lightgoldenrodyellow" => [250, 250, 210],
|
|
89
|
+
"lightgray" => [211, 211, 211],
|
|
90
|
+
"lightgreen" => [144, 238, 144],
|
|
91
|
+
"lightgrey" => [211, 211, 211],
|
|
92
|
+
"lightpink" => [255, 182, 193],
|
|
93
|
+
"lightsalmon" => [255, 160, 122],
|
|
94
|
+
"lightseagreen" => [32, 178, 170],
|
|
95
|
+
"lightskyblue" => [135, 206, 250],
|
|
96
|
+
"lightslategray" => [119, 136, 153],
|
|
97
|
+
"lightslategrey" => [119, 136, 153],
|
|
98
|
+
"lightsteelblue" => [176, 196, 222],
|
|
99
|
+
"lightyellow" => [255, 255, 224],
|
|
100
|
+
"lime" => [0, 255, 0],
|
|
101
|
+
"limegreen" => [50, 205, 50],
|
|
102
|
+
"linen" => [250, 240, 230],
|
|
103
|
+
"magenta" => [255, 0, 255],
|
|
104
|
+
"maroon" => [128, 0, 0],
|
|
105
|
+
"mediumaquamarine" => [102, 205, 170],
|
|
106
|
+
"mediumblue" => [0, 0, 205],
|
|
107
|
+
"mediumorchid" => [186, 85, 211],
|
|
108
|
+
"mediumpurple" => [147, 112, 219],
|
|
109
|
+
"mediumseagreen" => [60, 179, 113],
|
|
110
|
+
"mediumslateblue" => [123, 104, 238],
|
|
111
|
+
"mediumspringgreen" => [0, 250, 154],
|
|
112
|
+
"mediumturquoise" => [72, 209, 204],
|
|
113
|
+
"mediumvioletred" => [199, 21, 133],
|
|
114
|
+
"midnightblue" => [25, 25, 112],
|
|
115
|
+
"mintcream" => [245, 255, 250],
|
|
116
|
+
"mistyrose" => [255, 228, 225],
|
|
117
|
+
"moccasin" => [255, 228, 181],
|
|
118
|
+
"navajowhite" => [255, 222, 173],
|
|
119
|
+
"navy" => [0, 0, 128],
|
|
120
|
+
"oldlace" => [253, 245, 230],
|
|
121
|
+
"olive" => [128, 128, 0],
|
|
122
|
+
"olivedrab" => [107, 142, 35],
|
|
123
|
+
"orange" => [255, 165, 0],
|
|
124
|
+
"orangered" => [255, 69, 0],
|
|
125
|
+
"orchid" => [218, 112, 214],
|
|
126
|
+
"palegoldenrod" => [238, 232, 170],
|
|
127
|
+
"palegreen" => [152, 251, 152],
|
|
128
|
+
"paleturquoise" => [175, 238, 238],
|
|
129
|
+
"palevioletred" => [219, 112, 147],
|
|
130
|
+
"papayawhip" => [255, 239, 213],
|
|
131
|
+
"peachpuff" => [255, 218, 185],
|
|
132
|
+
"peru" => [205, 133, 63],
|
|
133
|
+
"pink" => [255, 192, 203],
|
|
134
|
+
"plum" => [221, 160, 221],
|
|
135
|
+
"powderblue" => [176, 224, 230],
|
|
136
|
+
"purple" => [128, 0, 128],
|
|
137
|
+
"rebeccapurple" => [102, 51, 153],
|
|
138
|
+
"red" => [255, 0, 0],
|
|
139
|
+
"rosybrown" => [188, 143, 143],
|
|
140
|
+
"royalblue" => [65, 105, 225],
|
|
141
|
+
"saddlebrown" => [139, 69, 19],
|
|
142
|
+
"salmon" => [250, 128, 114],
|
|
143
|
+
"sandybrown" => [244, 164, 96],
|
|
144
|
+
"seagreen" => [46, 139, 87],
|
|
145
|
+
"seashell" => [255, 245, 238],
|
|
146
|
+
"sienna" => [160, 82, 45],
|
|
147
|
+
"silver" => [192, 192, 192],
|
|
148
|
+
"skyblue" => [135, 206, 235],
|
|
149
|
+
"slateblue" => [106, 90, 205],
|
|
150
|
+
"slategray" => [112, 128, 144],
|
|
151
|
+
"slategrey" => [112, 128, 144],
|
|
152
|
+
"snow" => [255, 250, 250],
|
|
153
|
+
"springgreen" => [0, 255, 127],
|
|
154
|
+
"steelblue" => [70, 130, 180],
|
|
155
|
+
"tan" => [210, 180, 140],
|
|
156
|
+
"teal" => [0, 128, 128],
|
|
157
|
+
"thistle" => [216, 191, 216],
|
|
158
|
+
"tomato" => [255, 99, 71],
|
|
159
|
+
"turquoise" => [64, 224, 208],
|
|
160
|
+
"violet" => [238, 130, 238],
|
|
161
|
+
"wheat" => [245, 222, 179],
|
|
162
|
+
"white" => [255, 255, 255],
|
|
163
|
+
"whitesmoke" => [245, 245, 245],
|
|
164
|
+
"yellow" => [255, 255, 0],
|
|
165
|
+
"yellowgreen" => [154, 205, 50]
|
|
166
|
+
}.freeze
|
|
167
|
+
|
|
168
|
+
HEX_PATTERN = /\A#(\h{3}|\h{4}|\h{6}|\h{8})\z/
|
|
169
|
+
private_constant :HEX_PATTERN
|
|
170
|
+
|
|
171
|
+
# rgb()/rgba()/hsl()/hsla() in either legacy comma syntax or modern
|
|
172
|
+
# space syntax (`rgb(R G B / A)`), with integer or percentage channels.
|
|
173
|
+
COLOR_FN_PATTERN = /\A(rgba?|hsla?)\((.*)\)\z/im
|
|
174
|
+
private_constant :COLOR_FN_PATTERN
|
|
175
|
+
|
|
176
|
+
# Normalizes +value+ to the computed-style serialization.
|
|
177
|
+
# Unrecognized values are returned as-is (never raises).
|
|
178
|
+
#
|
|
179
|
+
# @param value [String] a CSS color value, e.g. "Red", "#0f0", "rgb(0,0,0)"
|
|
180
|
+
# @return [String] "rgb(r, g, b)", "rgba(r, g, b, a)", or +value+ itself
|
|
181
|
+
def normalize(value)
|
|
182
|
+
stripped = value.strip
|
|
183
|
+
lower = stripped.downcase
|
|
184
|
+
|
|
185
|
+
return "rgba(0, 0, 0, 0)" if lower == "transparent"
|
|
186
|
+
|
|
187
|
+
if (rgb = NAMED[lower])
|
|
188
|
+
return serialize(*rgb)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
if (match = HEX_PATTERN.match(lower))
|
|
192
|
+
return from_hex(match[1])
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
if (match = COLOR_FN_PATTERN.match(stripped))
|
|
196
|
+
return normalize_function(match[1].downcase, match[2]) || value
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
value
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Extracts the first color token from a compound value (used by
|
|
203
|
+
# shorthand expansion, e.g. `background: red url(x)`). Returns the
|
|
204
|
+
# normalized color, or nil when no recognizable color is present.
|
|
205
|
+
#
|
|
206
|
+
# @param value [String] a whitespace-separated CSS shorthand value
|
|
207
|
+
# @return [String, nil]
|
|
208
|
+
def extract(value)
|
|
209
|
+
top_level_tokens(value).each do |token|
|
|
210
|
+
lower = token.downcase
|
|
211
|
+
if token.include?("(")
|
|
212
|
+
# Function tokens are skipped wholesale (url(#abc),
|
|
213
|
+
# linear-gradient(red, blue), …) — except rgb()/rgba(),
|
|
214
|
+
# which are themselves colors.
|
|
215
|
+
return normalize(token) if lower.start_with?("rgb(", "rgba(")
|
|
216
|
+
elsif HEX_PATTERN.match?(lower) || lower == "transparent" || NAMED.key?(lower)
|
|
217
|
+
return normalize(token)
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
nil
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Splits +value+ into top-level tokens: whitespace separates tokens
|
|
224
|
+
# only outside parentheses, so a function and its arguments (e.g.
|
|
225
|
+
# `linear-gradient(to right, red)`) stay one token.
|
|
226
|
+
def top_level_tokens(value)
|
|
227
|
+
tokens = []
|
|
228
|
+
current = +""
|
|
229
|
+
depth = 0
|
|
230
|
+
value.each_char do |char|
|
|
231
|
+
case char
|
|
232
|
+
when "("
|
|
233
|
+
depth += 1
|
|
234
|
+
current << char
|
|
235
|
+
when ")"
|
|
236
|
+
depth -= 1 if depth.positive?
|
|
237
|
+
current << char
|
|
238
|
+
when /\s/
|
|
239
|
+
if depth.positive?
|
|
240
|
+
current << char
|
|
241
|
+
elsif !current.empty?
|
|
242
|
+
tokens << current
|
|
243
|
+
current = +""
|
|
244
|
+
end
|
|
245
|
+
else
|
|
246
|
+
current << char
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
tokens << current unless current.empty?
|
|
250
|
+
tokens
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Normalize an rgb()/hsl() function body to rgb()/rgba(). Returns nil
|
|
254
|
+
# when the channels don't parse (the caller keeps the original text).
|
|
255
|
+
def normalize_function(name, args)
|
|
256
|
+
channels, alpha = split_color_args(args)
|
|
257
|
+
return nil unless channels.length == 3
|
|
258
|
+
|
|
259
|
+
rgb =
|
|
260
|
+
if name.start_with?("rgb")
|
|
261
|
+
channels.map { |channel| rgb_channel(channel) }
|
|
262
|
+
else
|
|
263
|
+
hsl_to_rgb(hue(channels[0]), percentage(channels[1]), percentage(channels[2]))
|
|
264
|
+
end
|
|
265
|
+
return nil if rgb.any?(&:nil?)
|
|
266
|
+
|
|
267
|
+
serialize(*rgb, alpha)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# [[c1, c2, c3], alpha_or_nil]. Channels are comma- or space-separated;
|
|
271
|
+
# a `/` introduces the alpha (modern syntax), as does a 4th legacy comma
|
|
272
|
+
# value.
|
|
273
|
+
def split_color_args(args)
|
|
274
|
+
args = args.strip
|
|
275
|
+
if args.include?("/")
|
|
276
|
+
main, alpha = args.split("/", 2)
|
|
277
|
+
[main.strip.split(/[\s,]+/), parse_alpha(alpha.strip)]
|
|
278
|
+
else
|
|
279
|
+
parts = args.split(/[\s,]+/)
|
|
280
|
+
parts.length == 4 ? [parts[0, 3], parse_alpha(parts[3])] : [parts, nil]
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# The css-color-4 `none` keyword marks a missing component; in the
|
|
285
|
+
# legacy rgb()/rgba() serialization a computed color uses 0 for it.
|
|
286
|
+
def none?(text) = text.casecmp("none").zero?
|
|
287
|
+
|
|
288
|
+
def rgb_channel(channel)
|
|
289
|
+
return 0 if none?(channel)
|
|
290
|
+
|
|
291
|
+
if channel.end_with?("%")
|
|
292
|
+
value = Float(channel[0..-2]) / 100.0 * 255
|
|
293
|
+
else
|
|
294
|
+
value = Float(channel)
|
|
295
|
+
end
|
|
296
|
+
value.round.clamp(0, 255)
|
|
297
|
+
rescue ArgumentError, TypeError
|
|
298
|
+
nil
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def parse_alpha(text)
|
|
302
|
+
return 0.0 if none?(text)
|
|
303
|
+
|
|
304
|
+
value = text.end_with?("%") ? Float(text[0..-2]) / 100.0 : Float(text)
|
|
305
|
+
value.clamp(0.0, 1.0)
|
|
306
|
+
rescue ArgumentError, TypeError
|
|
307
|
+
nil
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# An <hue>: a bare number or <angle> (deg/grad/rad/turn), wrapped into
|
|
311
|
+
# [0, 360). nil when unparseable.
|
|
312
|
+
def hue(text)
|
|
313
|
+
text = text.downcase
|
|
314
|
+
return 0.0 if none?(text)
|
|
315
|
+
|
|
316
|
+
degrees =
|
|
317
|
+
case text
|
|
318
|
+
when /\A(-?\d*\.?\d+)deg\z/ then Float(Regexp.last_match(1))
|
|
319
|
+
when /\A(-?\d*\.?\d+)grad\z/ then Float(Regexp.last_match(1)) * 0.9
|
|
320
|
+
when /\A(-?\d*\.?\d+)rad\z/ then Float(Regexp.last_match(1)) * 180 / Math::PI
|
|
321
|
+
when /\A(-?\d*\.?\d+)turn\z/ then Float(Regexp.last_match(1)) * 360
|
|
322
|
+
when /\A-?\d*\.?\d+\z/ then Float(text)
|
|
323
|
+
end
|
|
324
|
+
degrees && degrees % 360
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def percentage(text)
|
|
328
|
+
return 0.0 if none?(text)
|
|
329
|
+
return nil unless text.end_with?("%")
|
|
330
|
+
|
|
331
|
+
(Float(text[0..-2]) / 100.0).clamp(0.0, 1.0)
|
|
332
|
+
rescue ArgumentError, TypeError
|
|
333
|
+
nil
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# HSL -> [r, g, b] (0..255). h in degrees, s/l in 0..1.
|
|
337
|
+
def hsl_to_rgb(h, s, l)
|
|
338
|
+
return [nil] if h.nil? || s.nil? || l.nil?
|
|
339
|
+
|
|
340
|
+
chroma = (1 - (2 * l - 1).abs) * s
|
|
341
|
+
secondary = chroma * (1 - ((h / 60.0) % 2 - 1).abs)
|
|
342
|
+
match = l - chroma / 2
|
|
343
|
+
r, g, b =
|
|
344
|
+
case h
|
|
345
|
+
when 0...60 then [chroma, secondary, 0]
|
|
346
|
+
when 60...120 then [secondary, chroma, 0]
|
|
347
|
+
when 120...180 then [0, chroma, secondary]
|
|
348
|
+
when 180...240 then [0, secondary, chroma]
|
|
349
|
+
when 240...300 then [secondary, 0, chroma]
|
|
350
|
+
else [chroma, 0, secondary]
|
|
351
|
+
end
|
|
352
|
+
[r, g, b].map { |channel| ((channel + match) * 255).round }
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# Expands a 3/4/6/8-digit hex body into the rgb()/rgba() form.
|
|
356
|
+
def from_hex(hex)
|
|
357
|
+
hex = hex.chars.map { |c| c * 2 }.join if hex.length <= 4
|
|
358
|
+
r, g, b, a = hex.scan(/\h{2}/).map { |pair| pair.to_i(16) }
|
|
359
|
+
serialize(r, g, b, a && a / 255.0)
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# Builds "rgb(r, g, b)" or, when alpha is present and not 1,
|
|
363
|
+
# "rgba(r, g, b, a)".
|
|
364
|
+
def serialize(r, g, b, alpha = nil)
|
|
365
|
+
return "rgb(#{r}, #{g}, #{b})" if alpha.nil? || alpha == 1
|
|
366
|
+
|
|
367
|
+
"rgba(#{r}, #{g}, #{b}, #{format_alpha(alpha)})"
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
# Formats an alpha component: round to 5 decimals, drop trailing
|
|
371
|
+
# zeros, and serialize integral values without a decimal point.
|
|
372
|
+
def format_alpha(alpha)
|
|
373
|
+
rounded = alpha.round(5)
|
|
374
|
+
return rounded.to_i.to_s if rounded == rounded.to_i
|
|
375
|
+
|
|
376
|
+
rounded.to_s
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
module Internal
|
|
5
|
+
module CSS
|
|
6
|
+
# The read-only CSSStyleDeclaration that getComputedStyle returns.
|
|
7
|
+
# CSSOM's "live object" behavior is approximated by recomputing lazily:
|
|
8
|
+
# every read goes through Cascade.computed_style, whose per-generation
|
|
9
|
+
# memo makes repeated reads cheap while DOM mutations show up on the
|
|
10
|
+
# next read. ::before/::after expose cascaded computed declarations even
|
|
11
|
+
# though Dommy renders no generated boxes.
|
|
12
|
+
class ComputedStyleDeclaration
|
|
13
|
+
EMPTY = {}.freeze
|
|
14
|
+
|
|
15
|
+
# Pseudo-elements getComputedStyle exposes a cascaded computed
|
|
16
|
+
# declaration for (no box generation, just the cascade). Unknown
|
|
17
|
+
# pseudo-elements yield an empty declaration, as browsers do.
|
|
18
|
+
KNOWN_PSEUDO_ELEMENTS = %w[
|
|
19
|
+
before after first-line first-letter marker selection placeholder backdrop
|
|
20
|
+
].freeze
|
|
21
|
+
|
|
22
|
+
def initialize(element, pseudo_element: nil)
|
|
23
|
+
@element = element
|
|
24
|
+
@pseudo_element = pseudo_element
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def get_property_value(name)
|
|
28
|
+
key = name.to_s
|
|
29
|
+
# Custom properties are case-sensitive; everything else isn't.
|
|
30
|
+
key = key.downcase unless key.start_with?("--")
|
|
31
|
+
styles[key].to_s
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def [](name)
|
|
35
|
+
get_property_value(name)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def length
|
|
39
|
+
styles.size
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def item(index)
|
|
43
|
+
styles.keys[index].to_s
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def css_text
|
|
47
|
+
styles.map { |name, value| "#{name}: #{value};" }.join(" ")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def to_h
|
|
51
|
+
styles.dup
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def set_property(*, **)
|
|
55
|
+
raise_read_only
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def remove_property(*)
|
|
59
|
+
raise_read_only
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# --- JS bridge ---
|
|
63
|
+
|
|
64
|
+
def __js_get__(key)
|
|
65
|
+
case key
|
|
66
|
+
when "cssText" then css_text
|
|
67
|
+
when "length" then length
|
|
68
|
+
when "parentRule" then nil
|
|
69
|
+
else
|
|
70
|
+
# An unset/unknown property reads as "" per CSSStyleDeclaration
|
|
71
|
+
# (a detached element's whole declaration is empty), not undefined.
|
|
72
|
+
styles[camel_to_kebab(key)].to_s
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def __js_set__(_key, _value)
|
|
77
|
+
Bridge::UNHANDLED
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
include Bridge::Methods
|
|
81
|
+
js_methods %w[getPropertyValue getPropertyPriority item setProperty removeProperty]
|
|
82
|
+
def __js_call__(method, args)
|
|
83
|
+
case method
|
|
84
|
+
when "getPropertyValue"
|
|
85
|
+
get_property_value(args[0])
|
|
86
|
+
when "getPropertyPriority"
|
|
87
|
+
""
|
|
88
|
+
when "item"
|
|
89
|
+
item(args[0].to_i)
|
|
90
|
+
when "setProperty", "removeProperty"
|
|
91
|
+
raise_read_only
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Ruby-side property readers, e.g. `computed.background_color` or
|
|
96
|
+
# `computed.backgroundColor`.
|
|
97
|
+
def method_missing(name, *args)
|
|
98
|
+
raise_read_only if name.to_s.end_with?("=")
|
|
99
|
+
return super unless args.empty?
|
|
100
|
+
|
|
101
|
+
styles.fetch(camel_to_kebab(name.to_s)) { return super }.to_s
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def respond_to_missing?(name, include_private = false)
|
|
105
|
+
styles.key?(camel_to_kebab(name.to_s)) || super
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
def styles
|
|
111
|
+
if @pseudo_element
|
|
112
|
+
name = @pseudo_element.to_s.delete_prefix("::").delete_prefix(":")
|
|
113
|
+
return EMPTY unless KNOWN_PSEUDO_ELEMENTS.include?(name)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
Cascade.computed_style(@element, pseudo_element: @pseudo_element)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def camel_to_kebab(name)
|
|
120
|
+
name.to_s.tr("_", "-").gsub(/([A-Z])/) { "-#{Regexp.last_match(1).downcase}" }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def raise_read_only
|
|
124
|
+
raise DOMException::NoModificationAllowedError,
|
|
125
|
+
"Cannot modify a computed style declaration"
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|