dommy 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/dommy/backend/makiri_adapter.rb +63 -29
- data/lib/dommy/backend.rb +32 -9
- data/lib/dommy/bridge/constructor.rb +72 -0
- data/lib/dommy/bridge/errors.rb +42 -0
- data/lib/dommy/bridge/methods.rb +21 -14
- data/lib/dommy/bridge/sentinels.rb +39 -0
- data/lib/dommy/bridge/values.rb +78 -0
- data/lib/dommy/{js → bridge}/wire_tags.rb +14 -11
- data/lib/dommy/bridge.rb +22 -250
- data/lib/dommy/browser.rb +112 -11
- data/lib/dommy/character_data.rb +539 -0
- data/lib/dommy/class_list.rb +311 -0
- data/lib/dommy/crypto.rb +12 -1
- data/lib/dommy/css.rb +12 -0
- data/lib/dommy/data_transfer.rb +106 -0
- data/lib/dommy/document.rb +307 -369
- data/lib/dommy/dom_parser.rb +3 -0
- data/lib/dommy/element.rb +199 -1817
- data/lib/dommy/encodings/tables.rb +4 -4
- data/lib/dommy/encodings.rb +58 -27
- data/lib/dommy/event.rb +227 -1
- data/lib/dommy/event_source.rb +41 -0
- data/lib/dommy/form_data.rb +6 -64
- data/lib/dommy/form_entry_list.rb +227 -0
- data/lib/dommy/fragment.rb +320 -0
- data/lib/dommy/history.rb +3 -0
- data/lib/dommy/html_collection.rb +18 -8
- data/lib/dommy/html_elements/base.rb +338 -0
- data/lib/dommy/html_elements/embedded.rb +237 -0
- data/lib/dommy/html_elements/forms/form.rb +1088 -0
- data/lib/dommy/html_elements/forms/input.rb +642 -0
- data/lib/dommy/html_elements/forms/select.rb +536 -0
- data/lib/dommy/html_elements/forms.rb +8 -0
- data/lib/dommy/html_elements/interactive.rb +382 -0
- data/lib/dommy/html_elements/media.rb +329 -0
- data/lib/dommy/html_elements/metadata.rb +353 -0
- data/lib/dommy/html_elements/miscellaneous.rb +77 -0
- data/lib/dommy/html_elements/tables.rb +468 -0
- data/lib/dommy/html_elements.rb +11 -5322
- data/lib/dommy/hyperlink_utils.rb +171 -0
- data/lib/dommy/interaction/debug.rb +7 -6
- data/lib/dommy/interaction/dom_summary.rb +65 -40
- data/lib/dommy/interaction/driver.rb +29 -115
- data/lib/dommy/interaction/event_synthesis.rb +97 -4
- data/lib/dommy/interaction/field_interactor.rb +1 -13
- data/lib/dommy/interaction/form_submission.rb +89 -167
- data/lib/dommy/interaction/key_sender.rb +121 -0
- data/lib/dommy/interaction/mime_types.rb +25 -0
- data/lib/dommy/interaction.rb +2 -0
- data/lib/dommy/internal/accessibility_tree.rb +32 -9
- data/lib/dommy/internal/accessibility_visibility.rb +44 -0
- data/lib/dommy/internal/accessible_name.rb +85 -65
- data/lib/dommy/internal/aria_role.rb +21 -6
- data/lib/dommy/internal/aria_snapshot.rb +5 -0
- data/lib/dommy/internal/aria_state.rb +24 -5
- data/lib/dommy/internal/autofill.rb +141 -0
- data/lib/dommy/internal/backend_prefilter.rb +170 -0
- data/lib/dommy/internal/bounded_cache.rb +50 -0
- data/lib/dommy/internal/child_node.rb +9 -34
- data/lib/dommy/internal/css/calc.rb +143 -145
- data/lib/dommy/internal/css/cascade.rb +38 -393
- data/lib/dommy/internal/css/cascaded_declarations.rb +245 -0
- data/lib/dommy/internal/css/color.rb +61 -34
- data/lib/dommy/internal/css/computed_style_builder.rb +205 -0
- data/lib/dommy/internal/css/computed_style_declaration.rb +20 -4
- data/lib/dommy/internal/css/counters.rb +60 -37
- data/lib/dommy/internal/css/custom_properties.rb +87 -39
- data/lib/dommy/internal/css/media_query.rb +228 -203
- data/lib/dommy/internal/css/parser.rb +29 -12
- data/lib/dommy/internal/css/property_registry.rb +32 -56
- data/lib/dommy/internal/css/renderability.rb +61 -0
- data/lib/dommy/internal/css/rule_index.rb +17 -140
- data/lib/dommy/internal/css/selector_dependencies.rb +162 -0
- data/lib/dommy/internal/css/style_cache.rb +61 -0
- data/lib/dommy/internal/css/ua_stylesheet.rb +40 -1
- data/lib/dommy/internal/css/value_tokens.rb +60 -0
- data/lib/dommy/internal/directionality.rb +188 -0
- data/lib/dommy/internal/document_generations.rb +169 -0
- data/lib/dommy/internal/document_interaction_state.rb +47 -0
- data/lib/dommy/internal/document_live_ranges.rb +97 -0
- data/lib/dommy/internal/dom_matching.rb +28 -12
- data/lib/dommy/internal/element_aria.rb +163 -0
- data/lib/dommy/internal/element_geometry.rb +69 -0
- data/lib/dommy/internal/element_matching.rb +5 -12
- data/lib/dommy/internal/element_shadow.rb +117 -0
- data/lib/dommy/internal/element_state.rb +232 -0
- data/lib/dommy/internal/element_tasks.rb +19 -0
- data/lib/dommy/internal/element_top_layer.rb +63 -0
- data/lib/dommy/internal/enumerated_keyword_sets.rb +53 -0
- data/lib/dommy/internal/exception_report.rb +1 -1
- data/lib/dommy/internal/input_type.rb +280 -0
- data/lib/dommy/internal/insertion_point.rb +62 -0
- data/lib/dommy/internal/mime_type.rb +74 -0
- data/lib/dommy/internal/mutation_coordinator.rb +57 -198
- data/lib/dommy/internal/namespaces.rb +41 -43
- data/lib/dommy/internal/node_adopter.rb +84 -77
- data/lib/dommy/internal/node_factory.rb +157 -0
- data/lib/dommy/internal/node_identity.rb +31 -0
- data/lib/dommy/internal/node_wrapper_cache.rb +61 -207
- data/lib/dommy/internal/observer_options.rb +93 -0
- data/lib/dommy/internal/parent_node.rb +27 -27
- data/lib/dommy/internal/post_insertion_steps.rb +213 -0
- data/lib/dommy/internal/reflected_attributes.rb +425 -13
- data/lib/dommy/internal/rendered_text/collector.rb +221 -0
- data/lib/dommy/internal/rendered_text/computed_box.rb +39 -0
- data/lib/dommy/internal/rendered_text/fragment.rb +55 -0
- data/lib/dommy/internal/rendered_text/items.rb +57 -0
- data/lib/dommy/internal/rendered_text/whitespace_collapser.rb +132 -0
- data/lib/dommy/internal/selector_arguments.rb +94 -0
- data/lib/dommy/internal/selector_index.rb +3 -1
- data/lib/dommy/internal/selector_matcher.rb +23 -383
- data/lib/dommy/internal/selector_parser.rb +2 -37
- data/lib/dommy/internal/template_content_registry.rb +112 -66
- data/lib/dommy/internal/text_flattening.rb +20 -0
- data/lib/dommy/internal/text_selection.rb +104 -0
- data/lib/dommy/internal/toggle_events.rb +56 -0
- data/lib/dommy/internal/toggle_task_tracker.rb +57 -0
- data/lib/dommy/internal/url_parser.rb +722 -569
- data/lib/dommy/internal/url_record_accessors.rb +90 -0
- data/lib/dommy/internal/window_constructors.rb +190 -0
- data/lib/dommy/internal/xml_serialization.rb +168 -80
- data/lib/dommy/js/crossing_profile.rb +75 -0
- data/lib/dommy/js/dom_interfaces.rb +22 -2
- data/lib/dommy/js/host_bridge.rb +43 -187
- data/lib/dommy/js/host_callbacks.rb +96 -0
- data/lib/dommy/js/host_runtime.js +840 -763
- data/lib/dommy/js/import_map.rb +10 -1
- data/lib/dommy/js/marshaller.rb +94 -65
- data/lib/dommy/js/observable_runtime.js +125 -146
- data/lib/dommy/js/runtime.rb +10 -43
- data/lib/dommy/js/runtime_registry.rb +46 -0
- data/lib/dommy/js/script_boot.rb +38 -73
- data/lib/dommy/js/webidl_tables.js +619 -0
- data/lib/dommy/js/window_wiring.rb +69 -0
- data/lib/dommy/location.rb +152 -103
- data/lib/dommy/mutation_observer.rb +3 -57
- data/lib/dommy/navigation.rb +65 -12
- data/lib/dommy/node.rb +46 -20
- data/lib/dommy/promise.rb +58 -4
- data/lib/dommy/resources.rb +19 -1
- data/lib/dommy/shadow_root.rb +11 -5
- data/lib/dommy/style_declaration.rb +284 -0
- data/lib/dommy/svg_elements.rb +1 -45
- data/lib/dommy/url.rb +20 -69
- data/lib/dommy/url_pattern/regexp_translator.rb +1 -6
- data/lib/dommy/url_pattern/tokenizer.rb +4 -2
- data/lib/dommy/validity_state.rb +383 -0
- data/lib/dommy/version.rb +1 -1
- data/lib/dommy/window.rb +30 -166
- data/lib/dommy/xml_http_request.rb +35 -4
- data/lib/dommy.rb +23 -3
- metadata +74 -7
- data/lib/dommy/bridge/constructor_registry.rb +0 -28
- data/lib/dommy/internal/css_pseudo_handlers.rb +0 -378
data/lib/dommy/bridge.rb
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Dommy
|
|
4
|
-
# `Dommy::Bridge` —
|
|
5
|
-
#
|
|
6
|
-
# through the `__js_get__` / `__js_set__` / `__js_call__` /
|
|
7
|
-
# `__js_new__` protocol).
|
|
4
|
+
# `Dommy::Bridge` — the protocol every JS host speaks to Dommy, and the
|
|
5
|
+
# vocabulary the DOM answers it in.
|
|
8
6
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
7
|
+
# THE RULE, because it has drifted before: what is true of ANY host belongs
|
|
8
|
+
# here; what is true of the host we happen to ship belongs in `Dommy::Js`.
|
|
9
|
+
# So the sentinels, the value types that cross the boundary, the exceptions
|
|
10
|
+
# a spec mandates and the wire tags are Bridge's, while the engine ABI, the
|
|
11
|
+
# marshaller that reads those tags and the QuickJS-facing runtime are Js's.
|
|
12
|
+
#
|
|
13
|
+
# This namespace is NOT optional or embedder-only, whatever an older version
|
|
14
|
+
# of this comment claimed: `Bridge::UNDEFINED` and `Bridge::Methods` are
|
|
15
|
+
# spoken by most of the DOM classes in this gem. A backend gem (dommy-js-
|
|
16
|
+
# quickjs, a future wasm one) depends on Bridge; nothing in the DOM depends
|
|
17
|
+
# on Js.
|
|
13
18
|
#
|
|
14
19
|
# The protocol contract:
|
|
15
20
|
# - `__js_get__(name)` reads a JS-style property by string name
|
|
@@ -17,246 +22,13 @@ module Dommy
|
|
|
17
22
|
# - `__js_call__(method, args)` invokes a method with positional
|
|
18
23
|
# args (Array)
|
|
19
24
|
# - `__js_new__(args)` invokes the value as a JS constructor
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
# rather than silently dropping it.
|
|
24
|
-
UNHANDLED = :__js_unhandled__
|
|
25
|
-
|
|
26
|
-
# Sentinel for the JS `undefined` value, used in both directions:
|
|
27
|
-
# - a `__js_call__` returns it for a void (undefined-returning) op, so the
|
|
28
|
-
# host marshals JS `undefined` rather than the `null` a bare Ruby `nil`
|
|
29
|
-
# would (e.g. DOMTokenList add/remove return undefined);
|
|
30
|
-
# - a top-level JS `undefined` *argument* arrives as it (whereas JS `null`
|
|
31
|
-
# arrives as `nil`), so WebIDL-style dispatch can tell an omitted optional
|
|
32
|
-
# argument from an explicit null.
|
|
33
|
-
# Its `to_s` is "undefined" so a DOMString coercion of a stray undefined is
|
|
34
|
-
# still spec-faithful.
|
|
35
|
-
UNDEFINED = Object.new
|
|
36
|
-
def UNDEFINED.to_s = "undefined"
|
|
37
|
-
def UNDEFINED.inspect = "#<Dommy::Bridge::UNDEFINED>"
|
|
38
|
-
UNDEFINED.freeze
|
|
39
|
-
|
|
40
|
-
# The sentinel a `__js_get__` returns for a GENUINELY-ABSENT property (a key
|
|
41
|
-
# the object does not have), as distinct from a present property whose value
|
|
42
|
-
# is `nil`/JS null or `UNDEFINED`/JS undefined. It marshals to JS `undefined`
|
|
43
|
-
# for the value, but the proxy reports `("x" in obj) === false` for it — so
|
|
44
|
-
# feature detection like `isUndefined(window.Vue)` AND `"Vue" in window` are
|
|
45
|
-
# both correct. (UNDEFINED means present-but-undefined → reported present.)
|
|
46
|
-
ABSENT = Object.new
|
|
47
|
-
def ABSENT.to_s = "undefined"
|
|
48
|
-
def ABSENT.inspect = "#<Dommy::Bridge::ABSENT>"
|
|
49
|
-
ABSENT.freeze
|
|
50
|
-
|
|
51
|
-
# An opaque handle to a JS-side value that Ruby only stores and hands back
|
|
52
|
-
# (an AbortSignal's reason, a CustomEvent's detail). A non-plain JS object
|
|
53
|
-
# (Error, class instance, …) crosses as one of these instead of being
|
|
54
|
-
# flattened to a Hash, so it round-trips with IDENTITY preserved. `to_s`
|
|
55
|
-
# exposes the captured JS string form for the rare Ruby consumer that needs
|
|
56
|
-
# text (e.g. building a message).
|
|
57
|
-
class JSValue
|
|
58
|
-
attr_reader :ref
|
|
59
|
-
|
|
60
|
-
# `stack_frames` are the value's JS stack, when the tag that crossed
|
|
61
|
-
# carried one. Ruby cannot reach through a ref to read `.stack`, so an
|
|
62
|
-
# error-shaped value brings its frames with it (see host_runtime.js
|
|
63
|
-
# tagValue) and reporting uses them for the source position.
|
|
64
|
-
attr_reader :stack_frames
|
|
65
|
-
|
|
66
|
-
# The value's JS constructor name, when the tag carried one.
|
|
67
|
-
attr_reader :js_name
|
|
68
|
-
|
|
69
|
-
def initialize(ref, label = nil, stack_frames = nil, js_name = nil)
|
|
70
|
-
@ref = ref
|
|
71
|
-
@label = label
|
|
72
|
-
@stack_frames = stack_frames
|
|
73
|
-
@js_name = js_name
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def to_s = (@label || "[object]").to_s
|
|
77
|
-
def inspect = "#<Dommy::Bridge::JSValue #{to_s}>"
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
# Raised by a host method that must throw an ARBITRARY value back to JS —
|
|
81
|
-
# not a DOMException/Error, but e.g. `signal.throwIfAborted()` throwing the
|
|
82
|
-
# exact abort reason (a string, number, or opaque JSValue). The bridge
|
|
83
|
-
# re-throws the wrapped value verbatim (identity preserved). Subclasses
|
|
84
|
-
# RuntimeError (with the value's string form as the message) so standalone
|
|
85
|
-
# CRuby callers still see a normal `raise`-able error.
|
|
86
|
-
class ThrowValue < RuntimeError
|
|
87
|
-
attr_reader :value
|
|
88
|
-
|
|
89
|
-
# `message` overrides the value's string form, for a caller that can say
|
|
90
|
-
# more about it than `to_s` can (a JS error's kind, say).
|
|
91
|
-
def initialize(value, message = nil)
|
|
92
|
-
@value = value
|
|
93
|
-
super(message || value.to_s)
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
# A byte buffer that crosses the JS boundary as a `Uint8Array` (rather than a
|
|
98
|
-
# plain Array). Wrap a host method's byte-array result in this so JS sees a
|
|
99
|
-
# real typed array — e.g. `TextEncoder#encode`, `Blob#arrayBuffer`. The
|
|
100
|
-
# reverse direction (a JS ArrayBuffer/TypedArray argument) arrives as a
|
|
101
|
-
# `Bytes` too. It subclasses Array so plain-Array callers (and `== [..]`
|
|
102
|
-
# comparisons) keep working; only the bridge treats it specially.
|
|
103
|
-
class Bytes < ::Array
|
|
104
|
-
def initialize(bytes = [])
|
|
105
|
-
super()
|
|
106
|
-
concat(Array(bytes).map { |b| b.to_i & 0xFF })
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
alias bytes to_a
|
|
110
|
-
def pack_bytes = pack("C*")
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
# Like `Bytes`, but crosses the JS boundary as a bare `ArrayBuffer` rather
|
|
114
|
-
# than a `Uint8Array` view. Use this for the spec methods whose return type
|
|
115
|
-
# is `ArrayBuffer` — `Response`/`Blob`/`FileReader`/`XMLHttpRequest`'s
|
|
116
|
-
# `arrayBuffer`. Subclasses `Bytes` so Ruby-level `== [..]` comparisons still
|
|
117
|
-
# hold; only the bridge distinguishes it (and must check it before `Bytes`).
|
|
118
|
-
class ArrayBuffer < Bytes
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
# A Ruby-side signal that the JS boundary should surface a JS `TypeError`
|
|
122
|
-
# (not a `DOMException`). Some WebIDL operations — notably the `URL`
|
|
123
|
-
# constructor and its `href` setter — throw `TypeError` on failure rather
|
|
124
|
-
# than a DOMException; raising this lets a host bridge rethrow the correct
|
|
125
|
-
# JS error type, while Ruby callers can still rescue it like any other
|
|
126
|
-
# error. Kept distinct from Ruby's built-in `::TypeError` so a host can map
|
|
127
|
-
# only deliberate, spec-mandated TypeErrors (and not mask genuine Ruby type
|
|
128
|
-
# bugs) across the boundary.
|
|
129
|
-
class TypeError < ::StandardError; end
|
|
130
|
-
|
|
131
|
-
# Like `Bridge::TypeError`, but for spec-mandated `RangeError`s (e.g. the
|
|
132
|
-
# `Response` constructor rejecting a status outside 200–599). A host bridge
|
|
133
|
-
# rethrows a real JS `RangeError`; kept distinct from Ruby's `::RangeError`.
|
|
134
|
-
class RangeError < ::StandardError; end
|
|
135
|
-
|
|
136
|
-
# Wraps an external callback handle (registered in a host-side
|
|
137
|
-
# callback table) so the JS bridge can resolve / invoke it. The
|
|
138
|
-
# external host that creates these is responsible for honoring
|
|
139
|
-
# `invoke_callback(callback_id, args)`.
|
|
140
|
-
#
|
|
141
|
-
# The `__callback_id__` key on this object exposes the integer
|
|
142
|
-
# id to JS-side code that needs to round-trip it (e.g. for
|
|
143
|
-
# release / introspection).
|
|
144
|
-
class Callback
|
|
145
|
-
ID_KEY = "__callback_id__"
|
|
146
|
-
|
|
147
|
-
def initialize(host, callback_id)
|
|
148
|
-
@host = host
|
|
149
|
-
@callback_id = callback_id
|
|
150
|
-
@props = {}
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
def __js_get__(key)
|
|
154
|
-
if key == ID_KEY
|
|
155
|
-
@props.fetch(key, @callback_id)
|
|
156
|
-
else
|
|
157
|
-
@props[key]
|
|
158
|
-
end
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
def __js_set__(key, value)
|
|
162
|
-
@props[key] = value
|
|
163
|
-
nil
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
def __js_call__(method, args)
|
|
167
|
-
case method
|
|
168
|
-
when "call"
|
|
169
|
-
@host.invoke_callback(@callback_id, args)
|
|
170
|
-
end
|
|
171
|
-
end
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
# Block-as-constructor adapter — invoking `__js_new__(args)`
|
|
175
|
-
# calls the wrapped block with `args` and returns whatever the
|
|
176
|
-
# block produces. Used by Window to wire up `new Event(init)`,
|
|
177
|
-
# `new CustomEvent(init)`, etc. without hand-rolling a class
|
|
178
|
-
# for each constructor.
|
|
179
|
-
class Constructor
|
|
180
|
-
def initialize(&block)
|
|
181
|
-
@block = block
|
|
182
|
-
@class_methods = {}
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
def __js_new__(args)
|
|
186
|
-
@block.call(args)
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
# Register a class-level method (e.g. `URL.createObjectURL`)
|
|
190
|
-
# that JS bridges resolve via `__js_call__` on the constructor
|
|
191
|
-
# itself. Returns self for chaining.
|
|
192
|
-
def define_class_method(name, &block)
|
|
193
|
-
@class_methods[name.to_s] = block
|
|
194
|
-
self
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
def __js_call__(method, args)
|
|
198
|
-
handler = @class_methods[method.to_s]
|
|
199
|
-
handler&.call(args)
|
|
200
|
-
end
|
|
201
|
-
|
|
202
|
-
# Names of the registered class-level (static) methods, so a JS host can
|
|
203
|
-
# expose them on the constructor function (e.g. `URL.createObjectURL`).
|
|
204
|
-
def __js_class_method_names__
|
|
205
|
-
@class_methods.keys
|
|
206
|
-
end
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
# `JS.global[:Promise]` view. Implements the `resolve` / `reject`
|
|
210
|
-
# class methods plus `new Promise(executor)` via `__js_new__`.
|
|
211
|
-
class PromiseConstructor
|
|
212
|
-
def initialize(window)
|
|
213
|
-
@window = window
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
def __js_call__(method, args)
|
|
217
|
-
case method
|
|
218
|
-
when "resolve"
|
|
219
|
-
PromiseValue.resolve(@window, args[0])
|
|
220
|
-
when "reject"
|
|
221
|
-
PromiseValue.reject(@window, args[0])
|
|
222
|
-
end
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
# `new Promise(executor)` — runs executor synchronously with
|
|
226
|
-
# (resolve, reject) callbacks.
|
|
227
|
-
def __js_new__(args)
|
|
228
|
-
executor = args[0]
|
|
229
|
-
promise = PromiseValue.new(@window)
|
|
230
|
-
resolve = PromiseSettler.new(promise, fulfilled: true)
|
|
231
|
-
reject = PromiseSettler.new(promise, fulfilled: false)
|
|
232
|
-
if executor.respond_to?(:__js_call__)
|
|
233
|
-
executor.__js_call__("call", [resolve, reject])
|
|
234
|
-
elsif executor.respond_to?(:call)
|
|
235
|
-
executor.call(resolve, reject)
|
|
236
|
-
end
|
|
237
|
-
|
|
238
|
-
promise
|
|
239
|
-
end
|
|
240
|
-
end
|
|
241
|
-
|
|
242
|
-
# Adapter so a Ruby-side executor can deliver resolve/reject
|
|
243
|
-
# through the same `__js_call__("call", args)` interface that
|
|
244
|
-
# the scheduler and JS bridge use for callbacks.
|
|
245
|
-
class PromiseSettler
|
|
246
|
-
def initialize(promise, fulfilled:)
|
|
247
|
-
@promise = promise
|
|
248
|
-
@fulfilled = fulfilled
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
def __js_call__(_method, args)
|
|
252
|
-
if @fulfilled
|
|
253
|
-
@promise.fulfill(args[0])
|
|
254
|
-
else
|
|
255
|
-
@promise.reject(args[0])
|
|
256
|
-
end
|
|
257
|
-
|
|
258
|
-
nil
|
|
259
|
-
end
|
|
260
|
-
end
|
|
261
|
-
end
|
|
25
|
+
#
|
|
26
|
+
# The pieces are one file each, by what they are — see the requires below.
|
|
27
|
+
module Bridge; end
|
|
262
28
|
end
|
|
29
|
+
|
|
30
|
+
require_relative "bridge/sentinels"
|
|
31
|
+
require_relative "bridge/values"
|
|
32
|
+
require_relative "bridge/errors"
|
|
33
|
+
require_relative "bridge/constructor"
|
|
34
|
+
require_relative "bridge/wire_tags"
|
data/lib/dommy/browser.rb
CHANGED
|
@@ -70,7 +70,7 @@ module Dommy
|
|
|
70
70
|
@error_log = Js::ErrorLog.new(strict: strict)
|
|
71
71
|
@console = []
|
|
72
72
|
@disposed = false
|
|
73
|
-
@
|
|
73
|
+
@pending_navigations = []
|
|
74
74
|
@runtime = nil
|
|
75
75
|
|
|
76
76
|
@window = Dommy.parse(html)
|
|
@@ -104,7 +104,7 @@ module Dommy
|
|
|
104
104
|
def visit(url, replace: false)
|
|
105
105
|
raise "browser is not navigable (use Browser.visit or navigable: true)" unless @navigable
|
|
106
106
|
|
|
107
|
-
@
|
|
107
|
+
@pending_navigations << {url: url.to_s, method: "GET", source: :visit, replace: replace}
|
|
108
108
|
flush_navigation!
|
|
109
109
|
self
|
|
110
110
|
end
|
|
@@ -128,14 +128,61 @@ module Dommy
|
|
|
128
128
|
# inside a script), record it and perform the fetch + swap at the next drain
|
|
129
129
|
# boundary (settle / after_interaction / advance_time). Ruby-initiated visits
|
|
130
130
|
# flush immediately since no JS is on the stack.
|
|
131
|
-
def navigate(url:, source:, method: "GET", body: nil, params: nil, enctype: nil, headers: {}, replace: false)
|
|
132
|
-
|
|
131
|
+
def navigate(url:, source:, method: "GET", body: nil, params: nil, enctype: nil, target: nil, headers: {}, replace: false)
|
|
132
|
+
# A target that names an iframe in this document navigates that nested
|
|
133
|
+
# browsing context. It can be performed now — it does not replace the top
|
|
134
|
+
# realm — with only the frame's `load` deferred to a microtask, so a
|
|
135
|
+
# listener installed right after `form.submit()` is registered first.
|
|
136
|
+
frame = @navigable ? resolve_target_frame(target) : nil
|
|
137
|
+
if frame
|
|
138
|
+
navigate_frame(frame,
|
|
139
|
+
{method: method, url: url, params: params, body: body, enctype: enctype, headers: headers},
|
|
140
|
+
resolve_against_current(url.to_s))
|
|
141
|
+
@window.scheduler.queue_microtask(proc { frame.dispatch_event(Dommy::Event.new("load")) })
|
|
142
|
+
return nil
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
@pending_navigations << {
|
|
133
146
|
url: url, method: method, body: body, params: params, enctype: enctype,
|
|
134
|
-
headers: headers, replace: replace, source: source
|
|
147
|
+
target: target, headers: headers, replace: replace, source: source
|
|
135
148
|
}
|
|
136
149
|
nil
|
|
137
150
|
end
|
|
138
151
|
|
|
152
|
+
# A navigation delegate for an embedder-managed nested browsing context (an
|
|
153
|
+
# iframe whose content document the host injected). A navigation from inside
|
|
154
|
+
# that document loads into the frame itself.
|
|
155
|
+
class FrameNavigationDelegate
|
|
156
|
+
def initialize(browser, frame)
|
|
157
|
+
@browser = browser
|
|
158
|
+
@frame = frame
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def navigate(url:, source:, method: "GET", body: nil, params: nil, enctype: nil, target: nil, headers: {}, replace: false)
|
|
162
|
+
@browser.__internal_load_frame__(@frame,
|
|
163
|
+
{url: url, method: method, body: body, params: params, enctype: enctype, headers: headers})
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def traverse(_delta) = nil
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def frame_navigation_delegate(frame) = FrameNavigationDelegate.new(self, frame)
|
|
170
|
+
|
|
171
|
+
# Load `nav` into `frame`, resolving the URL against the frame's own document,
|
|
172
|
+
# then fire the frame's `load`. Public (an `__internal_` seam) so a host that
|
|
173
|
+
# injected the frame's content document can wire it as that frame's delegate.
|
|
174
|
+
def __internal_load_frame__(frame, nav)
|
|
175
|
+
base = frame.content_window&.location&.__js_get__("href").to_s
|
|
176
|
+
resolved = begin
|
|
177
|
+
URI.join(base, nav[:url].to_s).to_s
|
|
178
|
+
rescue URI::InvalidURIError, ArgumentError
|
|
179
|
+
nav[:url].to_s
|
|
180
|
+
end
|
|
181
|
+
navigate_frame(frame, nav, resolved)
|
|
182
|
+
frame.dispatch_event(Dommy::Event.new("load"))
|
|
183
|
+
nil
|
|
184
|
+
end
|
|
185
|
+
|
|
139
186
|
# A cross-document history traversal. Ruby-initiated (back / forward), so it
|
|
140
187
|
# runs immediately: a same-document target (its window is still live)
|
|
141
188
|
# traverses in place (popstate); a document-boundary target is re-fetched.
|
|
@@ -316,8 +363,16 @@ module Dommy
|
|
|
316
363
|
MAX_META_REFRESHES = 20
|
|
317
364
|
|
|
318
365
|
def perform_navigation!(nav, rebind: false, refresh_depth: 0)
|
|
366
|
+
# A form's action is a document-relative URL; resolve it against the
|
|
367
|
+
# current address (links already arrive resolved via Location).
|
|
368
|
+
resolved = resolve_against_current(nav[:url].to_s)
|
|
369
|
+
# A `target` naming an iframe in this document navigates that nested
|
|
370
|
+
# browsing context instead of replacing the top-level page.
|
|
371
|
+
frame = resolve_target_frame(nav[:target])
|
|
372
|
+
return navigate_frame(frame, nav, resolved) if frame
|
|
373
|
+
|
|
319
374
|
response, final_url = @fetcher.request(
|
|
320
|
-
method: nav[:method] || "GET", url:
|
|
375
|
+
method: nav[:method] || "GET", url: resolved, params: nav[:params],
|
|
321
376
|
body: nav[:body], enctype: nav[:enctype], headers: nav[:headers] || {}
|
|
322
377
|
)
|
|
323
378
|
return unless response&.success? && document_response?(response)
|
|
@@ -343,6 +398,53 @@ module Dommy
|
|
|
343
398
|
follow_meta_refresh!(refresh_depth)
|
|
344
399
|
end
|
|
345
400
|
|
|
401
|
+
# The reserved browsing-context keywords; anything else names an iframe.
|
|
402
|
+
RESERVED_TARGETS = %w[_self _top _parent _blank].freeze
|
|
403
|
+
|
|
404
|
+
# The iframe a `target` names in the current document, or nil when the
|
|
405
|
+
# target is empty / a reserved keyword / names no frame (then the navigation
|
|
406
|
+
# is top-level, the only context Dommy models).
|
|
407
|
+
def resolve_target_frame(target)
|
|
408
|
+
name = target.to_s
|
|
409
|
+
return nil if name.empty? || RESERVED_TARGETS.include?(name.downcase)
|
|
410
|
+
|
|
411
|
+
@window.document.query_selector_all("iframe").find do |frame|
|
|
412
|
+
frame.get_attribute("name").to_s == name
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
# Load a navigation into a nested browsing context (a named iframe): fetch
|
|
417
|
+
# it, install the response document as the frame's content, and fire the
|
|
418
|
+
# frame's `load`. The top-level window and joint history are left alone.
|
|
419
|
+
def navigate_frame(frame, nav, resolved_url)
|
|
420
|
+
response, final_url = @fetcher.request(
|
|
421
|
+
method: nav[:method] || "GET", url: resolved_url, params: nav[:params],
|
|
422
|
+
body: nav[:body], enctype: nav[:enctype], headers: nav[:headers] || {}
|
|
423
|
+
)
|
|
424
|
+
return unless response&.success?
|
|
425
|
+
|
|
426
|
+
sub_window = frame_document_for(response)
|
|
427
|
+
sub_window.location.__internal_set_url__(final_url)
|
|
428
|
+
# A navigation from inside the loaded frame also stays in that frame.
|
|
429
|
+
sub_window.navigation_delegate = frame_navigation_delegate(frame)
|
|
430
|
+
# A nested realm needs the seeded constructors to run the response's
|
|
431
|
+
# scripts; a runtime that cannot expose them simply runs without them.
|
|
432
|
+
@runtime.expose_constructors_on(sub_window) if @runtime.respond_to?(:expose_constructors_on)
|
|
433
|
+
frame.__internal_set_content_document__(sub_window.document)
|
|
434
|
+
nil
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
# The document a frame shows for a response: HTML/XML is parsed as-is; a
|
|
438
|
+
# non-document response (e.g. text/plain from an echo endpoint) is displayed
|
|
439
|
+
# as text, so the frame gets a document whose body holds it.
|
|
440
|
+
def frame_document_for(response)
|
|
441
|
+
return Dommy.parse(response.body) if document_response?(response)
|
|
442
|
+
|
|
443
|
+
win = Dommy.parse("<!doctype html><html><head></head><body></body></html>")
|
|
444
|
+
win.document.body.text_content = response.body.to_s.dup.force_encoding(Encoding::UTF_8)
|
|
445
|
+
win
|
|
446
|
+
end
|
|
447
|
+
|
|
346
448
|
# If the freshly loaded document asks for an immediate `<meta http-equiv=
|
|
347
449
|
# refresh>`, follow it (as a replace, like a redirect), capped so a page that
|
|
348
450
|
# refreshes to itself can't loop forever.
|
|
@@ -383,12 +485,11 @@ module Dommy
|
|
|
383
485
|
# the swap never runs with the outgoing realm's JS on the stack.
|
|
384
486
|
def flush_navigation!
|
|
385
487
|
return unless @navigable
|
|
488
|
+
return if @pending_navigations.empty?
|
|
386
489
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
@pending_navigation = nil
|
|
391
|
-
perform_navigation!(nav)
|
|
490
|
+
navs = @pending_navigations
|
|
491
|
+
@pending_navigations = []
|
|
492
|
+
navs.each { |nav| perform_navigation!(nav) }
|
|
392
493
|
end
|
|
393
494
|
|
|
394
495
|
def fire_unload(window)
|