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.
Files changed (155) hide show
  1. checksums.yaml +4 -4
  2. data/lib/dommy/backend/makiri_adapter.rb +63 -29
  3. data/lib/dommy/backend.rb +32 -9
  4. data/lib/dommy/bridge/constructor.rb +72 -0
  5. data/lib/dommy/bridge/errors.rb +42 -0
  6. data/lib/dommy/bridge/methods.rb +21 -14
  7. data/lib/dommy/bridge/sentinels.rb +39 -0
  8. data/lib/dommy/bridge/values.rb +78 -0
  9. data/lib/dommy/{js → bridge}/wire_tags.rb +14 -11
  10. data/lib/dommy/bridge.rb +22 -250
  11. data/lib/dommy/browser.rb +112 -11
  12. data/lib/dommy/character_data.rb +539 -0
  13. data/lib/dommy/class_list.rb +311 -0
  14. data/lib/dommy/crypto.rb +12 -1
  15. data/lib/dommy/css.rb +12 -0
  16. data/lib/dommy/data_transfer.rb +106 -0
  17. data/lib/dommy/document.rb +307 -369
  18. data/lib/dommy/dom_parser.rb +3 -0
  19. data/lib/dommy/element.rb +199 -1817
  20. data/lib/dommy/encodings/tables.rb +4 -4
  21. data/lib/dommy/encodings.rb +58 -27
  22. data/lib/dommy/event.rb +227 -1
  23. data/lib/dommy/event_source.rb +41 -0
  24. data/lib/dommy/form_data.rb +6 -64
  25. data/lib/dommy/form_entry_list.rb +227 -0
  26. data/lib/dommy/fragment.rb +320 -0
  27. data/lib/dommy/history.rb +3 -0
  28. data/lib/dommy/html_collection.rb +18 -8
  29. data/lib/dommy/html_elements/base.rb +338 -0
  30. data/lib/dommy/html_elements/embedded.rb +237 -0
  31. data/lib/dommy/html_elements/forms/form.rb +1088 -0
  32. data/lib/dommy/html_elements/forms/input.rb +642 -0
  33. data/lib/dommy/html_elements/forms/select.rb +536 -0
  34. data/lib/dommy/html_elements/forms.rb +8 -0
  35. data/lib/dommy/html_elements/interactive.rb +382 -0
  36. data/lib/dommy/html_elements/media.rb +329 -0
  37. data/lib/dommy/html_elements/metadata.rb +353 -0
  38. data/lib/dommy/html_elements/miscellaneous.rb +77 -0
  39. data/lib/dommy/html_elements/tables.rb +468 -0
  40. data/lib/dommy/html_elements.rb +11 -5322
  41. data/lib/dommy/hyperlink_utils.rb +171 -0
  42. data/lib/dommy/interaction/debug.rb +7 -6
  43. data/lib/dommy/interaction/dom_summary.rb +65 -40
  44. data/lib/dommy/interaction/driver.rb +29 -115
  45. data/lib/dommy/interaction/event_synthesis.rb +97 -4
  46. data/lib/dommy/interaction/field_interactor.rb +1 -13
  47. data/lib/dommy/interaction/form_submission.rb +89 -167
  48. data/lib/dommy/interaction/key_sender.rb +121 -0
  49. data/lib/dommy/interaction/mime_types.rb +25 -0
  50. data/lib/dommy/interaction.rb +2 -0
  51. data/lib/dommy/internal/accessibility_tree.rb +32 -9
  52. data/lib/dommy/internal/accessibility_visibility.rb +44 -0
  53. data/lib/dommy/internal/accessible_name.rb +85 -65
  54. data/lib/dommy/internal/aria_role.rb +21 -6
  55. data/lib/dommy/internal/aria_snapshot.rb +5 -0
  56. data/lib/dommy/internal/aria_state.rb +24 -5
  57. data/lib/dommy/internal/autofill.rb +141 -0
  58. data/lib/dommy/internal/backend_prefilter.rb +170 -0
  59. data/lib/dommy/internal/bounded_cache.rb +50 -0
  60. data/lib/dommy/internal/child_node.rb +9 -34
  61. data/lib/dommy/internal/css/calc.rb +143 -145
  62. data/lib/dommy/internal/css/cascade.rb +38 -393
  63. data/lib/dommy/internal/css/cascaded_declarations.rb +245 -0
  64. data/lib/dommy/internal/css/color.rb +61 -34
  65. data/lib/dommy/internal/css/computed_style_builder.rb +205 -0
  66. data/lib/dommy/internal/css/computed_style_declaration.rb +20 -4
  67. data/lib/dommy/internal/css/counters.rb +60 -37
  68. data/lib/dommy/internal/css/custom_properties.rb +87 -39
  69. data/lib/dommy/internal/css/media_query.rb +228 -203
  70. data/lib/dommy/internal/css/parser.rb +29 -12
  71. data/lib/dommy/internal/css/property_registry.rb +32 -56
  72. data/lib/dommy/internal/css/renderability.rb +61 -0
  73. data/lib/dommy/internal/css/rule_index.rb +17 -140
  74. data/lib/dommy/internal/css/selector_dependencies.rb +162 -0
  75. data/lib/dommy/internal/css/style_cache.rb +61 -0
  76. data/lib/dommy/internal/css/ua_stylesheet.rb +40 -1
  77. data/lib/dommy/internal/css/value_tokens.rb +60 -0
  78. data/lib/dommy/internal/directionality.rb +188 -0
  79. data/lib/dommy/internal/document_generations.rb +169 -0
  80. data/lib/dommy/internal/document_interaction_state.rb +47 -0
  81. data/lib/dommy/internal/document_live_ranges.rb +97 -0
  82. data/lib/dommy/internal/dom_matching.rb +28 -12
  83. data/lib/dommy/internal/element_aria.rb +163 -0
  84. data/lib/dommy/internal/element_geometry.rb +69 -0
  85. data/lib/dommy/internal/element_matching.rb +5 -12
  86. data/lib/dommy/internal/element_shadow.rb +117 -0
  87. data/lib/dommy/internal/element_state.rb +232 -0
  88. data/lib/dommy/internal/element_tasks.rb +19 -0
  89. data/lib/dommy/internal/element_top_layer.rb +63 -0
  90. data/lib/dommy/internal/enumerated_keyword_sets.rb +53 -0
  91. data/lib/dommy/internal/exception_report.rb +1 -1
  92. data/lib/dommy/internal/input_type.rb +280 -0
  93. data/lib/dommy/internal/insertion_point.rb +62 -0
  94. data/lib/dommy/internal/mime_type.rb +74 -0
  95. data/lib/dommy/internal/mutation_coordinator.rb +57 -198
  96. data/lib/dommy/internal/namespaces.rb +41 -43
  97. data/lib/dommy/internal/node_adopter.rb +84 -77
  98. data/lib/dommy/internal/node_factory.rb +157 -0
  99. data/lib/dommy/internal/node_identity.rb +31 -0
  100. data/lib/dommy/internal/node_wrapper_cache.rb +61 -207
  101. data/lib/dommy/internal/observer_options.rb +93 -0
  102. data/lib/dommy/internal/parent_node.rb +27 -27
  103. data/lib/dommy/internal/post_insertion_steps.rb +213 -0
  104. data/lib/dommy/internal/reflected_attributes.rb +425 -13
  105. data/lib/dommy/internal/rendered_text/collector.rb +221 -0
  106. data/lib/dommy/internal/rendered_text/computed_box.rb +39 -0
  107. data/lib/dommy/internal/rendered_text/fragment.rb +55 -0
  108. data/lib/dommy/internal/rendered_text/items.rb +57 -0
  109. data/lib/dommy/internal/rendered_text/whitespace_collapser.rb +132 -0
  110. data/lib/dommy/internal/selector_arguments.rb +94 -0
  111. data/lib/dommy/internal/selector_index.rb +3 -1
  112. data/lib/dommy/internal/selector_matcher.rb +23 -383
  113. data/lib/dommy/internal/selector_parser.rb +2 -37
  114. data/lib/dommy/internal/template_content_registry.rb +112 -66
  115. data/lib/dommy/internal/text_flattening.rb +20 -0
  116. data/lib/dommy/internal/text_selection.rb +104 -0
  117. data/lib/dommy/internal/toggle_events.rb +56 -0
  118. data/lib/dommy/internal/toggle_task_tracker.rb +57 -0
  119. data/lib/dommy/internal/url_parser.rb +722 -569
  120. data/lib/dommy/internal/url_record_accessors.rb +90 -0
  121. data/lib/dommy/internal/window_constructors.rb +190 -0
  122. data/lib/dommy/internal/xml_serialization.rb +168 -80
  123. data/lib/dommy/js/crossing_profile.rb +75 -0
  124. data/lib/dommy/js/dom_interfaces.rb +22 -2
  125. data/lib/dommy/js/host_bridge.rb +43 -187
  126. data/lib/dommy/js/host_callbacks.rb +96 -0
  127. data/lib/dommy/js/host_runtime.js +840 -763
  128. data/lib/dommy/js/import_map.rb +10 -1
  129. data/lib/dommy/js/marshaller.rb +94 -65
  130. data/lib/dommy/js/observable_runtime.js +125 -146
  131. data/lib/dommy/js/runtime.rb +10 -43
  132. data/lib/dommy/js/runtime_registry.rb +46 -0
  133. data/lib/dommy/js/script_boot.rb +38 -73
  134. data/lib/dommy/js/webidl_tables.js +619 -0
  135. data/lib/dommy/js/window_wiring.rb +69 -0
  136. data/lib/dommy/location.rb +152 -103
  137. data/lib/dommy/mutation_observer.rb +3 -57
  138. data/lib/dommy/navigation.rb +65 -12
  139. data/lib/dommy/node.rb +46 -20
  140. data/lib/dommy/promise.rb +58 -4
  141. data/lib/dommy/resources.rb +19 -1
  142. data/lib/dommy/shadow_root.rb +11 -5
  143. data/lib/dommy/style_declaration.rb +284 -0
  144. data/lib/dommy/svg_elements.rb +1 -45
  145. data/lib/dommy/url.rb +20 -69
  146. data/lib/dommy/url_pattern/regexp_translator.rb +1 -6
  147. data/lib/dommy/url_pattern/tokenizer.rb +4 -2
  148. data/lib/dommy/validity_state.rb +383 -0
  149. data/lib/dommy/version.rb +1 -1
  150. data/lib/dommy/window.rb +30 -166
  151. data/lib/dommy/xml_http_request.rb +35 -4
  152. data/lib/dommy.rb +23 -3
  153. metadata +74 -7
  154. data/lib/dommy/bridge/constructor_registry.rb +0 -28
  155. 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: 64a22d9ba17102a03881baedbda66b5236b8c4499140d779e4c4acba86583241
