dommy 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/dommy/animation.rb +9 -1
- data/lib/dommy/attr.rb +192 -39
- data/lib/dommy/backend/nokogiri_adapter.rb +76 -0
- data/lib/dommy/backend/nokolexbor_adapter.rb +37 -0
- data/lib/dommy/backend.rb +46 -0
- data/lib/dommy/blob.rb +28 -9
- data/lib/dommy/bridge/constructor_registry.rb +28 -0
- data/lib/dommy/bridge/methods.rb +57 -0
- data/lib/dommy/bridge.rb +97 -0
- data/lib/dommy/callable_invoker.rb +36 -0
- data/lib/dommy/cookie_store.rb +3 -1
- data/lib/dommy/crypto.rb +7 -1
- data/lib/dommy/css.rb +46 -0
- data/lib/dommy/custom_elements.rb +27 -3
- data/lib/dommy/data_transfer.rb +4 -0
- data/lib/dommy/document.rb +615 -48
- data/lib/dommy/dom_parser.rb +28 -15
- data/lib/dommy/element.rb +999 -471
- data/lib/dommy/event.rb +260 -96
- data/lib/dommy/event_source.rb +6 -2
- data/lib/dommy/fetch.rb +505 -43
- data/lib/dommy/file_reader.rb +11 -3
- data/lib/dommy/form_data.rb +2 -0
- data/lib/dommy/history.rb +43 -8
- data/lib/dommy/html_collection.rb +55 -2
- data/lib/dommy/html_elements.rb +102 -1519
- data/lib/dommy/internal/css_pseudo_handlers.rb +109 -0
- data/lib/dommy/internal/global_functions.rb +26 -0
- data/lib/dommy/internal/idna.rb +16 -7
- data/lib/dommy/internal/ipv4_parser.rb +22 -7
- data/lib/dommy/internal/mutation_coordinator.rb +11 -2
- data/lib/dommy/internal/namespaces.rb +70 -0
- data/lib/dommy/internal/node_equality.rb +86 -0
- data/lib/dommy/internal/node_wrapper_cache.rb +62 -27
- data/lib/dommy/internal/observable_callback.rb +1 -5
- data/lib/dommy/internal/parent_node.rb +126 -0
- data/lib/dommy/internal/reflected_attributes.rb +103 -13
- data/lib/dommy/internal/selector_parser.rb +664 -0
- data/lib/dommy/internal/url_parser.rb +677 -0
- data/lib/dommy/intersection_observer.rb +2 -0
- data/lib/dommy/location.rb +2 -0
- data/lib/dommy/media_query_list.rb +7 -1
- data/lib/dommy/message_channel.rb +32 -2
- data/lib/dommy/mutation_observer.rb +55 -12
- data/lib/dommy/navigator.rb +26 -12
- data/lib/dommy/node.rb +158 -28
- data/lib/dommy/notification.rb +3 -1
- data/lib/dommy/performance.rb +4 -0
- data/lib/dommy/performance_observer.rb +2 -0
- data/lib/dommy/promise.rb +14 -14
- data/lib/dommy/range.rb +74 -5
- data/lib/dommy/resize_observer.rb +2 -0
- data/lib/dommy/scheduler.rb +34 -13
- data/lib/dommy/shadow_root.rb +23 -54
- data/lib/dommy/storage.rb +2 -0
- data/lib/dommy/streams.rb +18 -27
- data/lib/dommy/svg_elements.rb +204 -3606
- data/lib/dommy/text_codec.rb +174 -21
- data/lib/dommy/tree_walker.rb +255 -66
- data/lib/dommy/url.rb +287 -449
- data/lib/dommy/url_pattern.rb +2 -0
- data/lib/dommy/version.rb +1 -1
- data/lib/dommy/web_socket.rb +37 -7
- data/lib/dommy/window.rb +202 -213
- data/lib/dommy/worker.rb +7 -7
- data/lib/dommy/xml_http_request.rb +15 -5
- data/lib/dommy.rb +7 -0
- metadata +12 -3
data/lib/dommy/url_pattern.rb
CHANGED
data/lib/dommy/version.rb
CHANGED
data/lib/dommy/web_socket.rb
CHANGED
|
@@ -27,7 +27,7 @@ module Dommy
|
|
|
27
27
|
INLINE_HANDLERS = %w[open message close error].freeze
|
|
28
28
|
|
|
29
29
|
attr_reader :url, :protocol, :ready_state, :buffered_amount, :extensions
|
|
30
|
-
|
|
30
|
+
attr_reader :binary_type
|
|
31
31
|
|
|
32
32
|
def initialize(window, url, protocols = nil)
|
|
33
33
|
@window = window
|
|
@@ -36,7 +36,10 @@ module Dommy
|
|
|
36
36
|
@buffered_amount = 0
|
|
37
37
|
@extensions = ""
|
|
38
38
|
@binary_type = "blob"
|
|
39
|
-
|
|
39
|
+
# The subprotocol stays "" until the server selects one at the handshake;
|
|
40
|
+
# remember what was requested and adopt the first on open.
|
|
41
|
+
@requested_protocols = Array(protocols).flatten.map(&:to_s)
|
|
42
|
+
@protocol = ""
|
|
40
43
|
@sent_messages = []
|
|
41
44
|
@inline_handlers = {}
|
|
42
45
|
|
|
@@ -45,18 +48,41 @@ module Dommy
|
|
|
45
48
|
@window.scheduler.queue_microtask(proc { __test_simulate_open__ }) unless auto_open == false
|
|
46
49
|
end
|
|
47
50
|
|
|
51
|
+
# binaryType is an enumerated attribute: only "blob"/"arraybuffer" are
|
|
52
|
+
# accepted, any other assignment is ignored (per WebIDL enum reflection).
|
|
53
|
+
def binary_type=(value)
|
|
54
|
+
v = value.to_s
|
|
55
|
+
@binary_type = v if %w[blob arraybuffer].include?(v)
|
|
56
|
+
end
|
|
57
|
+
|
|
48
58
|
def send(data)
|
|
49
|
-
|
|
59
|
+
# send() before the connection opens is an InvalidStateError (a
|
|
60
|
+
# DOMException), not a bare Ruby error.
|
|
61
|
+
raise DOMException::InvalidStateError, "WebSocket is not open" if @ready_state == CONNECTING
|
|
62
|
+
return if @ready_state != OPEN # CLOSING/CLOSED silently discard (buffered)
|
|
50
63
|
|
|
51
64
|
@sent_messages << data
|
|
52
65
|
nil
|
|
53
66
|
end
|
|
54
67
|
|
|
55
|
-
|
|
68
|
+
# close([code[, reason]]): code must be 1000 or in 3000–4999, and the UTF-8
|
|
69
|
+
# reason must be ≤ 123 bytes, else throw — matching the WebSocket spec.
|
|
70
|
+
def close(code = nil, reason = nil)
|
|
71
|
+
unless code.nil?
|
|
72
|
+
c = code.to_i
|
|
73
|
+
unless c == 1000 || c.between?(3000, 4999)
|
|
74
|
+
raise DOMException::InvalidAccessError, "The close code must be 1000 or in 3000-4999, got #{c}."
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
if reason && reason.to_s.bytesize > 123
|
|
78
|
+
raise DOMException::SyntaxError, "The close reason must not exceed 123 UTF-8 bytes."
|
|
79
|
+
end
|
|
56
80
|
return if @ready_state == CLOSED || @ready_state == CLOSING
|
|
57
81
|
|
|
58
82
|
@ready_state = CLOSING
|
|
59
|
-
|
|
83
|
+
final_code = code.nil? ? 1005 : code.to_i
|
|
84
|
+
final_reason = reason.to_s
|
|
85
|
+
@window.scheduler.queue_microtask(proc { __test_simulate_close__(final_code, final_reason) })
|
|
60
86
|
nil
|
|
61
87
|
end
|
|
62
88
|
|
|
@@ -70,6 +96,8 @@ module Dommy
|
|
|
70
96
|
return if @ready_state != CONNECTING
|
|
71
97
|
|
|
72
98
|
@ready_state = OPEN
|
|
99
|
+
# The handshake "selects" the first requested subprotocol.
|
|
100
|
+
@protocol = @requested_protocols.first || ""
|
|
73
101
|
dispatch_event(Event.new("open"))
|
|
74
102
|
end
|
|
75
103
|
|
|
@@ -127,7 +155,7 @@ module Dommy
|
|
|
127
155
|
def __js_set__(key, value)
|
|
128
156
|
case key
|
|
129
157
|
when "binaryType"
|
|
130
|
-
|
|
158
|
+
self.binary_type = value
|
|
131
159
|
else
|
|
132
160
|
event = inline_event_for(key)
|
|
133
161
|
set_inline_handler(event, value) if event
|
|
@@ -136,6 +164,8 @@ module Dommy
|
|
|
136
164
|
nil
|
|
137
165
|
end
|
|
138
166
|
|
|
167
|
+
include Bridge::Methods
|
|
168
|
+
js_methods %w[send close addEventListener removeEventListener dispatchEvent]
|
|
139
169
|
def __js_call__(method, args)
|
|
140
170
|
case method
|
|
141
171
|
when "send"
|
|
@@ -145,7 +175,7 @@ module Dommy
|
|
|
145
175
|
when "addEventListener"
|
|
146
176
|
add_event_listener(args[0], args[1], args[2])
|
|
147
177
|
when "removeEventListener"
|
|
148
|
-
remove_event_listener(args[0], args[1])
|
|
178
|
+
remove_event_listener(args[0], args[1], args[2])
|
|
149
179
|
when "dispatchEvent"
|
|
150
180
|
dispatch_event(args[0])
|
|
151
181
|
end
|
data/lib/dommy/window.rb
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "cgi"
|
|
4
|
-
require "erb"
|
|
5
|
-
|
|
6
3
|
# Dommy — a happy-dom-style DOM polyfill in pure Ruby. Backbone is
|
|
7
4
|
# Nokogiri::HTML5 plus a small scheduler/event-loop layer.
|
|
8
5
|
#
|
|
@@ -26,93 +23,26 @@ module Dommy
|
|
|
26
23
|
def initialize(host = nil, nokogiri_doc: nil)
|
|
27
24
|
@host = host
|
|
28
25
|
@scheduler = Scheduler.new
|
|
29
|
-
@event_ctor = Bridge::Constructor.new { |args| Event.new(args[0], args[1]) }
|
|
30
|
-
@custom_event_ctor = Bridge::Constructor.new { |args| CustomEvent.new(args[0], args[1]) }
|
|
31
|
-
@mouse_event_ctor = Bridge::Constructor.new { |args| MouseEvent.new(args[0], args[1]) }
|
|
32
|
-
@keyboard_event_ctor = Bridge::Constructor.new { |args| KeyboardEvent.new(args[0], args[1]) }
|
|
33
|
-
@event_target_ctor = Bridge::Constructor.new { |_args| StandaloneEventTarget.new }
|
|
34
|
-
@error_ctor = Bridge::Constructor.new { |args| ErrorValue.new(args[0]) }
|
|
35
|
-
@promise_ctor = Bridge::PromiseConstructor.new(self)
|
|
36
|
-
@mutation_observer_ctor = Bridge::Constructor.new { |args| MutationObserver.new(self, args[0]) }
|
|
37
|
-
@abort_controller_ctor = Bridge::Constructor.new { |_args| AbortController.new }
|
|
38
|
-
@blob_ctor = Bridge::Constructor.new { |args| Blob.new(args[0] || [], args[1] || {}) }
|
|
39
|
-
@file_ctor = Bridge::Constructor.new { |args| File.new(args[0] || [], args[1].to_s, args[2] || {}) }
|
|
40
|
-
@file_list_ctor = Bridge::Constructor.new { |args| FileList.new(args[0] || []) }
|
|
41
|
-
@data_transfer_ctor = Bridge::Constructor.new { |args|
|
|
42
|
-
opts = args[0] || {}
|
|
43
|
-
DataTransfer.new(
|
|
44
|
-
files: opts["files"] || opts[:files] || [],
|
|
45
|
-
data: opts["data"] || opts[:data] || {}
|
|
46
|
-
)
|
|
47
|
-
}
|
|
48
|
-
@drag_event_ctor = Bridge::Constructor.new { |args| DragEvent.new(args[0], args[1]) }
|
|
49
|
-
@input_event_ctor = Bridge::Constructor.new { |args| InputEvent.new(args[0], args[1]) }
|
|
50
|
-
@pointer_event_ctor = Bridge::Constructor.new { |args| PointerEvent.new(args[0], args[1]) }
|
|
51
|
-
@progress_event_ctor = Bridge::Constructor.new { |args| ProgressEvent.new(args[0], args[1]) }
|
|
52
|
-
@touch_ctor = Bridge::Constructor.new { |args| Touch.new(args[0] || {}) }
|
|
53
|
-
@touch_event_ctor = Bridge::Constructor.new { |args| TouchEvent.new(args[0], args[1]) }
|
|
54
|
-
@clipboard_event_ctor = Bridge::Constructor.new { |args| ClipboardEvent.new(args[0], args[1]) }
|
|
55
|
-
@composition_event_ctor = Bridge::Constructor.new { |args| CompositionEvent.new(args[0], args[1]) }
|
|
56
|
-
@wheel_event_ctor = Bridge::Constructor.new { |args| WheelEvent.new(args[0], args[1]) }
|
|
57
|
-
@focus_event_ctor = Bridge::Constructor.new { |args| FocusEvent.new(args[0], args[1]) }
|
|
58
|
-
@before_unload_event_ctor = Bridge::Constructor.new { |args|
|
|
59
|
-
BeforeUnloadEvent.new(args[0] || "beforeunload", args[1])
|
|
60
|
-
}
|
|
61
|
-
win_ref = self
|
|
62
|
-
@animation_ctor = Bridge::Constructor.new { |args| Animation.new(args[0], args[1], window: win_ref) }
|
|
63
|
-
@keyframe_effect_ctor = Bridge::Constructor.new { |args| KeyframeEffect.new(args[0], args[1] || [], args[2]) }
|
|
64
26
|
@crypto = Crypto.new(self)
|
|
65
|
-
@
|
|
66
|
-
@
|
|
67
|
-
@intersection_observer_ctor = Bridge::Constructor.new { |args| IntersectionObserver.new(args[0], args[1]) }
|
|
68
|
-
@resize_observer_ctor = Bridge::Constructor.new { |args| ResizeObserver.new(args[0]) }
|
|
69
|
-
@performance_observer_ctor = Bridge::Constructor.new { |args| PerformanceObserver.new(args[0]) }
|
|
70
|
-
@request_ctor = Bridge::Constructor.new { |args| Request.new(args[0], args[1]) }
|
|
71
|
-
xhr_win_ref = self
|
|
72
|
-
@xhr_ctor = Bridge::Constructor.new { |_args| XMLHttpRequest.new(xhr_win_ref) }
|
|
73
|
-
@file_reader_ctor = Bridge::Constructor.new { |_args| FileReader.new(xhr_win_ref) }
|
|
74
|
-
@message_channel_ctor = Bridge::Constructor.new { |_args| MessageChannel.new(xhr_win_ref) }
|
|
75
|
-
@broadcast_channel_ctor = Bridge::Constructor.new { |args| BroadcastChannel.new(xhr_win_ref, args[0]) }
|
|
76
|
-
@web_socket_ctor = Bridge::Constructor.new { |args| WebSocket.new(xhr_win_ref, args[0], args[1]) }
|
|
77
|
-
@event_source_ctor = Bridge::Constructor.new { |args| EventSource.new(xhr_win_ref, args[0], args[1]) }
|
|
78
|
-
@notification_ctor = Bridge::Constructor.new { |args| Notification.new(xhr_win_ref, args[0], args[1]) }
|
|
79
|
-
@notification_ctor.define_class_method("requestPermission") do |args|
|
|
80
|
-
Notification.request_permission(xhr_win_ref, args[0])
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
@worker_ctor = Bridge::Constructor.new { |args| Worker.new(xhr_win_ref, args[0], args[1]) }
|
|
84
|
-
@readable_stream_ctor = Bridge::Constructor.new { |args| ReadableStream.new(xhr_win_ref, args[0]) }
|
|
85
|
-
@writable_stream_ctor = Bridge::Constructor.new { |args| WritableStream.new(xhr_win_ref, args[0]) }
|
|
86
|
-
@transform_stream_ctor = Bridge::Constructor.new { |args| TransformStream.new(xhr_win_ref, args[0]) }
|
|
87
|
-
@text_encoder_stream_ctor = Bridge::Constructor.new { |_args| TextEncoderStream.new(xhr_win_ref) }
|
|
88
|
-
@text_decoder_stream_ctor = Bridge::Constructor.new { |args|
|
|
89
|
-
TextDecoderStream.new(xhr_win_ref, args[0] || "utf-8", args[1])
|
|
90
|
-
}
|
|
91
|
-
@compression_stream_ctor = Bridge::Constructor.new { |args| CompressionStream.new(xhr_win_ref, args[0]) }
|
|
92
|
-
@decompression_stream_ctor = Bridge::Constructor.new { |args| DecompressionStream.new(xhr_win_ref, args[0]) }
|
|
93
|
-
@url_pattern_ctor = Bridge::Constructor.new { |args| URLPattern.new(args[0], args[1]) }
|
|
94
|
-
@cookie_store = CookieStore.new(xhr_win_ref)
|
|
95
|
-
|
|
96
|
-
@range_ctor = Bridge::Constructor.new { |_args| Range.new(@document) }
|
|
27
|
+
@css_namespace = CSSNamespace.new
|
|
28
|
+
@cookie_store = CookieStore.new(self)
|
|
97
29
|
@local_storage = Storage.new
|
|
98
30
|
@session_storage = Storage.new
|
|
99
31
|
@location = Location.new(self)
|
|
100
32
|
@history = History.new(self, @location)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
@url_ctor.define_class_method("canParse") { |args| URL.can_parse(args[0], args[1]) }
|
|
106
|
-
# `JS.global[:__some_key__] = ...` from user code lands here.
|
|
107
|
-
# Test code uses this for stub installation (e.g. a custom
|
|
108
|
-
# `__fetch_stub__`); production code stays on the typed
|
|
109
|
-
# accessors above. We keep it last in the read fallback to
|
|
110
|
-
# avoid shadowing intentional getters.
|
|
33
|
+
# `JS.global[:__some_key__] = ...` from user code lands here. Test code
|
|
34
|
+
# uses this for stub installation (e.g. a custom `__fetch_stub__`);
|
|
35
|
+
# production code stays on the typed accessors. Kept last in the read
|
|
36
|
+
# fallback so it can't shadow intentional getters.
|
|
111
37
|
@globals = {}
|
|
112
38
|
@document = Document.new(host, nokogiri_doc: nokogiri_doc)
|
|
113
39
|
@document.default_view = self
|
|
114
40
|
@custom_elements = CustomElementRegistry.new(self)
|
|
115
41
|
@navigator = Navigator.new(self)
|
|
42
|
+
# All JS global constructors (`new Event()`, `new URL()`, ...) live in a
|
|
43
|
+
# single name→Constructor registry rather than one ivar + one __js_get__
|
|
44
|
+
# arm each.
|
|
45
|
+
@constructors = Bridge::ConstructorRegistry.new(build_constructors)
|
|
116
46
|
end
|
|
117
47
|
|
|
118
48
|
# Bridge protocol: respond to a JS-style property read by name.
|
|
@@ -124,115 +54,23 @@ module Dommy
|
|
|
124
54
|
# nil (= JS undefined). Spec failures here are the signal to widen
|
|
125
55
|
# the surface in a future session.
|
|
126
56
|
def __js_get__(key)
|
|
57
|
+
ctor = @constructors[key]
|
|
58
|
+
return ctor if ctor
|
|
59
|
+
|
|
127
60
|
case key
|
|
128
61
|
when "document"
|
|
129
62
|
@document
|
|
130
|
-
when "
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
@mouse_event_ctor
|
|
136
|
-
when "KeyboardEvent"
|
|
137
|
-
@keyboard_event_ctor
|
|
138
|
-
when "EventTarget"
|
|
139
|
-
@event_target_ctor
|
|
140
|
-
when "Error"
|
|
141
|
-
@error_ctor
|
|
142
|
-
when "Promise"
|
|
143
|
-
@promise_ctor
|
|
144
|
-
when "MutationObserver"
|
|
145
|
-
@mutation_observer_ctor
|
|
146
|
-
when "AbortController"
|
|
147
|
-
@abort_controller_ctor
|
|
148
|
-
when "Blob"
|
|
149
|
-
@blob_ctor
|
|
150
|
-
when "File"
|
|
151
|
-
@file_ctor
|
|
152
|
-
when "FileList"
|
|
153
|
-
@file_list_ctor
|
|
154
|
-
when "DataTransfer"
|
|
155
|
-
@data_transfer_ctor
|
|
156
|
-
when "DragEvent"
|
|
157
|
-
@drag_event_ctor
|
|
158
|
-
when "InputEvent"
|
|
159
|
-
@input_event_ctor
|
|
160
|
-
when "PointerEvent"
|
|
161
|
-
@pointer_event_ctor
|
|
162
|
-
when "ProgressEvent"
|
|
163
|
-
@progress_event_ctor
|
|
164
|
-
when "Touch"
|
|
165
|
-
@touch_ctor
|
|
166
|
-
when "TouchEvent"
|
|
167
|
-
@touch_event_ctor
|
|
168
|
-
when "ClipboardEvent"
|
|
169
|
-
@clipboard_event_ctor
|
|
170
|
-
when "CompositionEvent"
|
|
171
|
-
@composition_event_ctor
|
|
172
|
-
when "WheelEvent"
|
|
173
|
-
@wheel_event_ctor
|
|
174
|
-
when "FocusEvent"
|
|
175
|
-
@focus_event_ctor
|
|
176
|
-
when "BeforeUnloadEvent"
|
|
177
|
-
@before_unload_event_ctor
|
|
178
|
-
when "Animation"
|
|
179
|
-
@animation_ctor
|
|
180
|
-
when "KeyframeEffect"
|
|
181
|
-
@keyframe_effect_ctor
|
|
63
|
+
when "window", "self", "parent", "top", "frames"
|
|
64
|
+
# A top-level browsing context refers to itself for these. Returning the
|
|
65
|
+
# window (not nil) lets `window === window.parent` and frame-walking
|
|
66
|
+
# loops (e.g. testharness.js's `while (w != w.parent)`) terminate.
|
|
67
|
+
self
|
|
182
68
|
when "crypto"
|
|
183
69
|
@crypto
|
|
184
|
-
when "TextEncoder"
|
|
185
|
-
@text_encoder_ctor
|
|
186
|
-
when "TextDecoder"
|
|
187
|
-
@text_decoder_ctor
|
|
188
|
-
when "IntersectionObserver"
|
|
189
|
-
@intersection_observer_ctor
|
|
190
|
-
when "ResizeObserver"
|
|
191
|
-
@resize_observer_ctor
|
|
192
|
-
when "PerformanceObserver"
|
|
193
|
-
@performance_observer_ctor
|
|
194
|
-
when "Request"
|
|
195
|
-
@request_ctor
|
|
196
|
-
when "XMLHttpRequest"
|
|
197
|
-
@xhr_ctor
|
|
198
|
-
when "FileReader"
|
|
199
|
-
@file_reader_ctor
|
|
200
|
-
when "MessageChannel"
|
|
201
|
-
@message_channel_ctor
|
|
202
|
-
when "BroadcastChannel"
|
|
203
|
-
@broadcast_channel_ctor
|
|
204
|
-
when "WebSocket"
|
|
205
|
-
@web_socket_ctor
|
|
206
|
-
when "EventSource"
|
|
207
|
-
@event_source_ctor
|
|
208
|
-
when "Notification"
|
|
209
|
-
@notification_ctor
|
|
210
|
-
when "Worker"
|
|
211
|
-
@worker_ctor
|
|
212
|
-
when "ReadableStream"
|
|
213
|
-
@readable_stream_ctor
|
|
214
|
-
when "WritableStream"
|
|
215
|
-
@writable_stream_ctor
|
|
216
|
-
when "TransformStream"
|
|
217
|
-
@transform_stream_ctor
|
|
218
|
-
when "TextEncoderStream"
|
|
219
|
-
@text_encoder_stream_ctor
|
|
220
|
-
when "TextDecoderStream"
|
|
221
|
-
@text_decoder_stream_ctor
|
|
222
|
-
when "CompressionStream"
|
|
223
|
-
@compression_stream_ctor
|
|
224
|
-
when "DecompressionStream"
|
|
225
|
-
@decompression_stream_ctor
|
|
226
|
-
when "URLPattern"
|
|
227
|
-
@url_pattern_ctor
|
|
228
70
|
when "cookieStore"
|
|
229
71
|
@cookie_store
|
|
230
|
-
when "Range"
|
|
231
|
-
@range_ctor
|
|
232
|
-
# handled by Symbol sentinel
|
|
233
72
|
when "console"
|
|
234
73
|
:console
|
|
235
|
-
# likewise
|
|
236
74
|
when "Object"
|
|
237
75
|
:object_ctor
|
|
238
76
|
when "Array"
|
|
@@ -249,14 +87,21 @@ module Dommy
|
|
|
249
87
|
@location
|
|
250
88
|
when "history"
|
|
251
89
|
@history
|
|
252
|
-
when "
|
|
253
|
-
@
|
|
90
|
+
when "CSS"
|
|
91
|
+
@css_namespace
|
|
254
92
|
when "fetch"
|
|
255
93
|
FetchFn.new(self)
|
|
256
94
|
when "customElements"
|
|
257
95
|
@custom_elements
|
|
258
96
|
when "navigator"
|
|
259
97
|
@navigator
|
|
98
|
+
when "scrollX", "pageXOffset"
|
|
99
|
+
@scroll_x || 0
|
|
100
|
+
when "scrollY", "pageYOffset"
|
|
101
|
+
@scroll_y || 0
|
|
102
|
+
when "scrollMaxX", "scrollMaxY"
|
|
103
|
+
# No real content box to scroll past, so the max offset is 0.
|
|
104
|
+
0
|
|
260
105
|
else
|
|
261
106
|
@globals[key]
|
|
262
107
|
end
|
|
@@ -275,40 +120,33 @@ module Dommy
|
|
|
275
120
|
nil
|
|
276
121
|
end
|
|
277
122
|
|
|
278
|
-
|
|
279
|
-
|
|
123
|
+
include Bridge::Methods
|
|
124
|
+
js_methods %w[
|
|
280
125
|
fetch encodeURIComponent decodeURIComponent addEventListener removeEventListener
|
|
281
126
|
dispatchEvent setTimeout clearTimeout setInterval clearInterval requestAnimationFrame
|
|
282
|
-
cancelAnimationFrame queueMicrotask requestIdleCallback cancelIdleCallback
|
|
283
|
-
|
|
284
|
-
]
|
|
285
|
-
def __js_method_names__
|
|
286
|
-
JS_METHOD_NAMES
|
|
287
|
-
end
|
|
288
|
-
|
|
127
|
+
cancelAnimationFrame queueMicrotask requestIdleCallback cancelIdleCallback structuredClone
|
|
128
|
+
matchMedia getComputedStyle scroll scrollTo scrollBy
|
|
129
|
+
]
|
|
289
130
|
def __js_call__(method, args)
|
|
290
131
|
case method
|
|
291
132
|
when "fetch"
|
|
292
133
|
FetchFn.new(self).__js_call__("call", args)
|
|
293
134
|
when "encodeURIComponent"
|
|
294
|
-
|
|
295
|
-
# `A-Za-z0-9 - _ . ! ~ * ' ( )`. Ruby's `CGI.escape` uses
|
|
296
|
-
# `+` for space; ERB::Util.url_encode matches JS behavior.
|
|
297
|
-
ERB::Util.url_encode(args[0].to_s)
|
|
135
|
+
Internal::GlobalFunctions.encode_uri_component(args[0])
|
|
298
136
|
when "decodeURIComponent"
|
|
299
|
-
|
|
137
|
+
Internal::GlobalFunctions.decode_uri_component(args[0])
|
|
300
138
|
when "addEventListener"
|
|
301
139
|
add_event_listener(args[0], args[1], args[2])
|
|
302
140
|
when "removeEventListener"
|
|
303
|
-
remove_event_listener(args[0], args[1])
|
|
141
|
+
remove_event_listener(args[0], args[1], args[2])
|
|
304
142
|
when "dispatchEvent"
|
|
305
143
|
dispatch_event(args[0])
|
|
306
144
|
when "setTimeout"
|
|
307
|
-
@scheduler.set_timeout(args[0], args[1]
|
|
145
|
+
@scheduler.set_timeout(args[0], timer_delay(args[1]))
|
|
308
146
|
when "clearTimeout"
|
|
309
147
|
@scheduler.clear_timeout(args[0])
|
|
310
148
|
when "setInterval"
|
|
311
|
-
@scheduler.set_interval(args[0], args[1]
|
|
149
|
+
@scheduler.set_interval(args[0], timer_delay(args[1]))
|
|
312
150
|
when "clearInterval"
|
|
313
151
|
@scheduler.clear_interval(args[0])
|
|
314
152
|
when "requestAnimationFrame"
|
|
@@ -318,20 +156,9 @@ module Dommy
|
|
|
318
156
|
when "queueMicrotask"
|
|
319
157
|
@scheduler.queue_microtask(args[0])
|
|
320
158
|
when "requestIdleCallback"
|
|
321
|
-
|
|
322
|
-
# dommy, so we model it as a deferred setTimeout. The
|
|
323
|
-
# callback receives an `IdleDeadline`-shaped Hash.
|
|
324
|
-
@scheduler.set_timeout(
|
|
325
|
-
proc {
|
|
326
|
-
args[0].respond_to?(:__js_call__) ? args[0].__js_call__(
|
|
327
|
-
"call",
|
|
328
|
-
[{"timeRemaining" => 50.0, "didTimeout" => false}]
|
|
329
|
-
) : args[0].call({"timeRemaining" => 50.0, "didTimeout" => false})
|
|
330
|
-
},
|
|
331
|
-
(args[1].is_a?(Hash) && args[1]["timeout"]) || 0
|
|
332
|
-
)
|
|
159
|
+
@scheduler.request_idle_callback(args[0], (args[1].is_a?(Hash) && args[1]["timeout"]) || 0)
|
|
333
160
|
when "cancelIdleCallback"
|
|
334
|
-
@scheduler.
|
|
161
|
+
@scheduler.cancel_idle_callback(args[0])
|
|
335
162
|
when "structuredClone"
|
|
336
163
|
Dommy.structured_clone(args[0])
|
|
337
164
|
when "matchMedia"
|
|
@@ -342,6 +169,10 @@ module Dommy
|
|
|
342
169
|
# values the test set inline via `el.style.color = "..."`.
|
|
343
170
|
target = args[0]
|
|
344
171
|
target.respond_to?(:style) ? target.style : nil
|
|
172
|
+
when "scroll", "scrollTo"
|
|
173
|
+
scroll_to(*args)
|
|
174
|
+
when "scrollBy"
|
|
175
|
+
scroll_by(*args)
|
|
345
176
|
else
|
|
346
177
|
# Additional window-level methods (fetch, location, history,
|
|
347
178
|
# Promise, MutationObserver, etc.) arrive in later sessions.
|
|
@@ -357,7 +188,9 @@ module Dommy
|
|
|
357
188
|
# hashchange events. Listeners registered on the Window via
|
|
358
189
|
# `addEventListener("popstate"|"hashchange", cb)` receive them.
|
|
359
190
|
def fire_popstate(state)
|
|
360
|
-
|
|
191
|
+
# PopStateEvent exposes the entry's state as `event.state` (the spec
|
|
192
|
+
# property). Routers (Turbo) branch on `event.state`.
|
|
193
|
+
event = PopStateEvent.new("popstate", "state" => state)
|
|
361
194
|
dispatch_event(event)
|
|
362
195
|
end
|
|
363
196
|
|
|
@@ -365,5 +198,161 @@ module Dommy
|
|
|
365
198
|
event = CustomEvent.new("hashchange", "detail" => {"oldURL" => old_hash, "newURL" => new_hash})
|
|
366
199
|
dispatch_event(event)
|
|
367
200
|
end
|
|
201
|
+
|
|
202
|
+
private
|
|
203
|
+
|
|
204
|
+
# Virtual scroll position. There's no real layout, but tracking a logical
|
|
205
|
+
# `(scrollX, scrollY)` makes scroll-dependent behaviour observable: scrollTo/
|
|
206
|
+
# scroll set it absolutely, scrollBy relatively, and a `scroll` event fires on
|
|
207
|
+
# change so observers (e.g. Turbo's ScrollObserver, which records the position
|
|
208
|
+
# into history restoration data and replays it on back/forward) work.
|
|
209
|
+
def scroll_to(*args)
|
|
210
|
+
x, y = parse_scroll_args(args, @scroll_x || 0, @scroll_y || 0, relative: false)
|
|
211
|
+
update_scroll(x, y)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def scroll_by(*args)
|
|
215
|
+
x, y = parse_scroll_args(args, @scroll_x || 0, @scroll_y || 0, relative: true)
|
|
216
|
+
update_scroll(x, y)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Accept either positional `(x, y)` or a `{ left:, top: }` options dict.
|
|
220
|
+
def parse_scroll_args(args, cur_x, cur_y, relative:)
|
|
221
|
+
if args[0].is_a?(Hash)
|
|
222
|
+
dx = scroll_coord(args[0]["left"] || args[0][:left])
|
|
223
|
+
dy = scroll_coord(args[0]["top"] || args[0][:top])
|
|
224
|
+
else
|
|
225
|
+
dx = scroll_coord(args[0])
|
|
226
|
+
dy = scroll_coord(args[1])
|
|
227
|
+
end
|
|
228
|
+
if relative
|
|
229
|
+
[cur_x + (dx || 0), cur_y + (dy || 0)]
|
|
230
|
+
else
|
|
231
|
+
[dx.nil? ? cur_x : dx, dy.nil? ? cur_y : dy]
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def scroll_coord(value)
|
|
236
|
+
return nil if value.nil? || (defined?(Bridge::UNDEFINED) && value.equal?(Bridge::UNDEFINED))
|
|
237
|
+
|
|
238
|
+
value.is_a?(Numeric) ? value.to_i : value.to_s.to_i
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def update_scroll(x, y)
|
|
242
|
+
return nil if x == (@scroll_x || 0) && y == (@scroll_y || 0)
|
|
243
|
+
|
|
244
|
+
@scroll_x = x
|
|
245
|
+
@scroll_y = y
|
|
246
|
+
dispatch_event(Event.new("scroll"))
|
|
247
|
+
nil
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# The timer delay (WebIDL `long`, default 0). A missing/undefined argument
|
|
251
|
+
# or any non-numeric value coerces to 0 rather than raising.
|
|
252
|
+
def timer_delay(value)
|
|
253
|
+
return value if value.is_a?(Numeric)
|
|
254
|
+
return value.to_i if value.is_a?(String) && value =~ /\A\s*-?\d+/
|
|
255
|
+
|
|
256
|
+
0
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Build the JS-global constructor map. Blocks are lazy (run at `new X()`
|
|
260
|
+
# time), so they may reference `win` / `@document` freely.
|
|
261
|
+
def build_constructors
|
|
262
|
+
win = self
|
|
263
|
+
|
|
264
|
+
notification = Bridge::Constructor.new { |args| Notification.new(win, args[0], args[1]) }
|
|
265
|
+
notification.define_class_method("requestPermission") { |args| Notification.request_permission(win, args[0]) }
|
|
266
|
+
|
|
267
|
+
url = Bridge::Constructor.new { |args| URL.new(args[0], args[1]) }
|
|
268
|
+
url.define_class_method("createObjectURL") { |args| URL.create_object_url(args[0]) }
|
|
269
|
+
url.define_class_method("revokeObjectURL") { |args| URL.revoke_object_url(args[0]) }
|
|
270
|
+
url.define_class_method("parse") { |args| URL.parse(args[0], args[1]) }
|
|
271
|
+
url.define_class_method("canParse") { |args| URL.can_parse(args[0], args[1]) }
|
|
272
|
+
|
|
273
|
+
# AbortSignal is not constructible (`new AbortSignal()` → TypeError); it is
|
|
274
|
+
# exposed only for its static factories abort()/any()/timeout().
|
|
275
|
+
abort_signal = Bridge::Constructor.new { |_args| raise Bridge::TypeError, "Illegal constructor" }
|
|
276
|
+
abort_signal.define_class_method("abort") { |args| args.empty? ? AbortSignal.abort : AbortSignal.abort(args[0]) }
|
|
277
|
+
abort_signal.define_class_method("any") { |args| AbortSignal.any(args[0]) }
|
|
278
|
+
abort_signal.define_class_method("timeout") { |args| AbortSignal.timeout(args[0], scheduler: win.scheduler) }
|
|
279
|
+
|
|
280
|
+
{
|
|
281
|
+
# `new Document()` — a fresh empty document (content type application/xml
|
|
282
|
+
# per the DOM Standard, so it behaves as a non-HTML document).
|
|
283
|
+
"Document" => Bridge::Constructor.new do
|
|
284
|
+
Document.new(nil, nokogiri_doc: Backend.document_class.new).tap { |d| d.content_type = "application/xml" }
|
|
285
|
+
end,
|
|
286
|
+
"Event" => Bridge::Constructor.new { |args| Event.new(args[0], args[1]) },
|
|
287
|
+
"CustomEvent" => Bridge::Constructor.new { |args| CustomEvent.new(args[0], args[1]) },
|
|
288
|
+
"MessageEvent" => Bridge::Constructor.new { |args| MessageEvent.new(args[0], args[1]) },
|
|
289
|
+
"PopStateEvent" => Bridge::Constructor.new { |args| PopStateEvent.new(args[0], args[1]) },
|
|
290
|
+
"CloseEvent" => Bridge::Constructor.new { |args| CloseEvent.new(args[0], args[1]) },
|
|
291
|
+
"MouseEvent" => Bridge::Constructor.new { |args| MouseEvent.new(args[0], args[1]) },
|
|
292
|
+
"KeyboardEvent" => Bridge::Constructor.new { |args| KeyboardEvent.new(args[0], args[1]) },
|
|
293
|
+
"EventTarget" => Bridge::Constructor.new { |_args| StandaloneEventTarget.new },
|
|
294
|
+
"Error" => Bridge::Constructor.new { |args| ErrorValue.new(args[0]) },
|
|
295
|
+
"Promise" => Bridge::PromiseConstructor.new(win),
|
|
296
|
+
"MutationObserver" => Bridge::Constructor.new { |args| MutationObserver.new(win, args[0]) },
|
|
297
|
+
"AbortController" => Bridge::Constructor.new { |_args| AbortController.new },
|
|
298
|
+
"AbortSignal" => abort_signal,
|
|
299
|
+
"Blob" => Bridge::Constructor.new { |args| Blob.new(args[0] || [], args[1] || {}, win) },
|
|
300
|
+
"File" => Bridge::Constructor.new { |args| File.new(args[0] || [], args[1].to_s, args[2] || {}, win) },
|
|
301
|
+
"FileList" => Bridge::Constructor.new { |args| FileList.new(args[0] || []) },
|
|
302
|
+
"FormData" => Bridge::Constructor.new { |args| FormData.new(args[0]) },
|
|
303
|
+
"DOMParser" => Bridge::Constructor.new { |_args| DOMParser.new },
|
|
304
|
+
"XMLSerializer" => Bridge::Constructor.new { |_args| XMLSerializer.new },
|
|
305
|
+
"URLSearchParams" => Bridge::Constructor.new { |args| URLSearchParams.new(args[0] || "") },
|
|
306
|
+
"Headers" => Bridge::Constructor.new { |args| Headers.new(args[0] || {}) },
|
|
307
|
+
"Response" => Bridge::Constructor.new { |args| Response.__construct__(win, args[0], args[1]) }
|
|
308
|
+
.define_class_method("json") { |args| Response.__json__(win, args.length >= 1 ? args[0] : Bridge::UNDEFINED, args[1]) }
|
|
309
|
+
.define_class_method("redirect") { |args| Response.__redirect__(win, args[0], args[1]) }
|
|
310
|
+
.define_class_method("error") { |_args| Response.__error__(win) },
|
|
311
|
+
"DataTransfer" => Bridge::Constructor.new { |args|
|
|
312
|
+
opts = args[0] || {}
|
|
313
|
+
DataTransfer.new(
|
|
314
|
+
files: opts["files"] || opts[:files] || [],
|
|
315
|
+
data: opts["data"] || opts[:data] || {}
|
|
316
|
+
)
|
|
317
|
+
},
|
|
318
|
+
"DragEvent" => Bridge::Constructor.new { |args| DragEvent.new(args[0], args[1]) },
|
|
319
|
+
"InputEvent" => Bridge::Constructor.new { |args| InputEvent.new(args[0], args[1]) },
|
|
320
|
+
"PointerEvent" => Bridge::Constructor.new { |args| PointerEvent.new(args[0], args[1]) },
|
|
321
|
+
"ProgressEvent" => Bridge::Constructor.new { |args| ProgressEvent.new(args[0], args[1]) },
|
|
322
|
+
"Touch" => Bridge::Constructor.new { |args| Touch.new(args[0] || {}) },
|
|
323
|
+
"TouchEvent" => Bridge::Constructor.new { |args| TouchEvent.new(args[0], args[1]) },
|
|
324
|
+
"ClipboardEvent" => Bridge::Constructor.new { |args| ClipboardEvent.new(args[0], args[1]) },
|
|
325
|
+
"CompositionEvent" => Bridge::Constructor.new { |args| CompositionEvent.new(args[0], args[1]) },
|
|
326
|
+
"WheelEvent" => Bridge::Constructor.new { |args| WheelEvent.new(args[0], args[1]) },
|
|
327
|
+
"FocusEvent" => Bridge::Constructor.new { |args| FocusEvent.new(args[0], args[1]) },
|
|
328
|
+
"BeforeUnloadEvent" => Bridge::Constructor.new { |args| BeforeUnloadEvent.new(args[0] || "beforeunload", args[1]) },
|
|
329
|
+
"Animation" => Bridge::Constructor.new { |args| Animation.new(args[0], args[1], window: win) },
|
|
330
|
+
"KeyframeEffect" => Bridge::Constructor.new { |args| KeyframeEffect.new(args[0], args[1] || [], args[2]) },
|
|
331
|
+
"TextEncoder" => Bridge::Constructor.new { |_args| TextEncoder.new },
|
|
332
|
+
"TextDecoder" => Bridge::Constructor.new { |args| TextDecoder.new(args[0] || "utf-8", args[1]) },
|
|
333
|
+
"IntersectionObserver" => Bridge::Constructor.new { |args| IntersectionObserver.new(args[0], args[1]) },
|
|
334
|
+
"ResizeObserver" => Bridge::Constructor.new { |args| ResizeObserver.new(args[0]) },
|
|
335
|
+
"PerformanceObserver" => Bridge::Constructor.new { |args| PerformanceObserver.new(args[0]) },
|
|
336
|
+
"Request" => Bridge::Constructor.new { |args| Request.new(args[0], args[1]) },
|
|
337
|
+
"XMLHttpRequest" => Bridge::Constructor.new { |_args| XMLHttpRequest.new(win) },
|
|
338
|
+
"FileReader" => Bridge::Constructor.new { |_args| FileReader.new(win) },
|
|
339
|
+
"MessageChannel" => Bridge::Constructor.new { |_args| MessageChannel.new(win) },
|
|
340
|
+
"BroadcastChannel" => Bridge::Constructor.new { |args| BroadcastChannel.new(win, args[0]) },
|
|
341
|
+
"WebSocket" => Bridge::Constructor.new { |args| WebSocket.new(win, args[0], args[1]) },
|
|
342
|
+
"EventSource" => Bridge::Constructor.new { |args| EventSource.new(win, args[0], args[1]) },
|
|
343
|
+
"Notification" => notification,
|
|
344
|
+
"Worker" => Bridge::Constructor.new { |args| Worker.new(win, args[0], args[1]) },
|
|
345
|
+
"ReadableStream" => Bridge::Constructor.new { |args| ReadableStream.new(win, args[0]) },
|
|
346
|
+
"WritableStream" => Bridge::Constructor.new { |args| WritableStream.new(win, args[0]) },
|
|
347
|
+
"TransformStream" => Bridge::Constructor.new { |args| TransformStream.new(win, args[0]) },
|
|
348
|
+
"TextEncoderStream" => Bridge::Constructor.new { |_args| TextEncoderStream.new(win) },
|
|
349
|
+
"TextDecoderStream" => Bridge::Constructor.new { |args| TextDecoderStream.new(win, args[0] || "utf-8", args[1]) },
|
|
350
|
+
"CompressionStream" => Bridge::Constructor.new { |args| CompressionStream.new(win, args[0]) },
|
|
351
|
+
"DecompressionStream" => Bridge::Constructor.new { |args| DecompressionStream.new(win, args[0]) },
|
|
352
|
+
"URLPattern" => Bridge::Constructor.new { |args| URLPattern.new(args[0], args[1]) },
|
|
353
|
+
"Range" => Bridge::Constructor.new { |_args| Range.new(@document) },
|
|
354
|
+
"URL" => url,
|
|
355
|
+
}
|
|
356
|
+
end
|
|
368
357
|
end
|
|
369
358
|
end
|
data/lib/dommy/worker.rb
CHANGED
|
@@ -85,10 +85,14 @@ module Dommy
|
|
|
85
85
|
|
|
86
86
|
def __js_set__(key, value)
|
|
87
87
|
event = inline_event_for(key)
|
|
88
|
-
|
|
88
|
+
return Bridge::UNHANDLED unless event
|
|
89
|
+
|
|
90
|
+
set_inline_handler(event, value)
|
|
89
91
|
nil
|
|
90
92
|
end
|
|
91
93
|
|
|
94
|
+
include Bridge::Methods
|
|
95
|
+
js_methods %w[postMessage terminate addEventListener removeEventListener dispatchEvent]
|
|
92
96
|
def __js_call__(method, args)
|
|
93
97
|
case method
|
|
94
98
|
when "postMessage"
|
|
@@ -98,7 +102,7 @@ module Dommy
|
|
|
98
102
|
when "addEventListener"
|
|
99
103
|
add_event_listener(args[0], args[1], args[2])
|
|
100
104
|
when "removeEventListener"
|
|
101
|
-
remove_event_listener(args[0], args[1])
|
|
105
|
+
remove_event_listener(args[0], args[1], args[2])
|
|
102
106
|
when "dispatchEvent"
|
|
103
107
|
dispatch_event(args[0])
|
|
104
108
|
end
|
|
@@ -133,11 +137,7 @@ module Dommy
|
|
|
133
137
|
end
|
|
134
138
|
|
|
135
139
|
def invoke(callback, args)
|
|
136
|
-
|
|
137
|
-
callback.__js_call__("call", args)
|
|
138
|
-
elsif callback.respond_to?(:call)
|
|
139
|
-
callback.call(*args)
|
|
140
|
-
end
|
|
140
|
+
CallableInvoker.invoke(callback, *args)
|
|
141
141
|
end
|
|
142
142
|
end
|
|
143
143
|
end
|