dommy 0.5.0 → 0.6.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 +30 -4
- data/lib/dommy/animation.rb +288 -0
- data/lib/dommy/compression_streams.rb +147 -0
- data/lib/dommy/cookie_store.rb +128 -0
- data/lib/dommy/crypto.rb +395 -0
- data/lib/dommy/document.rb +93 -1
- data/lib/dommy/element.rb +131 -9
- data/lib/dommy/event.rb +370 -0
- data/lib/dommy/event_source.rb +131 -0
- data/lib/dommy/fetch.rb +62 -0
- data/lib/dommy/file_reader.rb +176 -0
- data/lib/dommy/history.rb +79 -0
- data/lib/dommy/html_elements.rb +20 -25
- data/lib/dommy/internal/cookie_jar.rb +2 -0
- data/lib/dommy/internal/dom_matching.rb +1 -1
- data/lib/dommy/internal/idna.rb +443 -0
- data/lib/dommy/internal/idna_data.rb +10379 -0
- data/lib/dommy/internal/ipv4_parser.rb +78 -0
- data/lib/dommy/internal/node_wrapper_cache.rb +1 -1
- data/lib/dommy/internal/observable_callback.rb +25 -0
- data/lib/dommy/internal/punycode.rb +202 -0
- data/lib/dommy/internal/range_text_serializer.rb +72 -0
- data/lib/dommy/internal/reflected_attributes.rb +45 -0
- data/lib/dommy/intersection_observer.rb +82 -0
- data/lib/dommy/{router.rb → location.rb} +0 -138
- data/lib/dommy/media_query_list.rb +118 -0
- data/lib/dommy/message_channel.rb +249 -0
- data/lib/dommy/navigator.rb +361 -1
- data/lib/dommy/notification.rb +89 -0
- data/lib/dommy/performance.rb +146 -0
- data/lib/dommy/performance_observer.rb +55 -0
- data/lib/dommy/range.rb +597 -0
- data/lib/dommy/resize_observer.rb +53 -0
- data/lib/dommy/streams.rb +386 -0
- data/lib/dommy/svg_elements.rb +3863 -0
- data/lib/dommy/text_codec.rb +175 -0
- data/lib/dommy/url.rb +249 -21
- data/lib/dommy/url_pattern.rb +144 -0
- data/lib/dommy/version.rb +1 -1
- data/lib/dommy/web_socket.rb +209 -0
- data/lib/dommy/{world.rb → window.rb} +149 -2
- data/lib/dommy/worker.rb +143 -0
- data/lib/dommy/xml_http_request.rb +423 -0
- data/lib/dommy.rb +31 -3
- metadata +34 -5
- /data/lib/dommy/{observer.rb → mutation_observer.rb} +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7ce9aec097da24f1f6a44792f44615605a1405483b6c6c5183031757d11a0853
|
|
4
|
+
data.tar.gz: cf7c0e05839c313763590fcdb4d134459f5b1fe91ed1b479f819b18181cf1058
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0db6e083fb62dd609fd719264a4d35e6bcadd6b97ce8515d08a8a8a53028e1f3f870224ef40ca33240688ddaf782d41e09d261c079c145144fe7b47b83288ef6
|
|
7
|
+
data.tar.gz: d33e89e420bf0f93c2e478c93dd9241643ee47c40efa40927ff77714f20b92af561330b2cbedcaa287951cd07aab4ecfb899e24371fe8d6e2b20f289f6973b53
|
data/README.md
CHANGED
|
@@ -177,6 +177,7 @@ Implemented:
|
|
|
177
177
|
|
|
178
178
|
- Core DOM (Document, Element, Text/Comment/Fragment, NodeList, Attr)
|
|
179
179
|
- 26 specialized HTMLElement subclasses
|
|
180
|
+
- SVG: SVGElement base + ~63 specialized subclasses covering shapes, gradients, marker / mask, the full set of standard filter primitives (Gaussian blur, offset, blend, color matrix, flood, composite, merge, component transfer, tile, morphology, image, drop shadow, turbulence, displacement map, convolve matrix, diffuse / specular lighting + light sources), `<a>` / `<textPath>` / `<view>` / `<switch>` / `<metadata>`, and SMIL animation (`<animate>` / `<animateTransform>` / `<animateMotion>` / `<set>` / `<mpath>` / `<discard>`) — with case-sensitive attribute round-trip
|
|
180
181
|
- events with composedPath / AbortSignal
|
|
181
182
|
- MutationObserver (childList / attributes / characterData / subtree)
|
|
182
183
|
- Custom Elements lifecycle
|
|
@@ -184,12 +185,36 @@ Implemented:
|
|
|
184
185
|
- form validation
|
|
185
186
|
- Scheduler (timers + microtasks with `advance_time`)
|
|
186
187
|
- Promise
|
|
187
|
-
- Location / History / URL
|
|
188
|
+
- Location / History / URL (WHATWG-leaning parsing with whitespace/tab/newline stripping, percent-encoding of unsafe path chars, `\` → `/` conversion for special schemes, `./` and `../` resolution, IPv4 number forms normalization, ws/wss default port stripping, Punycode hostname encoding, full UTS #46 IDNA with RFC 5893 Bidi and RFC 5892 ContextJ/ContextO)
|
|
188
189
|
- Storage
|
|
189
|
-
- fetch (stub)
|
|
190
|
+
- fetch (stub) / XMLHttpRequest (stub-driven, sync + async, shares the `__fetchy_stub__` fixture map)
|
|
191
|
+
- WebSocket / EventSource — test seams (`__simulate_open__` / `__simulate_message__` / `__simulate_close__`) drive the streams
|
|
192
|
+
- MessageChannel / MessagePort / BroadcastChannel — in-process pub/sub with `structuredClone`-on-transfer
|
|
193
|
+
- FileReader (`readAsText` / `readAsDataURL` / `readAsArrayBuffer` / `readAsBinaryString`)
|
|
194
|
+
- Notification (permission settable via `Notification.__set_permission__`)
|
|
195
|
+
- Geolocation (mock position via `navigator.geolocation.__set_position__`)
|
|
196
|
+
- `window.matchMedia` returning a `MediaQueryList` (`__set_matches__` flips and fires `change`)
|
|
197
|
+
- `requestIdleCallback` / `cancelIdleCallback` (modelled on scheduler), `structuredClone` global
|
|
198
|
+
- `crypto.subtle.digest` (SHA-1/256/384/512), HMAC sign/verify/import/generateKey, and AES-GCM encrypt/decrypt (128/256-bit with additionalData and tagLength options)
|
|
199
|
+
- Streams API (`ReadableStream` / `WritableStream` / `TransformStream` + `TextEncoderStream` / `TextDecoderStream`)
|
|
200
|
+
- `CompressionStream` / `DecompressionStream` (gzip / deflate / deflate-raw via Ruby `Zlib`)
|
|
201
|
+
- Worker (inline-emulated — same-process message round-trip; tests register handlers via `worker.__on_message__`)
|
|
202
|
+
- Performance User Timing (`performance.mark` / `measure` / `getEntriesByName`)
|
|
203
|
+
- `cookieStore` (async Cookie Store API, backed by the same jar `document.cookie` uses)
|
|
204
|
+
- Navigator extras: `share` / `vibrate` / `wakeLock.request` / `getBattery` / `locks` (Web Locks) / `storage.estimate` / `persist` / `persisted` (StorageManager)
|
|
205
|
+
- Layout-adjacent stubs: `element.scrollIntoView` / `scrollTo` / scroll & client / offset metrics (return 0), `getComputedStyle` (inline style passthrough)
|
|
206
|
+
- Popover API (`showPopover` / `hidePopover` / `togglePopover` with `beforetoggle` / `toggle` events)
|
|
207
|
+
- Fullscreen API (`element.requestFullscreen` / `document.exitFullscreen` / `fullscreenchange`)
|
|
208
|
+
- `URLPattern` (pattern matching for URL components — protocol, username, password, hostname, port, pathname, search, hash — with named capture groups and modifiers: `:id`, `*`, `:version+`, `:version?`)
|
|
209
|
+
- `document.startViewTransition` (View Transitions API stub)
|
|
190
210
|
- Navigator / Clipboard
|
|
191
211
|
- TreeWalker / NodeIterator / NodeFilter
|
|
192
212
|
- File API (Blob / File / FileList / FormData / DataTransfer)
|
|
213
|
+
- Web Crypto (`crypto.randomUUID`, `getRandomValues`) / TextEncoder / TextDecoder
|
|
214
|
+
- IntersectionObserver / ResizeObserver / PerformanceObserver (test-driven `__trigger__`)
|
|
215
|
+
- Range / Selection (DOM-level only, no layout)
|
|
216
|
+
- Web Animations API (Animation / KeyframeEffect; lifecycle via scheduler, finished/ready Promises)
|
|
217
|
+
- Extended events: Touch / Clipboard / Composition / Wheel / Focus / BeforeUnload / Input / Pointer / Progress / Drag
|
|
193
218
|
|
|
194
219
|
> [!IMPORTANT]
|
|
195
220
|
> Out of scope:
|
|
@@ -198,8 +223,9 @@ Implemented:
|
|
|
198
223
|
> - CSS scoping (`:host`, `::slotted`, computed styles)
|
|
199
224
|
> - JS evaluation
|
|
200
225
|
> - Canvas / WebGL / media playback
|
|
201
|
-
> - layout-dependent Range / Selection
|
|
202
|
-
> - SVG
|
|
226
|
+
> - layout-dependent Range / Selection geometry (`getBoundingClientRect` returns zero rects)
|
|
227
|
+
> - SVG-specific value types (SVGAnimatedLength, SVGTransform, SVGMatrix)
|
|
228
|
+
> - Web Animations: no actual value interpolation — `Animation` is a state machine (`idle` / `running` / `paused` / `finished`) for testing lifecycle and event wiring
|
|
203
229
|
|
|
204
230
|
## Running the tests
|
|
205
231
|
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
# `KeyframeEffect` — wraps a target element + keyframes + timing.
|
|
5
|
+
# Dommy doesn't interpolate values (no layout / render); the
|
|
6
|
+
# effect is just a record of what the animation describes.
|
|
7
|
+
#
|
|
8
|
+
# Spec: https://drafts.csswg.org/web-animations/#keyframeeffect
|
|
9
|
+
class KeyframeEffect
|
|
10
|
+
attr_reader :target, :keyframes
|
|
11
|
+
|
|
12
|
+
def initialize(target, keyframes, options = nil)
|
|
13
|
+
@target = target
|
|
14
|
+
@keyframes = case keyframes
|
|
15
|
+
when nil
|
|
16
|
+
[]
|
|
17
|
+
when Array
|
|
18
|
+
keyframes
|
|
19
|
+
else
|
|
20
|
+
[keyframes]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
@timing = normalize_timing(options)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def get_timing
|
|
27
|
+
@timing.dup
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
alias getTiming get_timing
|
|
31
|
+
|
|
32
|
+
def update_timing(timing)
|
|
33
|
+
@timing.merge!(timing.transform_keys(&:to_s))
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
alias updateTiming update_timing
|
|
38
|
+
|
|
39
|
+
def duration_ms
|
|
40
|
+
d = @timing["duration"]
|
|
41
|
+
d.is_a?(Numeric) ? d.to_i : 0
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def __js_get__(key)
|
|
45
|
+
case key
|
|
46
|
+
when "target"
|
|
47
|
+
@target
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def __js_call__(method, args)
|
|
52
|
+
case method
|
|
53
|
+
when "getTiming"
|
|
54
|
+
get_timing
|
|
55
|
+
when "updateTiming"
|
|
56
|
+
update_timing(args[0] || {})
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def normalize_timing(input)
|
|
63
|
+
case input
|
|
64
|
+
when nil
|
|
65
|
+
{"duration" => 0}
|
|
66
|
+
when Numeric
|
|
67
|
+
{"duration" => input.to_i}
|
|
68
|
+
when Hash
|
|
69
|
+
h = input.transform_keys(&:to_s)
|
|
70
|
+
h["duration"] = (h["duration"] || 0).is_a?(Numeric) ? h["duration"].to_i : 0
|
|
71
|
+
h
|
|
72
|
+
else
|
|
73
|
+
{"duration" => 0}
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# `Animation` — represents a running animation, mirroring the Web
|
|
79
|
+
# Animations API's lifecycle and event surface. Dommy doesn't
|
|
80
|
+
# interpolate any property values; the animation transitions
|
|
81
|
+
# through "idle" → "running" → "finished" by either virtual time
|
|
82
|
+
# (`scheduler.advance_time`) or by an explicit `finish()` call.
|
|
83
|
+
#
|
|
84
|
+
# Spec: https://drafts.csswg.org/web-animations/#animation
|
|
85
|
+
class Animation
|
|
86
|
+
include EventTarget
|
|
87
|
+
|
|
88
|
+
attr_accessor :id
|
|
89
|
+
attr_reader :effect, :timeline, :play_state
|
|
90
|
+
|
|
91
|
+
def initialize(effect = nil, timeline = nil, window: nil)
|
|
92
|
+
@effect = effect
|
|
93
|
+
@timeline = timeline
|
|
94
|
+
@window = window
|
|
95
|
+
@play_state = "idle"
|
|
96
|
+
@playback_rate = 1.0
|
|
97
|
+
@current_time = nil
|
|
98
|
+
@start_time = nil
|
|
99
|
+
@id = ""
|
|
100
|
+
@finished_promise = nil
|
|
101
|
+
@ready_promise = nil
|
|
102
|
+
@scheduled_finish_id = nil
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def current_time
|
|
106
|
+
@current_time
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def current_time=(value)
|
|
110
|
+
@current_time = value
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def start_time
|
|
114
|
+
@start_time
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def start_time=(value)
|
|
118
|
+
@start_time = value
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def playback_rate
|
|
122
|
+
@playback_rate
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def playback_rate=(value)
|
|
126
|
+
@playback_rate = value.to_f
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Start (or resume) the animation. Returns self.
|
|
130
|
+
def play
|
|
131
|
+
return self if @play_state == "running"
|
|
132
|
+
|
|
133
|
+
previous = @play_state
|
|
134
|
+
@play_state = "running"
|
|
135
|
+
@start_time ||= @window&.scheduler&.now_ms || 0
|
|
136
|
+
ensure_ready_resolved
|
|
137
|
+
schedule_auto_finish if previous != "paused"
|
|
138
|
+
self
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def pause
|
|
142
|
+
cancel_scheduled_finish
|
|
143
|
+
@play_state = "paused" unless @play_state == "idle"
|
|
144
|
+
self
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def cancel
|
|
148
|
+
cancel_scheduled_finish
|
|
149
|
+
@play_state = "idle"
|
|
150
|
+
@current_time = nil
|
|
151
|
+
reject_finished_with_abort
|
|
152
|
+
dispatch_event(Event.new("cancel"))
|
|
153
|
+
self
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def finish
|
|
157
|
+
cancel_scheduled_finish
|
|
158
|
+
@play_state = "finished"
|
|
159
|
+
@current_time = effect_duration_ms
|
|
160
|
+
resolve_finished
|
|
161
|
+
dispatch_event(Event.new("finish"))
|
|
162
|
+
self
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def reverse
|
|
166
|
+
@playback_rate = -@playback_rate
|
|
167
|
+
play if @play_state == "idle"
|
|
168
|
+
self
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# PromiseValue that resolves when the animation finishes.
|
|
172
|
+
# Rejected (with AbortError-style RuntimeError) on cancel.
|
|
173
|
+
def finished
|
|
174
|
+
@finished_promise ||= PromiseValue.new(@window)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# PromiseValue that resolves once the animation is ready to play
|
|
178
|
+
# (immediately in Dommy — there's no render-thread handoff).
|
|
179
|
+
def ready
|
|
180
|
+
@ready_promise ||= if @window
|
|
181
|
+
PromiseValue.resolve(@window, self)
|
|
182
|
+
else
|
|
183
|
+
PromiseValue.new(@window)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def __js_get__(key)
|
|
188
|
+
case key
|
|
189
|
+
when "playState"
|
|
190
|
+
@play_state
|
|
191
|
+
when "playbackRate"
|
|
192
|
+
@playback_rate
|
|
193
|
+
when "currentTime"
|
|
194
|
+
@current_time
|
|
195
|
+
when "startTime"
|
|
196
|
+
@start_time
|
|
197
|
+
when "effect"
|
|
198
|
+
@effect
|
|
199
|
+
when "timeline"
|
|
200
|
+
@timeline
|
|
201
|
+
when "finished"
|
|
202
|
+
finished
|
|
203
|
+
when "ready"
|
|
204
|
+
ready
|
|
205
|
+
when "id"
|
|
206
|
+
@id
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def __js_set__(key, value)
|
|
211
|
+
case key
|
|
212
|
+
when "currentTime"
|
|
213
|
+
@current_time = value
|
|
214
|
+
when "startTime"
|
|
215
|
+
@start_time = value
|
|
216
|
+
when "playbackRate"
|
|
217
|
+
@playback_rate = value.to_f
|
|
218
|
+
when "id"
|
|
219
|
+
@id = value.to_s
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
nil
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def __js_call__(method, args)
|
|
226
|
+
case method
|
|
227
|
+
when "play"
|
|
228
|
+
play
|
|
229
|
+
when "pause"
|
|
230
|
+
pause
|
|
231
|
+
when "cancel"
|
|
232
|
+
cancel
|
|
233
|
+
when "finish"
|
|
234
|
+
finish
|
|
235
|
+
when "reverse"
|
|
236
|
+
reverse
|
|
237
|
+
when "addEventListener"
|
|
238
|
+
add_event_listener(args[0], args[1], args[2])
|
|
239
|
+
when "removeEventListener"
|
|
240
|
+
remove_event_listener(args[0], args[1])
|
|
241
|
+
when "dispatchEvent"
|
|
242
|
+
dispatch_event(args[0])
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Event bubbling stops at Animation — it isn't part of the DOM tree.
|
|
247
|
+
def __event_parent__
|
|
248
|
+
nil
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
private
|
|
252
|
+
|
|
253
|
+
def effect_duration_ms
|
|
254
|
+
@effect ? @effect.duration_ms : 0
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def schedule_auto_finish
|
|
258
|
+
return unless @window&.scheduler
|
|
259
|
+
return if effect_duration_ms <= 0
|
|
260
|
+
|
|
261
|
+
@scheduled_finish_id = @window.scheduler.set_timeout(
|
|
262
|
+
proc { finish if @play_state == "running" },
|
|
263
|
+
effect_duration_ms
|
|
264
|
+
)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def cancel_scheduled_finish
|
|
268
|
+
return unless @scheduled_finish_id && @window&.scheduler
|
|
269
|
+
|
|
270
|
+
@window.scheduler.clear_timeout(@scheduled_finish_id)
|
|
271
|
+
@scheduled_finish_id = nil
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def ensure_ready_resolved
|
|
275
|
+
ready.fulfill(self) if ready.respond_to?(:fulfill)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def resolve_finished
|
|
279
|
+
finished.fulfill(self)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def reject_finished_with_abort
|
|
283
|
+
return unless @finished_promise
|
|
284
|
+
|
|
285
|
+
@finished_promise.reject(RuntimeError.new("AbortError: animation cancelled"))
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "zlib"
|
|
4
|
+
require "stringio"
|
|
5
|
+
|
|
6
|
+
module Dommy
|
|
7
|
+
# `CompressionStream` / `DecompressionStream` — wrap `TransformStream`
|
|
8
|
+
# over Ruby's `Zlib` to gzip/deflate/raw-deflate byte chunks. Each
|
|
9
|
+
# `write(chunk)` accumulates into an internal buffer; `close()`
|
|
10
|
+
# finalizes and emits the compressed/decompressed bytes downstream.
|
|
11
|
+
#
|
|
12
|
+
# Spec: https://wicg.github.io/compression/
|
|
13
|
+
class CompressionStream
|
|
14
|
+
SUPPORTED = %w[gzip deflate deflate-raw].freeze
|
|
15
|
+
|
|
16
|
+
attr_reader :readable, :writable
|
|
17
|
+
|
|
18
|
+
def initialize(window, format)
|
|
19
|
+
raise ArgumentError, "unsupported format #{format.inspect}" unless SUPPORTED.include?(format.to_s)
|
|
20
|
+
|
|
21
|
+
@buffer = +""
|
|
22
|
+
compressor = build_compressor(format.to_s)
|
|
23
|
+
|
|
24
|
+
@readable = ReadableStream.new(window)
|
|
25
|
+
controller = TransformStreamDefaultController.new(@readable)
|
|
26
|
+
|
|
27
|
+
@writable = WritableStream.new(
|
|
28
|
+
window,
|
|
29
|
+
{
|
|
30
|
+
"write" => proc { |chunk| @buffer << coerce(chunk) },
|
|
31
|
+
"close" => proc do
|
|
32
|
+
compressed = compressor.call(@buffer)
|
|
33
|
+
controller.enqueue(compressed)
|
|
34
|
+
@readable.__close__
|
|
35
|
+
end,
|
|
36
|
+
"abort" => proc { |r| @readable.__error__(r) }
|
|
37
|
+
}
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def __js_get__(key)
|
|
42
|
+
case key
|
|
43
|
+
when "readable"
|
|
44
|
+
@readable
|
|
45
|
+
when "writable"
|
|
46
|
+
@writable
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def coerce(chunk)
|
|
53
|
+
chunk.is_a?(Array) ? chunk.pack("C*") : chunk.to_s
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def build_compressor(format)
|
|
57
|
+
case format
|
|
58
|
+
when "gzip"
|
|
59
|
+
proc do |data|
|
|
60
|
+
io = StringIO.new
|
|
61
|
+
gz = Zlib::GzipWriter.new(io)
|
|
62
|
+
gz.write(data)
|
|
63
|
+
gz.close
|
|
64
|
+
io.string.bytes
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
when "deflate"
|
|
68
|
+
proc { |data| Zlib::Deflate.deflate(data).bytes }
|
|
69
|
+
when "deflate-raw"
|
|
70
|
+
proc do |data|
|
|
71
|
+
z = Zlib::Deflate.new(Zlib::DEFAULT_COMPRESSION, -Zlib::MAX_WBITS)
|
|
72
|
+
out = z.deflate(data, Zlib::FINISH)
|
|
73
|
+
z.close
|
|
74
|
+
out.bytes
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
class DecompressionStream
|
|
81
|
+
SUPPORTED = %w[gzip deflate deflate-raw].freeze
|
|
82
|
+
|
|
83
|
+
attr_reader :readable, :writable
|
|
84
|
+
|
|
85
|
+
def initialize(window, format)
|
|
86
|
+
raise ArgumentError, "unsupported format #{format.inspect}" unless SUPPORTED.include?(format.to_s)
|
|
87
|
+
|
|
88
|
+
@buffer = +""
|
|
89
|
+
decompressor = build_decompressor(format.to_s)
|
|
90
|
+
|
|
91
|
+
@readable = ReadableStream.new(window)
|
|
92
|
+
controller = TransformStreamDefaultController.new(@readable)
|
|
93
|
+
|
|
94
|
+
@writable = WritableStream.new(
|
|
95
|
+
window,
|
|
96
|
+
{
|
|
97
|
+
"write" => proc { |chunk| @buffer << coerce(chunk) },
|
|
98
|
+
"close" => proc do
|
|
99
|
+
plain = decompressor.call(@buffer)
|
|
100
|
+
controller.enqueue(plain)
|
|
101
|
+
@readable.__close__
|
|
102
|
+
end,
|
|
103
|
+
"abort" => proc { |r| @readable.__error__(r) }
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def __js_get__(key)
|
|
109
|
+
case key
|
|
110
|
+
when "readable"
|
|
111
|
+
@readable
|
|
112
|
+
when "writable"
|
|
113
|
+
@writable
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
def coerce(chunk)
|
|
120
|
+
chunk.is_a?(Array) ? chunk.pack("C*") : chunk.to_s
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def build_decompressor(format)
|
|
124
|
+
case format
|
|
125
|
+
when "gzip"
|
|
126
|
+
proc do |data|
|
|
127
|
+
io = StringIO.new(data)
|
|
128
|
+
gz = Zlib::GzipReader.new(io)
|
|
129
|
+
out = gz.read
|
|
130
|
+
gz.close
|
|
131
|
+
out.bytes
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
when "deflate"
|
|
135
|
+
proc { |data| Zlib::Inflate.inflate(data).bytes }
|
|
136
|
+
when "deflate-raw"
|
|
137
|
+
proc do |data|
|
|
138
|
+
z = Zlib::Inflate.new(-Zlib::MAX_WBITS)
|
|
139
|
+
out = z.inflate(data)
|
|
140
|
+
z.finish
|
|
141
|
+
z.close
|
|
142
|
+
out.bytes
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
# `cookieStore` — the asynchronous Cookie Store API. Reads and
|
|
5
|
+
# writes go through the existing `Document#cookie_jar`, so values
|
|
6
|
+
# set via `document.cookie = ...` round-trip via `cookieStore.get`.
|
|
7
|
+
#
|
|
8
|
+
# Spec: https://wicg.github.io/cookie-store/
|
|
9
|
+
class CookieStore
|
|
10
|
+
include EventTarget
|
|
11
|
+
|
|
12
|
+
def initialize(window)
|
|
13
|
+
@window = window
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def get(name_or_options)
|
|
17
|
+
name = name_or_options.is_a?(Hash) ? (name_or_options["name"] || name_or_options[:name]) : name_or_options
|
|
18
|
+
raw = cookie_jar.cookies[name.to_s]
|
|
19
|
+
raw ? PromiseValue.resolve(@window, build_record(name.to_s, raw)) : PromiseValue.resolve(@window, nil)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def get_all(name = nil)
|
|
23
|
+
records = cookie_jar.cookies.map { |k, v| build_record(k, v) }
|
|
24
|
+
records = records.select { |r| r["name"] == name.to_s } if name
|
|
25
|
+
PromiseValue.resolve(@window, records)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
alias getAll get_all
|
|
29
|
+
|
|
30
|
+
def set(name_or_options, value = nil)
|
|
31
|
+
if name_or_options.is_a?(Hash)
|
|
32
|
+
opts = name_or_options.transform_keys(&:to_s)
|
|
33
|
+
name = opts["name"]
|
|
34
|
+
value = opts["value"]
|
|
35
|
+
else
|
|
36
|
+
name = name_or_options
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
cookie_jar.set_cookie("#{name}=#{value}")
|
|
40
|
+
dispatch_event(
|
|
41
|
+
CookieChangeEvent.new(
|
|
42
|
+
"change",
|
|
43
|
+
"changed" => [build_record(name.to_s, value.to_s)],
|
|
44
|
+
"deleted" => []
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
PromiseValue.resolve(@window, nil)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def delete(name_or_options)
|
|
51
|
+
name = name_or_options.is_a?(Hash) ? (name_or_options["name"] || name_or_options[:name]) : name_or_options
|
|
52
|
+
cookie_jar.cookies.delete(name.to_s)
|
|
53
|
+
dispatch_event(
|
|
54
|
+
CookieChangeEvent.new(
|
|
55
|
+
"change",
|
|
56
|
+
"changed" => [],
|
|
57
|
+
"deleted" => [build_record(name.to_s, "")]
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
PromiseValue.resolve(@window, nil)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def __js_call__(method, args)
|
|
64
|
+
case method
|
|
65
|
+
when "get"
|
|
66
|
+
get(args[0])
|
|
67
|
+
when "getAll"
|
|
68
|
+
get_all(args[0])
|
|
69
|
+
when "set"
|
|
70
|
+
set(args[0], args[1])
|
|
71
|
+
when "delete"
|
|
72
|
+
delete(args[0])
|
|
73
|
+
when "addEventListener"
|
|
74
|
+
add_event_listener(args[0], args[1], args[2])
|
|
75
|
+
when "removeEventListener"
|
|
76
|
+
remove_event_listener(args[0], args[1])
|
|
77
|
+
when "dispatchEvent"
|
|
78
|
+
dispatch_event(args[0])
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def __event_parent__
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def cookie_jar
|
|
89
|
+
# `document.cookie_jar` is private; we reach the same backing
|
|
90
|
+
# store via the public `document.cookie` round-trip. Easier: use
|
|
91
|
+
# instance_variable_get on the document.
|
|
92
|
+
@window.document.instance_variable_get(:@cookie_jar)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def build_record(name, value)
|
|
96
|
+
{
|
|
97
|
+
"name" => name.to_s,
|
|
98
|
+
"value" => value.to_s,
|
|
99
|
+
"domain" => nil,
|
|
100
|
+
"path" => "/",
|
|
101
|
+
"expires" => nil,
|
|
102
|
+
"secure" => false,
|
|
103
|
+
"sameSite" => "strict"
|
|
104
|
+
}
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
class CookieChangeEvent < Event
|
|
109
|
+
def initialize(type, init = nil)
|
|
110
|
+
super
|
|
111
|
+
@changed = Array(read_init(init, "changed") || [])
|
|
112
|
+
@deleted = Array(read_init(init, "deleted") || [])
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
attr_reader :changed, :deleted
|
|
116
|
+
|
|
117
|
+
def __js_get__(key)
|
|
118
|
+
case key
|
|
119
|
+
when "changed"
|
|
120
|
+
@changed
|
|
121
|
+
when "deleted"
|
|
122
|
+
@deleted
|
|
123
|
+
else
|
|
124
|
+
super
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|