4
- data.tar.gz: e111a114c5a7ae9485a3e99af339e70a59dce6b9a83a837639ac6564873a8d4e
3
+ metadata.gz: d623f452622acb0d99a40af340aefc6dfc86a4c466277097132e0a3dcff5b741
4
+ data.tar.gz: 38ea700cb1a482696ca8253b5bbf8901c5f34f564ed760c57dae1e0d0abf9f92
5
5
  SHA512:
6
- metadata.gz: 67c651bd499d2cd0d6cf1fb862b11a9f0f6b68ba1814abe40732b990e5b66b9b5f0494fd33cb989e4b7a5f64faa3a8a152e8979231f4c082f32187999188030a
7
- data.tar.gz: e7e8ee9194687b9fe6be28646e9f80d32631f3cf78e5b9fefbab5fd913b9a1d6116ab819bae3a54030f2fefc6e4fa8e478c8ab2667cc086e009c5ebb1a7c9481
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) && doc.respond_to?(:create_loose_dom_element)
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
- # Makiri doesn't track XML namespaces. We synthesize one for SVG by
235
- # walking ancestors — necessary so `element_class_for` routes SVG
236
- # tags to their specialized classes.
237
- # The element's namespace, from Lexbor's own namespace tracking (HTML /
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"). The element is still valid — its prefix and namespace
271
- # live on the wrapper — so the declaration is simply not written.
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 template_content_nodes(node)
283
- cf = node.respond_to?(:content_fragment) ? node.content_fragment : nil
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
- node.set_attribute_ns(presence(namespace), qualified_name.to_s, value.to_s)
303
- value.to_s
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 content child nodes of a `<template>` element. HTML5 parsers model
197
- # template contents differently — Lexbor/Makiri in a separate content fragment — so reading them goes
198
- # through the backend. Used by the template-content registry's migration.
199
- def template_content_nodes(node)
200
- current.template_content_nodes(node)
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
@@ -34,24 +34,31 @@ module Dommy
34
34
  end
35
35
 
36
36
  def js_methods(names)
37
- own = names.map(&:to_s).freeze
38
- const_set(:JS_METHOD_NAMES, own) unless const_defined?(:JS_METHOD_NAMES, false)
39
-
40
- # Capture the ancestor's __js_method_names__ as a real method (if any)
41
- # at definition time. We can't use `super` here: classes like
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
- define_method(:__js_method_names__) do
50
- base = parent ? parent.bind(self).call : []
51
- (base + own).uniq.freeze
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 Js
5
- # The wire protocol shared by every Ruby<->JS marshaller in this gem: the
6
- # tagged-Hash shapes that cross the boundary (a handle, a callback, an opaque
7
- # JS ref, a byte buffer, a tagged exception, …). Both Ruby marshallers —
8
- # HostBridge#wrap/#unwrap (the Proxy bridge) and WasmBridge#pack/#unpack (the
9
- # wasm guest bridge) — build and match these keys, so keeping them as one set
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
- # The JS half (host_runtime.js: dehydrate/rehydrate/wasmTag/wasmDeref) mirrors
13
- # the SAME string literals. When changing a tag here, update host_runtime.js
14
- # in lockstep — these constants are the canonical names; the JS literals are
15
- # the mirror.
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).