dommy 0.12.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 +22 -11
- data/lib/dommy/bridge.rb +22 -237
- data/lib/dommy/browser.rb +155 -58
- data/lib/dommy/callable_invoker.rb +16 -0
- data/lib/dommy/character_data.rb +539 -0
- data/lib/dommy/class_list.rb +311 -0
- data/lib/dommy/compression_streams.rb +12 -62
- 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 +312 -374
- data/lib/dommy/dom_parser.rb +3 -0
- data/lib/dommy/element.rb +201 -1820
- data/lib/dommy/encodings/tables.rb +751 -0
- data/lib/dommy/encodings.rb +330 -0
- data/lib/dommy/event.rb +231 -6
- data/lib/dommy/event_source.rb +41 -0
- data/lib/dommy/fetch.rb +60 -49
- data/lib/dommy/form_data.rb +16 -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 -5248
- 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 +123 -0
- data/lib/dommy/internal/idna.rb +4 -4
- data/lib/dommy/internal/idna_data.rb +513 -100
- 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 +732 -527
- 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 +23 -2
- data/lib/dommy/js/error_log.rb +137 -0
- data/lib/dommy/js/host_bridge.rb +56 -187
- data/lib/dommy/js/host_callbacks.rb +96 -0
- data/lib/dommy/js/host_runtime.js +896 -729
- data/lib/dommy/js/import_map.rb +10 -1
- data/lib/dommy/js/marshaller.rb +111 -63
- data/lib/dommy/js/observable_runtime.js +125 -146
- data/lib/dommy/js/runtime.rb +17 -43
- data/lib/dommy/js/runtime_registry.rb +46 -0
- data/lib/dommy/js/script_boot.rb +94 -85
- data/lib/dommy/js/webidl_tables.js +619 -0
- data/lib/dommy/js/window_wiring.rb +69 -0
- data/lib/dommy/location.rb +161 -102
- data/lib/dommy/mutation_observer.rb +3 -57
- data/lib/dommy/navigation.rb +65 -12
- data/lib/dommy/navigator.rb +3 -1
- data/lib/dommy/node.rb +46 -20
- data/lib/dommy/promise.rb +62 -8
- data/lib/dommy/resources.rb +19 -1
- data/lib/dommy/scheduler.rb +35 -6
- data/lib/dommy/shadow_root.rb +11 -5
- data/lib/dommy/streams.rb +1200 -187
- data/lib/dommy/style_declaration.rb +284 -0
- data/lib/dommy/svg_elements.rb +1 -45
- data/lib/dommy/text_codec.rb +110 -213
- data/lib/dommy/url.rb +32 -79
- 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 +184 -192
- data/lib/dommy/xml_http_request.rb +52 -7
- data/lib/dommy.rb +25 -3
- metadata +78 -7
- data/lib/dommy/bridge/constructor_registry.rb +0 -28
- data/lib/dommy/internal/css_pseudo_handlers.rb +0 -378
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d623f452622acb0d99a40af340aefc6dfc86a4c466277097132e0a3dcff5b741
|
|
4
|
+
data.tar.gz: 38ea700cb1a482696ca8253b5bbf8901c5f34f564ed760c57dae1e0d0abf9f92
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b94eb7cece5d3753d03a0e763f7b0a8ddf7cf38360cd3841f195e9616b9caa2f24ac286bf897866f762694a2f48a5292ba01abd69c6a9213f59d8703e32c9509
|
|
7
|
+
data.tar.gz: f6cd5abc39c423a6d5f621a2c6209df9e622c7ee9ec3c805d3e2730ace518de1efe8614d4a91b48748e2515f37917bb607c9f89719f9ef0168f9028e55b8aca5
|
|
@@ -23,12 +23,7 @@ module Dommy
|
|
|
23
23
|
DocumentFragment = ::Makiri::DocumentFragment
|
|
24
24
|
DocumentType = ::Makiri::DocumentType
|
|
25
25
|
Node = ::Makiri::Node
|
|
26
|
-
|
|
27
|
-
# A minimal namespace wrapper exposing the same `href` API that Nokogiri's
|
|
28
|
-
# Namespace object has, so calling code treats both backends uniformly.
|
|
29
|
-
Namespace = Struct.new(:href)
|
|
30
|
-
|
|
31
|
-
HTML_NAMESPACE_URI = "http://www.w3.org/1999/xhtml"
|
|
26
|
+
XMLSyntaxError = ::Makiri::XML::SyntaxError
|
|
32
27
|
|
|
33
28
|
# Throwaway attribute used to bind `:scope` to a context element — Lexbor
|
|
34
29
|
# has no `:scope`, so a scoped query temporarily marks the element and
|
|
@@ -98,6 +93,10 @@ module Dommy
|
|
|
98
93
|
target_doc.import_node(node, true)
|
|
99
94
|
end
|
|
100
95
|
|
|
96
|
+
def import_element(node, target_doc)
|
|
97
|
+
target_doc.import_node(node, false)
|
|
98
|
+
end
|
|
99
|
+
|
|
101
100
|
# Lexbor arenas can't move a node between documents — a foreign node must be
|
|
102
101
|
# imported (see #adopt) before insertion.
|
|
103
102
|
def moves_nodes_across_documents?
|
|
@@ -162,11 +161,22 @@ module Dommy
|
|
|
162
161
|
# Makiri's loose creator, which builds it verbatim (case/prefix preserved).
|
|
163
162
|
# nil for a non-XML backend → the caller uses the strict #create_element.
|
|
164
163
|
def create_element_loose(qualified_name, prefix, local, namespace, doc)
|
|
165
|
-
return nil unless doc.is_a?(::Makiri::XML::Document)
|
|
164
|
+
return nil unless doc.is_a?(::Makiri::XML::Document)
|
|
166
165
|
|
|
167
166
|
doc.create_loose_dom_element(qualified_name, prefix, local, namespace)
|
|
168
167
|
end
|
|
169
168
|
|
|
169
|
+
# createElementNS in an HTML document. Makiri builds the element in its
|
|
170
|
+
# own namespace, so the backend node is what the parser would have made:
|
|
171
|
+
# an SVG `feGaussianBlur` keeps its case and `[viewBox]` reads its
|
|
172
|
+
# attribute case-sensitively. nil for an XML document → the caller uses
|
|
173
|
+
# #create_element.
|
|
174
|
+
def create_element_ns(namespace, qualified_name, doc)
|
|
175
|
+
return nil unless doc.is_a?(::Makiri::HTML::Document)
|
|
176
|
+
|
|
177
|
+
doc.create_element_ns(presence(namespace), qualified_name.to_s)
|
|
178
|
+
end
|
|
179
|
+
|
|
170
180
|
# A detached DocumentType node owned by `doc`, for
|
|
171
181
|
# DOMImplementation.createDocumentType. Only the HTML backend ships the
|
|
172
182
|
# factory (`create_document_type`); nil signals the caller to fall back to a
|
|
@@ -231,20 +241,10 @@ module Dommy
|
|
|
231
241
|
::Makiri::ProcessingInstruction
|
|
232
242
|
end
|
|
233
243
|
|
|
234
|
-
#
|
|
235
|
-
#
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
# SVG / MathML). nil for the HTML namespace, so Element#namespace_uri
|
|
239
|
-
# falls back to its HTML default (and the wrapper is allocated only for
|
|
240
|
-
# genuine foreign content).
|
|
241
|
-
def namespace_of(node)
|
|
242
|
-
return nil unless node.respond_to?(:namespace_uri)
|
|
243
|
-
|
|
244
|
-
uri = node.namespace_uri
|
|
245
|
-
return nil if uri.nil? || uri.empty? || uri == HTML_NAMESPACE_URI
|
|
246
|
-
|
|
247
|
-
Namespace.new(uri)
|
|
244
|
+
# The element's namespace as the backend tracks it (Lexbor's HTML / SVG /
|
|
245
|
+
# MathML, an XML document's own), nil for none.
|
|
246
|
+
def namespace_uri(node)
|
|
247
|
+
presence(node.respond_to?(:namespace_uri) ? node.namespace_uri : nil)
|
|
248
248
|
end
|
|
249
249
|
|
|
250
250
|
# Bind a *prefixed* element's namespace so the prefix resolves. An XML
|
|
@@ -264,11 +264,13 @@ module Dommy
|
|
|
264
264
|
|
|
265
265
|
node["xmlns:#{prefix}"] = href.to_s
|
|
266
266
|
nil
|
|
267
|
-
rescue ArgumentError
|
|
267
|
+
rescue ArgumentError, ::Makiri::Error
|
|
268
268
|
# DOM validates a qualified name against the Name production, which
|
|
269
269
|
# admits prefixes an XML backend cannot spell as an `xmlns:` attribute
|
|
270
|
-
# ("0:a", ";:a"
|
|
271
|
-
#
|
|
270
|
+
# ("0:a", ";:a" — ArgumentError), and binds prefixes Namespaces in XML
|
|
271
|
+
# forbids declaring ("f" to the XML namespace, anything to the XMLNS
|
|
272
|
+
# one — Makiri::Error). The element is still valid — its prefix and
|
|
273
|
+
# namespace live on the wrapper — so the declaration is simply not written.
|
|
272
274
|
nil
|
|
273
275
|
end
|
|
274
276
|
|
|
@@ -279,9 +281,8 @@ module Dommy
|
|
|
279
281
|
|
|
280
282
|
# Lexbor keeps <template> contents in a separate content fragment rather
|
|
281
283
|
# than the normal child chain.
|
|
282
|
-
def
|
|
283
|
-
|
|
284
|
-
cf ? cf.children.to_a : []
|
|
284
|
+
def template_contents(node)
|
|
285
|
+
node.respond_to?(:content_fragment) ? node.content_fragment : nil
|
|
285
286
|
end
|
|
286
287
|
|
|
287
288
|
# ----- Namespaced attributes -----
|
|
@@ -299,8 +300,41 @@ module Dommy
|
|
|
299
300
|
end
|
|
300
301
|
|
|
301
302
|
def set_attribute_ns(node, namespace, _prefix, _local_name, qualified_name, value)
|
|
302
|
-
|
|
303
|
-
|
|
303
|
+
ns = presence(namespace)
|
|
304
|
+
name = qualified_name.to_s
|
|
305
|
+
value = value.to_s
|
|
306
|
+
node.set_attribute_ns(ns, name, value) unless ns.nil? && set_null_namespace_attribute(node, name, value)
|
|
307
|
+
value
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# A null-namespace name that only the DOM's `setAttribute` can make: one
|
|
311
|
+
# whose local name holds a colon ("xlink:href", "v-on:click") or is
|
|
312
|
+
# "xmlns". Makiri checks set_attribute_ns as the DOM's setAttributeNS
|
|
313
|
+
# does, which refuses these (a prefix needs a namespace).
|
|
314
|
+
def set_attribute_only_name?(name)
|
|
315
|
+
name == "xmlns" || name.include?(":")
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Makiri's own `setAttribute` for the setAttribute-only names, which
|
|
319
|
+
# set_attribute_ns refuses. true when it made the attribute; false leaves
|
|
320
|
+
# every other name to set_attribute_ns, which matches an existing
|
|
321
|
+
# attribute by (namespace, local name) rather than by qualified name:
|
|
322
|
+
# `setAttributeNS("u", "a")` then a null-namespace "a" are two attributes.
|
|
323
|
+
#
|
|
324
|
+
# - An XML document: set_loose_dom_attribute, a plain attribute (never a
|
|
325
|
+
# namespace declaration) under the name as given.
|
|
326
|
+
# - An HTML document: `[]=`. It lower-cases only on an HTML-namespace
|
|
327
|
+
# element, as setAttribute does, and makes the colon part of the local
|
|
328
|
+
# name.
|
|
329
|
+
def set_null_namespace_attribute(node, name, value)
|
|
330
|
+
return false unless set_attribute_only_name?(name)
|
|
331
|
+
|
|
332
|
+
if node.respond_to?(:set_loose_dom_attribute)
|
|
333
|
+
node.set_loose_dom_attribute(name, value)
|
|
334
|
+
else
|
|
335
|
+
node[name] = value
|
|
336
|
+
end
|
|
337
|
+
true
|
|
304
338
|
end
|
|
305
339
|
|
|
306
340
|
def remove_attribute_ns(node, namespace, local_name)
|
data/lib/dommy/backend.rb
CHANGED
|
@@ -79,6 +79,12 @@ module Dommy
|
|
|
79
79
|
current.adopt(node, target_doc)
|
|
80
80
|
end
|
|
81
81
|
|
|
82
|
+
# A copy of the element `node` alone, owned by `target_doc`: its name and
|
|
83
|
+
# attributes exactly as the backend holds them.
|
|
84
|
+
def import_element(node, target_doc)
|
|
85
|
+
current.import_element(node, target_doc)
|
|
86
|
+
end
|
|
87
|
+
|
|
82
88
|
# Whether the backend can move a node between documents in place or must adopt a copy first
|
|
83
89
|
# (Lexbor's arenas can't move a node, so inserting a foreign node requires importing
|
|
84
90
|
# it). Lets callers skip a needless — and on an empty target, root-less and
|
|
@@ -134,6 +140,20 @@ module Dommy
|
|
|
134
140
|
current.create_element_loose(qualified_name, prefix, local, namespace, doc)
|
|
135
141
|
end
|
|
136
142
|
|
|
143
|
+
# The element's own namespace URI as the DOM reports it: nil for no
|
|
144
|
+
# namespace.
|
|
145
|
+
def namespace_uri(node)
|
|
146
|
+
current.namespace_uri(node)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Create an element in `namespace` in an HTML document, so the backend
|
|
150
|
+
# node carries the namespace and the name's case itself. Returns nil for
|
|
151
|
+
# an XML document (fall back to #create_element); raises ArgumentError
|
|
152
|
+
# for an invalid name (the caller maps it to InvalidCharacterError).
|
|
153
|
+
def create_element_ns(namespace, qualified_name, doc)
|
|
154
|
+
current.create_element_ns(namespace, qualified_name, doc)
|
|
155
|
+
end
|
|
156
|
+
|
|
137
157
|
# A detached DocumentType node owned by `doc` (for
|
|
138
158
|
# DOMImplementation.createDocumentType). Returns nil when the backend has no
|
|
139
159
|
# doctype factory (the caller falls back to a synthetic DocumentType); raises
|
|
@@ -183,21 +203,18 @@ module Dommy
|
|
|
183
203
|
current.processing_instruction_class
|
|
184
204
|
end
|
|
185
205
|
|
|
186
|
-
def namespace_of(node)
|
|
187
|
-
current.namespace_of(node)
|
|
188
|
-
end
|
|
189
|
-
|
|
190
206
|
# The element's in-scope namespace declarations (each responds to
|
|
191
207
|
# `prefix`/`href`). Empty on backends without an XML namespace model.
|
|
192
208
|
def namespace_definitions(node)
|
|
193
209
|
current.namespace_definitions(node)
|
|
194
210
|
end
|
|
195
211
|
|
|
196
|
-
# The
|
|
197
|
-
#
|
|
198
|
-
#
|
|
199
|
-
|
|
200
|
-
|
|
212
|
+
# The backend's own fragment holding a `<template>` element's contents
|
|
213
|
+
# (Lexbor keeps them off the child list), the same one every time; nil
|
|
214
|
+
# for a node that has none — any node of an XML document, whose contents
|
|
215
|
+
# the template-content registry keeps instead.
|
|
216
|
+
def template_contents(node)
|
|
217
|
+
current.template_contents(node)
|
|
201
218
|
end
|
|
202
219
|
|
|
203
220
|
def add_namespace_definition(node, prefix, href)
|
|
@@ -262,6 +279,12 @@ module Dommy
|
|
|
262
279
|
current::Document
|
|
263
280
|
end
|
|
264
281
|
|
|
282
|
+
# What the backend raises for markup that is not well-formed XML (an XML
|
|
283
|
+
# document's fragment parse included).
|
|
284
|
+
def xml_syntax_error_class
|
|
285
|
+
current::XMLSyntaxError
|
|
286
|
+
end
|
|
287
|
+
|
|
265
288
|
def text_class
|
|
266
289
|
current::Text
|
|
267
290
|
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
module Bridge
|
|
5
|
+
# A JS constructor function as Ruby sees it: `new X(...)` plus the static
|
|
6
|
+
# methods hanging off it, and the table Window looks names up in. The
|
|
7
|
+
# registry maps to these, so they share a file.
|
|
8
|
+
|
|
9
|
+
# Block-as-constructor adapter — invoking `__js_new__(args)`
|
|
10
|
+
# calls the wrapped block with `args` and returns whatever the
|
|
11
|
+
# block produces. Used by Window to wire up `new Event(init)`,
|
|
12
|
+
# `new CustomEvent(init)`, etc. without hand-rolling a class
|
|
13
|
+
# for each constructor.
|
|
14
|
+
class Constructor
|
|
15
|
+
def initialize(&block)
|
|
16
|
+
@block = block
|
|
17
|
+
@class_methods = {}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def __js_new__(args)
|
|
21
|
+
@block.call(args)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Register a class-level method (e.g. `URL.createObjectURL`)
|
|
25
|
+
# that JS bridges resolve via `__js_call__` on the constructor
|
|
26
|
+
# itself. Returns self for chaining.
|
|
27
|
+
def define_class_method(name, &block)
|
|
28
|
+
@class_methods[name.to_s] = block
|
|
29
|
+
self
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# A name outside the registered statics is not a method of this
|
|
33
|
+
# constructor, and answering nil would make `URL.notAThing()` read as a
|
|
34
|
+
# call that returned null rather than one that should not have compiled.
|
|
35
|
+
def __js_call__(method, args)
|
|
36
|
+
handler = @class_methods[method.to_s]
|
|
37
|
+
raise TypeError, "#{method} is not a function" unless handler
|
|
38
|
+
|
|
39
|
+
handler.call(args)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Names of the registered class-level (static) methods, so a JS host can
|
|
43
|
+
# expose them on the constructor function (e.g. `URL.createObjectURL`).
|
|
44
|
+
def __js_class_method_names__
|
|
45
|
+
@class_methods.keys
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Maps JS global constructor names (e.g. "Event", "URL", "XMLHttpRequest")
|
|
50
|
+
# to their `Bridge::Constructor` instances. Window builds one and routes
|
|
51
|
+
# `__js_get__` name lookups through it, instead of carrying one ivar plus
|
|
52
|
+
# one `when` arm per constructor.
|
|
53
|
+
class ConstructorRegistry
|
|
54
|
+
def initialize(map)
|
|
55
|
+
@map = map.freeze
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def [](name)
|
|
59
|
+
@map[name]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def key?(name)
|
|
63
|
+
@map.key?(name)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# The set of constructor names, e.g. for host-side enumeration / tests.
|
|
67
|
+
def names
|
|
68
|
+
@map.keys
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
module Bridge
|
|
5
|
+
# The exceptions a spec mandates the JS side see as something other than a
|
|
6
|
+
# DOMException — a real TypeError, a real RangeError, or an arbitrary thrown
|
|
7
|
+
# value. Each is kept distinct from Ruby's own so a host maps only the
|
|
8
|
+
# deliberate ones and never masks a genuine Ruby bug.
|
|
9
|
+
|
|
10
|
+
# Raised by a host method that must throw an ARBITRARY value back to JS —
|
|
11
|
+
# not a DOMException/Error, but e.g. `signal.throwIfAborted()` throwing the
|
|
12
|
+
# exact abort reason (a string, number, or opaque JSValue). The bridge
|
|
13
|
+
# re-throws the wrapped value verbatim (identity preserved). Subclasses
|
|
14
|
+
# RuntimeError (with the value's string form as the message) so standalone
|
|
15
|
+
# CRuby callers still see a normal `raise`-able error.
|
|
16
|
+
class ThrowValue < RuntimeError
|
|
17
|
+
attr_reader :value
|
|
18
|
+
|
|
19
|
+
# `message` overrides the value's string form, for a caller that can say
|
|
20
|
+
# more about it than `to_s` can (a JS error's kind, say).
|
|
21
|
+
def initialize(value, message = nil)
|
|
22
|
+
@value = value
|
|
23
|
+
super(message || value.to_s)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# A Ruby-side signal that the JS boundary should surface a JS `TypeError`
|
|
28
|
+
# (not a `DOMException`). Some WebIDL operations — notably the `URL`
|
|
29
|
+
# constructor and its `href` setter — throw `TypeError` on failure rather
|
|
30
|
+
# than a DOMException; raising this lets a host bridge rethrow the correct
|
|
31
|
+
# JS error type, while Ruby callers can still rescue it like any other
|
|
32
|
+
# error. Kept distinct from Ruby's built-in `::TypeError` so a host can map
|
|
33
|
+
# only deliberate, spec-mandated TypeErrors (and not mask genuine Ruby type
|
|
34
|
+
# bugs) across the boundary.
|
|
35
|
+
class TypeError < ::StandardError; end
|
|
36
|
+
|
|
37
|
+
# Like `Bridge::TypeError`, but for spec-mandated `RangeError`s (e.g. the
|
|
38
|
+
# `Response` constructor rejecting a status outside 200–599). A host bridge
|
|
39
|
+
# rethrows a real JS `RangeError`; kept distinct from Ruby's `::RangeError`.
|
|
40
|
+
class RangeError < ::StandardError; end
|
|
41
|
+
end
|
|
42
|
+
end
|
data/lib/dommy/bridge/methods.rb
CHANGED
|
@@ -34,24 +34,31 @@ module Dommy
|
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
def js_methods(names)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
# StyleDeclaration define `method_missing`, so `super` would fall
|
|
43
|
-
# through to it and return a CSS-property String instead of raising.
|
|
44
|
-
parent =
|
|
45
|
-
if superclass.method_defined?(:__js_method_names__)
|
|
46
|
-
superclass.instance_method(:__js_method_names__)
|
|
47
|
-
end
|
|
37
|
+
@own_js_methods = names.map(&:to_s).freeze
|
|
38
|
+
@js_method_names = nil # a declaration after a first read must win
|
|
39
|
+
const_set(:JS_METHOD_NAMES, @own_js_methods) unless const_defined?(:JS_METHOD_NAMES, false)
|
|
40
|
+
@own_js_methods
|
|
41
|
+
end
|
|
48
42
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
43
|
+
# The class's names plus its ancestors', ancestors first. Composed on
|
|
44
|
+
# the CLASS, which is where the answer belongs: a per-instance method
|
|
45
|
+
# could not use `super` to reach the ancestor's copy, because a class
|
|
46
|
+
# like StyleDeclaration answers `method_missing` for any name and would
|
|
47
|
+
# hand back a CSS property string instead. Walking `superclass` has no
|
|
48
|
+
# such problem.
|
|
49
|
+
def js_method_names
|
|
50
|
+
@js_method_names ||= begin
|
|
51
|
+
inherited_names = superclass.respond_to?(:js_method_names) ? superclass.js_method_names : []
|
|
52
|
+
(inherited_names + own_js_methods).uniq.freeze
|
|
52
53
|
end
|
|
53
54
|
end
|
|
55
|
+
|
|
56
|
+
def own_js_methods = @own_js_methods || []
|
|
54
57
|
end
|
|
58
|
+
|
|
59
|
+
# The bridge ABI reads this per interface to decide which property names
|
|
60
|
+
# are callable functions.
|
|
61
|
+
def __js_method_names__ = self.class.js_method_names
|
|
55
62
|
end
|
|
56
63
|
end
|
|
57
64
|
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
module Bridge
|
|
5
|
+
# The three values that are not values: what the protocol says when a
|
|
6
|
+
# property is missing, undefined, or not ours to handle. Every host has to
|
|
7
|
+
# tell these apart, which is why they are Bridge's.
|
|
8
|
+
|
|
9
|
+
# Sentinel returned by `__js_set__` when a key is not a known DOM property
|
|
10
|
+
# (so the JS host can keep it as a JS-side expando, preserving identity)
|
|
11
|
+
# rather than silently dropping it.
|
|
12
|
+
UNHANDLED = :__js_unhandled__
|
|
13
|
+
|
|
14
|
+
# Sentinel for the JS `undefined` value, used in both directions:
|
|
15
|
+
# - a `__js_call__` returns it for a void (undefined-returning) op, so the
|
|
16
|
+
# host marshals JS `undefined` rather than the `null` a bare Ruby `nil`
|
|
17
|
+
# would (e.g. DOMTokenList add/remove return undefined);
|
|
18
|
+
# - a top-level JS `undefined` *argument* arrives as it (whereas JS `null`
|
|
19
|
+
# arrives as `nil`), so WebIDL-style dispatch can tell an omitted optional
|
|
20
|
+
# argument from an explicit null.
|
|
21
|
+
# Its `to_s` is "undefined" so a DOMString coercion of a stray undefined is
|
|
22
|
+
# still spec-faithful.
|
|
23
|
+
UNDEFINED = Object.new
|
|
24
|
+
def UNDEFINED.to_s = "undefined"
|
|
25
|
+
def UNDEFINED.inspect = "#<Dommy::Bridge::UNDEFINED>"
|
|
26
|
+
UNDEFINED.freeze
|
|
27
|
+
|
|
28
|
+
# The sentinel a `__js_get__` returns for a GENUINELY-ABSENT property (a key
|
|
29
|
+
# the object does not have), as distinct from a present property whose value
|
|
30
|
+
# is `nil`/JS null or `UNDEFINED`/JS undefined. It marshals to JS `undefined`
|
|
31
|
+
# for the value, but the proxy reports `("x" in obj) === false` for it — so
|
|
32
|
+
# feature detection like `isUndefined(window.Vue)` AND `"Vue" in window` are
|
|
33
|
+
# both correct. (UNDEFINED means present-but-undefined → reported present.)
|
|
34
|
+
ABSENT = Object.new
|
|
35
|
+
def ABSENT.to_s = "undefined"
|
|
36
|
+
def ABSENT.inspect = "#<Dommy::Bridge::ABSENT>"
|
|
37
|
+
ABSENT.freeze
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
module Bridge
|
|
5
|
+
# The Ruby shapes a JS value takes while it is on this side: an opaque
|
|
6
|
+
# reference, and the two byte buffers that cross as a typed array or a bare
|
|
7
|
+
# ArrayBuffer.
|
|
8
|
+
|
|
9
|
+
# An opaque handle to a JS-side value that Ruby only stores and hands back
|
|
10
|
+
# (an AbortSignal's reason, a CustomEvent's detail). A non-plain JS object
|
|
11
|
+
# (Error, class instance, …) crosses as one of these instead of being
|
|
12
|
+
# flattened to a Hash, so it round-trips with IDENTITY preserved. `to_s`
|
|
13
|
+
# exposes the captured JS string form for the rare Ruby consumer that needs
|
|
14
|
+
# text (e.g. building a message).
|
|
15
|
+
class JSValue
|
|
16
|
+
attr_reader :ref
|
|
17
|
+
|
|
18
|
+
# `stack_frames` are the value's JS stack, when the tag that crossed
|
|
19
|
+
# carried one. Ruby cannot reach through a ref to read `.stack`, so an
|
|
20
|
+
# error-shaped value brings its frames with it (see host_runtime.js
|
|
21
|
+
# tagValue) and reporting uses them for the source position.
|
|
22
|
+
attr_reader :stack_frames
|
|
23
|
+
|
|
24
|
+
# The value's JS constructor name, when the tag carried one.
|
|
25
|
+
attr_reader :js_name
|
|
26
|
+
|
|
27
|
+
def initialize(ref, label = nil, stack_frames = nil, js_name = nil)
|
|
28
|
+
@ref = ref
|
|
29
|
+
@label = label
|
|
30
|
+
@stack_frames = stack_frames
|
|
31
|
+
@js_name = js_name
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def to_s = (@label || "[object]").to_s
|
|
35
|
+
def inspect = "#<Dommy::Bridge::JSValue #{to_s}>"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# A byte buffer that crosses the JS boundary as a `Uint8Array` (rather than a
|
|
39
|
+
# plain Array). Wrap a host method's byte-array result in this so JS sees a
|
|
40
|
+
# real typed array — e.g. `TextEncoder#encode`, `Blob#arrayBuffer`. The
|
|
41
|
+
# reverse direction (a JS ArrayBuffer/TypedArray argument) arrives as a
|
|
42
|
+
# `Bytes` too. It subclasses Array so plain-Array callers (and `== [..]`
|
|
43
|
+
# comparisons) keep working; only the bridge treats it specially.
|
|
44
|
+
class Bytes < ::Array
|
|
45
|
+
def initialize(bytes = [])
|
|
46
|
+
super()
|
|
47
|
+
concat(coerce(bytes))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
alias bytes to_a
|
|
51
|
+
def pack_bytes = pack("C*")
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
# A String is the packed form — what #pack_bytes produces — so it unpacks
|
|
56
|
+
# rather than going through Array(), which would wrap it and take `to_i`
|
|
57
|
+
# of the whole thing: `Bytes.new("abc")` used to be one zero byte, with no
|
|
58
|
+
# complaint, which is the shape of a round trip gone wrong.
|
|
59
|
+
def coerce(bytes)
|
|
60
|
+
return bytes.b.unpack("C*") if bytes.is_a?(::String)
|
|
61
|
+
|
|
62
|
+
Array(bytes).map do |byte|
|
|
63
|
+
raise ArgumentError, "not a byte: #{byte.inspect}" unless byte.respond_to?(:to_int)
|
|
64
|
+
|
|
65
|
+
byte.to_int & 0xFF
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Like `Bytes`, but crosses the JS boundary as a bare `ArrayBuffer` rather
|
|
71
|
+
# than a `Uint8Array` view. Use this for the spec methods whose return type
|
|
72
|
+
# is `ArrayBuffer` — `Response`/`Blob`/`FileReader`/`XMLHttpRequest`'s
|
|
73
|
+
# `arrayBuffer`. Subclasses `Bytes` so Ruby-level `== [..]` comparisons still
|
|
74
|
+
# hold; only the bridge distinguishes it (and must check it before `Bytes`).
|
|
75
|
+
class ArrayBuffer < Bytes
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Dommy
|
|
4
|
-
module
|
|
5
|
-
# The wire protocol
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# of constants prevents the two sides from drifting apart.
|
|
4
|
+
module Bridge
|
|
5
|
+
# The wire protocol: the tagged-Hash shapes that cross the boundary (a
|
|
6
|
+
# handle, a callback, an opaque JS ref, a byte buffer, a tagged exception,
|
|
7
|
+
# …). Every marshaller builds and matches these keys — Js::Marshaller for
|
|
8
|
+
# the Proxy bridge, WasmBridge#pack/#unpack for the wasm guest — so one set
|
|
9
|
+
# of constants keeps them from drifting apart.
|
|
11
10
|
#
|
|
12
|
-
#
|
|
13
|
-
# the
|
|
14
|
-
#
|
|
15
|
-
#
|
|
11
|
+
# It is Bridge's rather than Js's for exactly that reason: a tag is true of
|
|
12
|
+
# any host, not of the one we ship. A backend gem reads these to speak the
|
|
13
|
+
# protocol without depending on our engine.
|
|
14
|
+
#
|
|
15
|
+
# The JS half (host_runtime.js: dehydrate/rehydrate/wasmTag/wasmDeref)
|
|
16
|
+
# mirrors the SAME string literals. When changing a tag here, update
|
|
17
|
+
# host_runtime.js in lockstep — these constants are the canonical names; the
|
|
18
|
+
# JS literals are the mirror.
|
|
16
19
|
module WireTags
|
|
17
20
|
# A bridged Ruby object, referenced by its HandleTable id (becomes an ES
|
|
18
21
|
# Proxy on the JS side).
|
|
@@ -33,6 +36,14 @@ module Dommy
|
|
|
33
36
|
JS_REF = "__rb_js_ref"
|
|
34
37
|
# A human-readable label captured alongside a JS ref (for #to_s/#inspect).
|
|
35
38
|
JS_LABEL = "__rb_js_label"
|
|
39
|
+
# The thrown value's JS stack, carried alongside a callback's throw so the
|
|
40
|
+
# host can report WHERE the page failed (see ErrorEvent's filename /
|
|
41
|
+
# lineno / colno). A JS value crosses as an opaque ref, so Ruby cannot
|
|
42
|
+
# read `.stack` off it afterwards; it has to come over with the throw.
|
|
43
|
+
JS_STACK = "__rb_js_stack"
|
|
44
|
+
# The value's JS constructor name ("TypeError"), so a host log can say what
|
|
45
|
+
# kind of thing it holds rather than only naming the Ruby wrapper.
|
|
46
|
+
JS_NAME = "__rb_js_name"
|
|
36
47
|
# Marks a JS ref that implements the EventListener interface (handleEvent).
|
|
37
48
|
HANDLE_EVENT = "__rb_handle_event"
|
|
38
49
|
# Marks a JS ref that implements the NodeFilter interface (acceptNode).
|