dommy 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +30 -38
- data/lib/dommy/animation.rb +10 -2
- data/lib/dommy/attr.rb +197 -32
- data/lib/dommy/backend/nokogiri_adapter.rb +127 -0
- data/lib/dommy/backend/nokolexbor_adapter.rb +117 -0
- data/lib/dommy/backend.rb +175 -0
- data/lib/dommy/blob.rb +30 -11
- data/lib/dommy/bridge/constructor_registry.rb +28 -0
- data/lib/dommy/bridge/methods.rb +57 -0
- data/lib/dommy/bridge.rb +97 -0
- data/lib/dommy/callable_invoker.rb +36 -0
- data/lib/dommy/compression_streams.rb +4 -4
- data/lib/dommy/cookie_store.rb +4 -2
- data/lib/dommy/crypto.rb +16 -9
- data/lib/dommy/css.rb +53 -7
- data/lib/dommy/custom_elements.rb +33 -9
- data/lib/dommy/data_transfer.rb +4 -0
- data/lib/dommy/document.rb +693 -60
- data/lib/dommy/dom_parser.rb +29 -15
- data/lib/dommy/element.rb +1147 -438
- data/lib/dommy/event.rb +279 -79
- data/lib/dommy/event_source.rb +14 -10
- data/lib/dommy/fetch.rb +509 -39
- data/lib/dommy/file_reader.rb +14 -6
- data/lib/dommy/form_data.rb +3 -3
- data/lib/dommy/history.rb +46 -8
- data/lib/dommy/html_collection.rb +59 -6
- data/lib/dommy/html_elements.rb +153 -1502
- data/lib/dommy/internal/css_pseudo_handlers.rb +137 -0
- data/lib/dommy/internal/dom_matching.rb +3 -3
- data/lib/dommy/internal/global_functions.rb +26 -0
- data/lib/dommy/internal/idna.rb +16 -7
- data/lib/dommy/internal/ipv4_parser.rb +22 -7
- data/lib/dommy/internal/mutation_coordinator.rb +11 -2
- data/lib/dommy/internal/namespaces.rb +70 -0
- data/lib/dommy/internal/node_equality.rb +86 -0
- data/lib/dommy/internal/node_traversal.rb +1 -1
- data/lib/dommy/internal/node_wrapper_cache.rb +77 -31
- data/lib/dommy/internal/observable_callback.rb +1 -5
- data/lib/dommy/internal/parent_node.rb +126 -0
- data/lib/dommy/internal/reflected_attributes.rb +103 -13
- data/lib/dommy/internal/selector_parser.rb +664 -0
- data/lib/dommy/internal/template_content_registry.rb +6 -6
- data/lib/dommy/internal/url_parser.rb +677 -0
- data/lib/dommy/intersection_observer.rb +4 -2
- data/lib/dommy/location.rb +10 -4
- data/lib/dommy/media_query_list.rb +10 -4
- data/lib/dommy/message_channel.rb +41 -11
- data/lib/dommy/mutation_observer.rb +76 -23
- data/lib/dommy/navigator.rb +38 -24
- data/lib/dommy/node.rb +158 -16
- data/lib/dommy/notification.rb +6 -4
- data/lib/dommy/parser.rb +13 -13
- data/lib/dommy/performance.rb +4 -0
- data/lib/dommy/performance_observer.rb +4 -2
- data/lib/dommy/promise.rb +14 -14
- data/lib/dommy/range.rb +74 -5
- data/lib/dommy/resize_observer.rb +4 -2
- data/lib/dommy/scheduler.rb +34 -13
- data/lib/dommy/shadow_root.rb +31 -60
- data/lib/dommy/storage.rb +2 -0
- data/lib/dommy/streams.rb +40 -49
- data/lib/dommy/svg_elements.rb +204 -3606
- data/lib/dommy/text_codec.rb +178 -25
- data/lib/dommy/tree_walker.rb +270 -81
- data/lib/dommy/url.rb +305 -450
- data/lib/dommy/url_pattern.rb +2 -0
- data/lib/dommy/version.rb +1 -1
- data/lib/dommy/web_socket.rb +49 -19
- data/lib/dommy/window.rb +205 -203
- data/lib/dommy/worker.rb +12 -12
- data/lib/dommy/xml_http_request.rb +32 -7
- data/lib/dommy.rb +19 -2
- metadata +22 -27
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "nokolexbor"
|
|
4
|
+
|
|
5
|
+
module Dommy
|
|
6
|
+
module Backend
|
|
7
|
+
# Nokolexbor (Lexbor-based) backend. 3-7× faster CSS selectors,
|
|
8
|
+
# 6× faster HTML5 parsing. HTML-only — XML namespaces are not
|
|
9
|
+
# tracked, so SVG detection falls back to ancestor walking.
|
|
10
|
+
module Nokolexbor
|
|
11
|
+
# Class references for `is_a?` / type-checking.
|
|
12
|
+
Element = ::Nokolexbor::Element
|
|
13
|
+
Document = ::Nokolexbor::Document
|
|
14
|
+
Text = ::Nokolexbor::Text
|
|
15
|
+
Comment = ::Nokolexbor::Comment
|
|
16
|
+
DocumentFragment = ::Nokolexbor::DocumentFragment
|
|
17
|
+
Node = ::Nokolexbor::Node
|
|
18
|
+
|
|
19
|
+
# Fake "namespace" wrapper returned for nodes inside <svg> subtrees.
|
|
20
|
+
# Provides the same `href` API that Nokogiri's Namespace object has,
|
|
21
|
+
# so calling code can treat them uniformly.
|
|
22
|
+
SvgNamespace = Struct.new(:href) do
|
|
23
|
+
def initialize
|
|
24
|
+
super("http://www.w3.org/2000/svg")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
SVG_NAMESPACE = SvgNamespace.new.freeze
|
|
29
|
+
|
|
30
|
+
module_function
|
|
31
|
+
|
|
32
|
+
def parse(html)
|
|
33
|
+
::Nokolexbor.parse(html.to_s)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def fragment(html, owner_doc:)
|
|
37
|
+
::Nokolexbor::DocumentFragment.parse(html.to_s)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def create_element(name, doc)
|
|
41
|
+
::Nokolexbor::Node.new(name, doc)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def create_text(content, doc)
|
|
45
|
+
::Nokolexbor::Text.new(content, doc)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def create_comment(content, doc)
|
|
49
|
+
# Nokolexbor's argument order is (content, doc).
|
|
50
|
+
::Nokolexbor::Comment.new(content, doc)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Nokolexbor doesn't track XML namespaces. We synthesize one for
|
|
54
|
+
# SVG by walking ancestors — necessary so `element_class_for`
|
|
55
|
+
# routes SVG tags to their specialized classes.
|
|
56
|
+
def namespace_of(node)
|
|
57
|
+
return nil unless node.respond_to?(:name)
|
|
58
|
+
|
|
59
|
+
in_svg_subtree?(node) ? SVG_NAMESPACE : nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def add_namespace_definition(_node, _prefix, _href)
|
|
63
|
+
# No-op: Nokolexbor doesn't support XML namespaces.
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# ----- Namespaced attributes (degraded) -----
|
|
67
|
+
# Nokolexbor has no namespace model, so *AttributeNS collapses to the
|
|
68
|
+
# qualified name in the null namespace. Fine for HTML (all attributes are
|
|
69
|
+
# null-namespace); foreign-content (SVG/MathML) fidelity is lost.
|
|
70
|
+
|
|
71
|
+
def get_attribute_ns(node, _namespace, local_name)
|
|
72
|
+
node[local_name.to_s]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def has_attribute_ns?(node, _namespace, local_name)
|
|
76
|
+
node.key?(local_name.to_s)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def set_attribute_ns(node, _namespace, _prefix, _local_name, qualified_name, value)
|
|
80
|
+
node[qualified_name] = value.to_s
|
|
81
|
+
value.to_s
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def remove_attribute_ns(node, _namespace, local_name)
|
|
85
|
+
node.remove_attribute(local_name.to_s) if node.key?(local_name.to_s)
|
|
86
|
+
nil
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def attribute_ns_info(attr_node)
|
|
90
|
+
{
|
|
91
|
+
namespace_uri: nil,
|
|
92
|
+
prefix: nil,
|
|
93
|
+
local_name: attr_node.name,
|
|
94
|
+
qualified_name: attr_node.name,
|
|
95
|
+
value: attr_node.value,
|
|
96
|
+
}
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def attribute_nodes(node)
|
|
100
|
+
node.attribute_nodes
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Internal helper — visible to allow testing.
|
|
104
|
+
def in_svg_subtree?(node)
|
|
105
|
+
return true if node.name.to_s.downcase == "svg"
|
|
106
|
+
|
|
107
|
+
current = node.parent
|
|
108
|
+
while current
|
|
109
|
+
return true if current.respond_to?(:name) && current.name.to_s.downcase == "svg"
|
|
110
|
+
current = current.respond_to?(:parent) ? current.parent : nil
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
false
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
# `Dommy::Backend` — pluggable HTML parser abstraction. Lets Dommy
|
|
5
|
+
# work with either Nokogiri (mature, full namespace support) or
|
|
6
|
+
# Nokolexbor (faster, HTML5-only). Internally, all DOM library
|
|
7
|
+
# code goes through this facade rather than referencing the parser
|
|
8
|
+
# directly.
|
|
9
|
+
#
|
|
10
|
+
# Defaults to Nokogiri if available, else Nokolexbor.
|
|
11
|
+
#
|
|
12
|
+
# Switching backends:
|
|
13
|
+
#
|
|
14
|
+
# require "dommy"
|
|
15
|
+
# Dommy::Backend.use(:nokolexbor)
|
|
16
|
+
#
|
|
17
|
+
# Or set directly:
|
|
18
|
+
#
|
|
19
|
+
# Dommy::Backend.current = Dommy::Backend::Nokolexbor
|
|
20
|
+
#
|
|
21
|
+
# All adapters must implement the same interface — see
|
|
22
|
+
# `Backend::Nokogiri` for the canonical reference.
|
|
23
|
+
module Backend
|
|
24
|
+
class BackendNotAvailable < StandardError
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class << self
|
|
28
|
+
def current
|
|
29
|
+
@current ||= detect_default
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
attr_writer :current
|
|
33
|
+
|
|
34
|
+
def use(name)
|
|
35
|
+
@current = case name.to_sym
|
|
36
|
+
when :nokogiri
|
|
37
|
+
require_relative "backend/nokogiri_adapter"
|
|
38
|
+
Nokogiri
|
|
39
|
+
when :nokolexbor
|
|
40
|
+
require_relative "backend/nokolexbor_adapter"
|
|
41
|
+
Nokolexbor
|
|
42
|
+
else
|
|
43
|
+
raise ArgumentError, "Unknown backend: #{name.inspect}. Use :nokogiri or :nokolexbor."
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Delegate calls so internal code can use `Backend.parse(...)`.
|
|
48
|
+
def parse(html)
|
|
49
|
+
current.parse(html)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Parse XML input into an XML document. Backends without a real XML parser
|
|
53
|
+
# (HTML-only, e.g. Nokolexbor) fall back to the HTML parser.
|
|
54
|
+
def parse_xml(xml)
|
|
55
|
+
current.respond_to?(:parse_xml) ? current.parse_xml(xml) : current.parse(xml)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def fragment(html, owner_doc:)
|
|
59
|
+
current.fragment(html, owner_doc: owner_doc)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def create_element(name, doc)
|
|
63
|
+
current.create_element(name, doc)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def create_text(content, doc)
|
|
67
|
+
current.create_text(content, doc)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def create_comment(content, doc)
|
|
71
|
+
current.create_comment(content, doc)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# CDATA section node (XML documents). Backends without CDATA fall back to a
|
|
75
|
+
# text node.
|
|
76
|
+
def create_cdata(content, doc)
|
|
77
|
+
current.respond_to?(:create_cdata) ? current.create_cdata(content, doc) : current.create_text(content, doc)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def cdata_class
|
|
81
|
+
current.respond_to?(:cdata_class) ? current.cdata_class : nil
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def namespace_of(node)
|
|
85
|
+
current.namespace_of(node)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def add_namespace_definition(node, prefix, href)
|
|
89
|
+
current.add_namespace_definition(node, prefix, href)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Namespaced attribute access (DOM *AttributeNS). `namespace` is an href
|
|
93
|
+
# String or nil. Nokolexbor degrades to qualified-name (null-namespace).
|
|
94
|
+
def get_attribute_ns(node, namespace, local_name)
|
|
95
|
+
current.get_attribute_ns(node, namespace, local_name)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def set_attribute_ns(node, namespace, prefix, local_name, qualified_name, value)
|
|
99
|
+
current.set_attribute_ns(node, namespace, prefix, local_name, qualified_name, value)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def remove_attribute_ns(node, namespace, local_name)
|
|
103
|
+
current.remove_attribute_ns(node, namespace, local_name)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def has_attribute_ns?(node, namespace, local_name)
|
|
107
|
+
current.has_attribute_ns?(node, namespace, local_name)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Reads a backend attribute node into {namespace_uri:, prefix:,
|
|
111
|
+
# local_name:, qualified_name:, value:} (namespace-aware).
|
|
112
|
+
def attribute_ns_info(attr_node)
|
|
113
|
+
current.attribute_ns_info(attr_node)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# The element's attribute nodes (each readable via attribute_ns_info).
|
|
117
|
+
# The single choke point so DOM code doesn't touch parser internals.
|
|
118
|
+
def attribute_nodes(node)
|
|
119
|
+
current.attribute_nodes(node)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Type constants — proxy through to the current backend so
|
|
123
|
+
# `node.is_a?(Backend::Element)` resolves dynamically.
|
|
124
|
+
def element_class
|
|
125
|
+
current::Element
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def document_class
|
|
129
|
+
current::Document
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def text_class
|
|
133
|
+
current::Text
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def comment_class
|
|
137
|
+
current::Comment
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def document_fragment_class
|
|
141
|
+
current::DocumentFragment
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def node_class
|
|
145
|
+
current::Node
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
private
|
|
149
|
+
|
|
150
|
+
def detect_default
|
|
151
|
+
try_nokogiri ||
|
|
152
|
+
try_nokolexbor ||
|
|
153
|
+
raise(BackendNotAvailable, "Dommy requires either 'nokogiri' or 'nokolexbor' gem to be installed.")
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def try_nokogiri
|
|
157
|
+
require "nokogiri"
|
|
158
|
+
|
|
159
|
+
require_relative "backend/nokogiri_adapter"
|
|
160
|
+
Nokogiri
|
|
161
|
+
rescue LoadError
|
|
162
|
+
nil
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def try_nokolexbor
|
|
166
|
+
require "nokolexbor"
|
|
167
|
+
|
|
168
|
+
require_relative "backend/nokolexbor_adapter"
|
|
169
|
+
Nokolexbor
|
|
170
|
+
rescue LoadError
|
|
171
|
+
nil
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
data/lib/dommy/blob.rb
CHANGED
|
@@ -17,12 +17,16 @@ module Dommy
|
|
|
17
17
|
# - anything else: coerced via to_s
|
|
18
18
|
#
|
|
19
19
|
# `options["type"]` sets the MIME type (lowercased per spec).
|
|
20
|
-
|
|
20
|
+
# `window` (optional) lets the JS-facing `text()`/`arrayBuffer()` return real
|
|
21
|
+
# Promises (they need a scheduler). A window-less Blob falls back to a
|
|
22
|
+
# synchronous result, which `await` still handles.
|
|
23
|
+
def initialize(parts = [], options = {}, window = nil)
|
|
21
24
|
parts = [parts] unless parts.is_a?(Array)
|
|
22
25
|
@data = collect_bytes(parts)
|
|
23
26
|
@size = @data.bytesize
|
|
24
27
|
raw_type = options["type"] || options[:type] || ""
|
|
25
28
|
@type = raw_type.to_s.downcase
|
|
29
|
+
@window = window
|
|
26
30
|
end
|
|
27
31
|
|
|
28
32
|
# Return a new Blob over a byte range of this one.
|
|
@@ -31,7 +35,7 @@ module Dommy
|
|
|
31
35
|
s = clamp_index(start.to_i, @size)
|
|
32
36
|
e = clamp_index(last.to_i, @size)
|
|
33
37
|
e = s if e < s
|
|
34
|
-
Blob.new([@data.byteslice(s, e - s) || ""], "type" => content_type.to_s)
|
|
38
|
+
Blob.new([@data.byteslice(s, e - s) || ""], {"type" => content_type.to_s}, @window)
|
|
35
39
|
end
|
|
36
40
|
|
|
37
41
|
# Read the bytes as UTF-8 text. The DOM spec returns a Promise,
|
|
@@ -40,15 +44,16 @@ module Dommy
|
|
|
40
44
|
@data.dup.force_encoding(Encoding::UTF_8)
|
|
41
45
|
end
|
|
42
46
|
|
|
43
|
-
# Read the bytes as
|
|
44
|
-
#
|
|
47
|
+
# Read the bytes as a real ArrayBuffer (the spec return type, wrapped so it
|
|
48
|
+
# crosses the JS boundary as a bare ArrayBuffer rather than an Array/typed
|
|
49
|
+
# array). The DOM spec returns a Promise<ArrayBuffer>; Dommy is synchronous.
|
|
45
50
|
def array_buffer
|
|
46
|
-
@data.bytes
|
|
51
|
+
Bridge::ArrayBuffer.new(@data.bytes)
|
|
47
52
|
end
|
|
48
53
|
|
|
49
54
|
# Raw binary bytes (Ruby ASCII-8BIT string). Used by FormData /
|
|
50
55
|
# fetch when serializing multipart bodies.
|
|
51
|
-
def
|
|
56
|
+
def __dommy_bytes__
|
|
52
57
|
@data
|
|
53
58
|
end
|
|
54
59
|
|
|
@@ -61,25 +66,37 @@ module Dommy
|
|
|
61
66
|
end
|
|
62
67
|
end
|
|
63
68
|
|
|
69
|
+
# Methods routed through __js_call__ (keep in sync with its when-arms).
|
|
70
|
+
# File < Blob inherits these (it adds only properties).
|
|
71
|
+
include Bridge::Methods
|
|
72
|
+
js_methods %w[slice text arrayBuffer]
|
|
64
73
|
def __js_call__(method, args)
|
|
65
74
|
case method
|
|
66
75
|
when "slice"
|
|
67
76
|
slice(args[0] || 0, args[1] || @size, args[2] || "")
|
|
68
77
|
when "text"
|
|
69
|
-
text
|
|
78
|
+
# WHATWG: Blob.text() returns a Promise<string>.
|
|
79
|
+
promise_or_value(text)
|
|
70
80
|
when "arrayBuffer"
|
|
71
|
-
|
|
81
|
+
# WHATWG: Blob.arrayBuffer() returns a Promise<ArrayBuffer>.
|
|
82
|
+
promise_or_value(array_buffer)
|
|
72
83
|
end
|
|
73
84
|
end
|
|
74
85
|
|
|
75
86
|
private
|
|
76
87
|
|
|
88
|
+
# Wrap a consumed value in a resolved Promise when a window is available;
|
|
89
|
+
# otherwise return it directly (a window-less Blob — `await` copes either way).
|
|
90
|
+
def promise_or_value(value)
|
|
91
|
+
@window ? PromiseValue.resolve(@window, value) : value
|
|
92
|
+
end
|
|
93
|
+
|
|
77
94
|
def collect_bytes(parts)
|
|
78
95
|
buf = String.new(encoding: Encoding::ASCII_8BIT)
|
|
79
96
|
parts.each do |part|
|
|
80
97
|
case part
|
|
81
98
|
when Blob
|
|
82
|
-
buf << part.
|
|
99
|
+
buf << part.__dommy_bytes__
|
|
83
100
|
when String
|
|
84
101
|
buf << part.dup.force_encoding(Encoding::ASCII_8BIT)
|
|
85
102
|
when Array
|
|
@@ -106,8 +123,8 @@ module Dommy
|
|
|
106
123
|
class File < Blob
|
|
107
124
|
attr_reader :name, :last_modified
|
|
108
125
|
|
|
109
|
-
def initialize(parts, name, options = {})
|
|
110
|
-
super(parts, options)
|
|
126
|
+
def initialize(parts, name, options = {}, window = nil)
|
|
127
|
+
super(parts, options, window)
|
|
111
128
|
@name = name.to_s
|
|
112
129
|
raw_lm = options["lastModified"] || options[:lastModified]
|
|
113
130
|
@last_modified = (raw_lm || (Time.now.to_f * 1000)).to_i
|
|
@@ -172,6 +189,8 @@ module Dommy
|
|
|
172
189
|
end
|
|
173
190
|
end
|
|
174
191
|
|
|
192
|
+
include Bridge::Methods
|
|
193
|
+
js_methods %w[item]
|
|
175
194
|
def __js_call__(method, args)
|
|
176
195
|
case method
|
|
177
196
|
when "item"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
module Bridge
|
|
5
|
+
# Maps JS global constructor names (e.g. "Event", "URL", "XMLHttpRequest")
|
|
6
|
+
# to their `Bridge::Constructor` instances. Window builds one and routes
|
|
7
|
+
# `__js_get__` name lookups through it, instead of carrying one ivar plus
|
|
8
|
+
# one `when` arm per constructor.
|
|
9
|
+
class ConstructorRegistry
|
|
10
|
+
def initialize(map)
|
|
11
|
+
@map = map.freeze
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def [](name)
|
|
15
|
+
@map[name]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def key?(name)
|
|
19
|
+
@map.key?(name)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# The set of constructor names, e.g. for host-side enumeration / tests.
|
|
23
|
+
def names
|
|
24
|
+
@map.keys
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
module Bridge
|
|
5
|
+
# Declares, in one line, the set of JS-callable method names a bridge class
|
|
6
|
+
# routes through `__js_call__` (as opposed to data properties read via
|
|
7
|
+
# `__js_get__`). The QuickJS host reads `__js_method_names__` once per
|
|
8
|
+
# interface to decide which property names to expose as callable functions.
|
|
9
|
+
#
|
|
10
|
+
# class Blob
|
|
11
|
+
# include Bridge::Methods
|
|
12
|
+
# js_methods %w[slice text arrayBuffer]
|
|
13
|
+
# def __js_call__(method, args) = ...
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# Subclasses compose automatically: a subclass's own `js_methods` are merged
|
|
17
|
+
# with its ancestors' (ancestors first), so `__js_call__ ... else super`
|
|
18
|
+
# chains stay in sync with the exposed names without a manual `super + own`.
|
|
19
|
+
#
|
|
20
|
+
# The per-class `JS_METHOD_NAMES` constant holds the class's OWN names; the
|
|
21
|
+
# suite asserts it matches the class's own `__js_call__` `when` arms — see
|
|
22
|
+
# test/test_js_call_dispatch_invariant.rb.
|
|
23
|
+
module Methods
|
|
24
|
+
def self.included(base)
|
|
25
|
+
base.extend(ClassMethods)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
module ClassMethods
|
|
29
|
+
# `extend` is per-singleton, so a subclass of an includer would not
|
|
30
|
+
# inherit `js_methods`. Re-extend each subclass as it is defined.
|
|
31
|
+
def inherited(subclass)
|
|
32
|
+
super
|
|
33
|
+
subclass.extend(ClassMethods)
|
|
34
|
+
end
|
|
35
|
+
|
|
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
|
|
48
|
+
|
|
49
|
+
define_method(:__js_method_names__) do
|
|
50
|
+
base = parent ? parent.bind(self).call : []
|
|
51
|
+
(base + own).uniq.freeze
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
data/lib/dommy/bridge.rb
CHANGED
|
@@ -18,6 +18,97 @@ module Dommy
|
|
|
18
18
|
# args (Array)
|
|
19
19
|
# - `__js_new__(args)` invokes the value as a JS constructor
|
|
20
20
|
module Bridge
|
|
21
|
+
# Sentinel returned by `__js_set__` when a key is not a known DOM property
|
|
22
|
+
# (so the JS host can keep it as a JS-side expando, preserving identity)
|
|
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
|
+
# An opaque handle to a JS-side value that Ruby only stores and hands back
|
|
41
|
+
# (an AbortSignal's reason, a CustomEvent's detail). A non-plain JS object
|
|
42
|
+
# (Error, class instance, …) crosses as one of these instead of being
|
|
43
|
+
# flattened to a Hash, so it round-trips with IDENTITY preserved. `to_s`
|
|
44
|
+
# exposes the captured JS string form for the rare Ruby consumer that needs
|
|
45
|
+
# text (e.g. building a message).
|
|
46
|
+
class JSValue
|
|
47
|
+
attr_reader :ref
|
|
48
|
+
|
|
49
|
+
def initialize(ref, label = nil)
|
|
50
|
+
@ref = ref
|
|
51
|
+
@label = label
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def to_s = (@label || "[object]").to_s
|
|
55
|
+
def inspect = "#<Dommy::Bridge::JSValue #{to_s}>"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Raised by a host method that must throw an ARBITRARY value back to JS —
|
|
59
|
+
# not a DOMException/Error, but e.g. `signal.throwIfAborted()` throwing the
|
|
60
|
+
# exact abort reason (a string, number, or opaque JSValue). The bridge
|
|
61
|
+
# re-throws the wrapped value verbatim (identity preserved). Subclasses
|
|
62
|
+
# RuntimeError (with the value's string form as the message) so standalone
|
|
63
|
+
# CRuby callers still see a normal `raise`-able error.
|
|
64
|
+
class ThrowValue < RuntimeError
|
|
65
|
+
attr_reader :value
|
|
66
|
+
|
|
67
|
+
def initialize(value)
|
|
68
|
+
@value = value
|
|
69
|
+
super(value.to_s)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# A byte buffer that crosses the JS boundary as a `Uint8Array` (rather than a
|
|
74
|
+
# plain Array). Wrap a host method's byte-array result in this so JS sees a
|
|
75
|
+
# real typed array — e.g. `TextEncoder#encode`, `Blob#arrayBuffer`. The
|
|
76
|
+
# reverse direction (a JS ArrayBuffer/TypedArray argument) arrives as a
|
|
77
|
+
# `Bytes` too. It subclasses Array so plain-Array callers (and `== [..]`
|
|
78
|
+
# comparisons) keep working; only the bridge treats it specially.
|
|
79
|
+
class Bytes < ::Array
|
|
80
|
+
def initialize(bytes = [])
|
|
81
|
+
super()
|
|
82
|
+
concat(Array(bytes).map { |b| b.to_i & 0xFF })
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
alias bytes to_a
|
|
86
|
+
def pack_bytes = pack("C*")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Like `Bytes`, but crosses the JS boundary as a bare `ArrayBuffer` rather
|
|
90
|
+
# than a `Uint8Array` view. Use this for the spec methods whose return type
|
|
91
|
+
# is `ArrayBuffer` — `Response`/`Blob`/`FileReader`/`XMLHttpRequest`'s
|
|
92
|
+
# `arrayBuffer`. Subclasses `Bytes` so Ruby-level `== [..]` comparisons still
|
|
93
|
+
# hold; only the bridge distinguishes it (and must check it before `Bytes`).
|
|
94
|
+
class ArrayBuffer < Bytes
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# A Ruby-side signal that the JS boundary should surface a JS `TypeError`
|
|
98
|
+
# (not a `DOMException`). Some WebIDL operations — notably the `URL`
|
|
99
|
+
# constructor and its `href` setter — throw `TypeError` on failure rather
|
|
100
|
+
# than a DOMException; raising this lets a host bridge rethrow the correct
|
|
101
|
+
# JS error type, while Ruby callers can still rescue it like any other
|
|
102
|
+
# error. Kept distinct from Ruby's built-in `::TypeError` so a host can map
|
|
103
|
+
# only deliberate, spec-mandated TypeErrors (and not mask genuine Ruby type
|
|
104
|
+
# bugs) across the boundary.
|
|
105
|
+
class TypeError < ::StandardError; end
|
|
106
|
+
|
|
107
|
+
# Like `Bridge::TypeError`, but for spec-mandated `RangeError`s (e.g. the
|
|
108
|
+
# `Response` constructor rejecting a status outside 200–599). A host bridge
|
|
109
|
+
# rethrows a real JS `RangeError`; kept distinct from Ruby's `::RangeError`.
|
|
110
|
+
class RangeError < ::StandardError; end
|
|
111
|
+
|
|
21
112
|
# Wraps an external callback handle (registered in a host-side
|
|
22
113
|
# callback table) so the JS bridge can resolve / invoke it. The
|
|
23
114
|
# external host that creates these is responsible for honoring
|
|
@@ -83,6 +174,12 @@ module Dommy
|
|
|
83
174
|
handler = @class_methods[method.to_s]
|
|
84
175
|
handler&.call(args)
|
|
85
176
|
end
|
|
177
|
+
|
|
178
|
+
# Names of the registered class-level (static) methods, so a JS host can
|
|
179
|
+
# expose them on the constructor function (e.g. `URL.createObjectURL`).
|
|
180
|
+
def __js_class_method_names__
|
|
181
|
+
@class_methods.keys
|
|
182
|
+
end
|
|
86
183
|
end
|
|
87
184
|
|
|
88
185
|
# `JS.global[:Promise]` view. Implements the `resolve` / `reject`
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
# Invokes a callback that may be a JS-bridged object (responds to `__js_call__`)
|
|
5
|
+
# or a plain Ruby callable (responds to `call`). Centralizes the dispatch used
|
|
6
|
+
# by promises, the scheduler, and streams so the JS/Ruby fork lives in one place.
|
|
7
|
+
module CallableInvoker
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
# Invoke `callback` with `args`. A JS-bridged callable goes through
|
|
11
|
+
# `__js_call__("call", args)`; a Ruby callable through `call(*args)`. A nil
|
|
12
|
+
# or non-callable callback is a no-op (returns nil).
|
|
13
|
+
def invoke(callback, *args)
|
|
14
|
+
return if callback.nil?
|
|
15
|
+
|
|
16
|
+
if callback.respond_to?(:__js_call__)
|
|
17
|
+
callback.__js_call__("call", args)
|
|
18
|
+
elsif callback.respond_to?(:call)
|
|
19
|
+
callback.call(*args)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Invoke a DOM event listener per the EventTarget rule: an object with
|
|
24
|
+
# `handle_event`, else a Ruby callable, else a JS-bridged callable (tried in
|
|
25
|
+
# that order).
|
|
26
|
+
def invoke_listener(listener, event)
|
|
27
|
+
if listener.respond_to?(:handle_event)
|
|
28
|
+
listener.handle_event(event)
|
|
29
|
+
elsif listener.respond_to?(:call) && !listener.is_a?(Module)
|
|
30
|
+
listener.call(event)
|
|
31
|
+
elsif listener.respond_to?(:__js_call__)
|
|
32
|
+
listener.__js_call__("call", [event])
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -31,9 +31,9 @@ module Dommy
|
|
|
31
31
|
"close" => proc do
|
|
32
32
|
compressed = compressor.call(@buffer)
|
|
33
33
|
controller.enqueue(compressed)
|
|
34
|
-
@readable.
|
|
34
|
+
@readable.__internal_close__
|
|
35
35
|
end,
|
|
36
|
-
"abort" => proc { |r| @readable.
|
|
36
|
+
"abort" => proc { |r| @readable.__internal_error__(r) }
|
|
37
37
|
}
|
|
38
38
|
)
|
|
39
39
|
end
|
|
@@ -98,9 +98,9 @@ module Dommy
|
|
|
98
98
|
"close" => proc do
|
|
99
99
|
plain = decompressor.call(@buffer)
|
|
100
100
|
controller.enqueue(plain)
|
|
101
|
-
@readable.
|
|
101
|
+
@readable.__internal_close__
|
|
102
102
|
end,
|
|
103
|
-
"abort" => proc { |r| @readable.
|
|
103
|
+
"abort" => proc { |r| @readable.__internal_error__(r) }
|
|
104
104
|
}
|
|
105
105
|
)
|
|
106
106
|
end
|
data/lib/dommy/cookie_store.rb
CHANGED
|
@@ -60,6 +60,8 @@ module Dommy
|
|
|
60
60
|
PromiseValue.resolve(@window, nil)
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
+
include Bridge::Methods
|
|
64
|
+
js_methods %w[get getAll set delete addEventListener removeEventListener dispatchEvent]
|
|
63
65
|
def __js_call__(method, args)
|
|
64
66
|
case method
|
|
65
67
|
when "get"
|
|
@@ -73,13 +75,13 @@ module Dommy
|
|
|
73
75
|
when "addEventListener"
|
|
74
76
|
add_event_listener(args[0], args[1], args[2])
|
|
75
77
|
when "removeEventListener"
|
|
76
|
-
remove_event_listener(args[0], args[1])
|
|
78
|
+
remove_event_listener(args[0], args[1], args[2])
|
|
77
79
|
when "dispatchEvent"
|
|
78
80
|
dispatch_event(args[0])
|
|
79
81
|
end
|
|
80
82
|
end
|
|
81
83
|
|
|
82
|
-
def
|
|
84
|
+
def __internal_event_parent__
|
|
83
85
|
nil
|
|
84
86
|
end
|
|
85
87
|
|