dommy-js-quickjs 0.1.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.
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dommy
4
+ module Js
5
+ # Resolves a JS constructor by interface name for reverse construction
6
+ # (`new Event(...)`, `new DOMException(...)`). The window object is the
7
+ # source for most constructors — it exposes them via __js_get__ — while a
8
+ # few not on the window are provided directly. Engine-agnostic.
9
+ class ConstructorRegistry
10
+ # The window whose __js_get__ exposes Event/CustomEvent/MouseEvent/… .
11
+ attr_writer :source
12
+
13
+ def initialize
14
+ @source = nil
15
+ end
16
+
17
+ # An object responding to __js_new__ for `name`, or nil if `name` isn't
18
+ # constructable (the bridge then makes the JS side throw).
19
+ def resolve(name)
20
+ if @source.respond_to?(:__js_get__)
21
+ ctor = @source.__js_get__(name)
22
+ return ctor if ctor.respond_to?(:__js_new__)
23
+ end
24
+ extra(name)
25
+ end
26
+
27
+ private
28
+
29
+ # Constructors the window doesn't expose.
30
+ def extra(name)
31
+ case name
32
+ when "DOMException"
33
+ return unless defined?(Dommy::DOMException)
34
+
35
+ Dommy::Bridge::Constructor.new { |args| Dommy::DOMException.new(args[0], args[1] || "Error") }
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dommy
4
+ module Js
5
+ # Bridges JS-defined custom elements to Dommy's custom element pipeline.
6
+ # `customElements.define(name, JSClass)` on the JS side calls in here, which
7
+ # registers a Dommy::HTMLElement subclass for `name` whose lifecycle
8
+ # reactions (connected/disconnected/adopted/attributeChanged) route back to
9
+ # the JS instance through the bridge. The JS class's constructor itself runs
10
+ # on the JS side via the construction-stack upgrade in host_runtime.js.
11
+ class CustomElements
12
+ attr_writer :window
13
+
14
+ def initialize(bridge)
15
+ @bridge = bridge
16
+ @window = nil
17
+ end
18
+
19
+ def define(name, observed)
20
+ return unless @window.respond_to?(:custom_elements)
21
+
22
+ @window.custom_elements.define(name, build_class(name, observed))
23
+ nil
24
+ end
25
+
26
+ # customElements.upgrade(root): delegate to Dommy's registry so a subtree
27
+ # attached without firing reactions gets its registered elements upgraded.
28
+ def upgrade(root)
29
+ return unless @window.respond_to?(:custom_elements)
30
+
31
+ @window.custom_elements.upgrade(root)
32
+ nil
33
+ end
34
+
35
+ private
36
+
37
+ # A Dommy custom element class that forwards each reaction to the JS
38
+ # instance. `__js_custom_element_name__` marks the node so the bridge tells
39
+ # the JS side to upgrade it on first crossing (see HostBridge interface info).
40
+ def build_class(name, observed)
41
+ bridge = @bridge
42
+ Class.new(Dommy::HTMLElement) do
43
+ define_singleton_method(:observed_attributes) { observed }
44
+ define_method(:__js_custom_element_name__) { name }
45
+ define_method(:connected_callback) { bridge.invoke_lifecycle(self, "connectedCallback", []) }
46
+ define_method(:disconnected_callback) { bridge.invoke_lifecycle(self, "disconnectedCallback", []) }
47
+ define_method(:adopted_callback) { bridge.invoke_lifecycle(self, "adoptedCallback", []) }
48
+ define_method(:attribute_changed_callback) do |attr, old_value, new_value|
49
+ bridge.invoke_lifecycle(self, "attributeChangedCallback", [attr, old_value, new_value])
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dommy
4
+ module Js
5
+ # Derives WebIDL interface metadata for a Dommy DOM object: the most-derived
6
+ # interface name and the single-inheritance chain up to the root
7
+ # (EventTarget for nodes). This mirrors the JS prototype chain the bridge
8
+ # builds so `instanceof` / Object.prototype.toString resolve correctly.
9
+ #
10
+ # Engine-agnostic, and the single home for DOM interface hierarchy knowledge:
11
+ # BASE_CHAINS (seeded eagerly on the JS side) must stay consistent with what
12
+ # #chain_for derives from real objects.
13
+ module DomInterfaces
14
+ # Dommy class basename -> WebIDL interface name, where they diverge.
15
+ # Anything not listed uses the class basename verbatim (HTMLDivElement, …).
16
+ NAME_OVERRIDES = {
17
+ "TextNode" => "Text",
18
+ "CommentNode" => "Comment",
19
+ "CharacterDataNode" => "CharacterData",
20
+ "Fragment" => "DocumentFragment",
21
+ "ClassList" => "DOMTokenList",
22
+ "DatasetMap" => "DOMStringMap",
23
+ "StyleDeclaration" => "CSSStyleDeclaration",
24
+ "LiveNodeList" => "NodeList",
25
+ "StandaloneEventTarget" => "EventTarget"
26
+ }.freeze
27
+
28
+ # Concrete HTML element interfaces. A browser exposes every one of these as
29
+ # a global constructor whether or not an instance exists, so a framework's
30
+ # bare `instanceof HTMLInputElement` feature check resolves regardless of
31
+ # page content (idiomorph, Turbo's morph engine, probes `instanceof
32
+ # HTMLInputElement`/`HTMLTextAreaElement` during focus restoration even when
33
+ # the page has no such element). Each is a direct HTMLElement subclass
34
+ # except the two media leaves, appended with their chains below. Mirrors the
35
+ # `class HTMLxxxElement < HTMLElement` set in the dommy gem's html_elements.
36
+ HTML_LEAF_INTERFACES = %w[
37
+ HTMLAnchorElement HTMLAreaElement HTMLBaseElement HTMLBodyElement
38
+ HTMLBRElement HTMLButtonElement HTMLDataElement HTMLDetailsElement
39
+ HTMLDialogElement HTMLDivElement HTMLEmbedElement HTMLFieldsetElement
40
+ HTMLFormElement HTMLHeadElement HTMLHeadingElement HTMLHRElement
41
+ HTMLHtmlElement HTMLIFrameElement HTMLImageElement HTMLInputElement
42
+ HTMLLabelElement HTMLLegendElement HTMLLIElement HTMLLinkElement
43
+ HTMLMapElement HTMLMetaElement HTMLMeterElement HTMLModElement
44
+ HTMLObjectElement HTMLOListElement HTMLOptGroupElement HTMLOptionElement
45
+ HTMLOutputElement HTMLParagraphElement HTMLPictureElement HTMLPreElement
46
+ HTMLProgressElement HTMLQuoteElement HTMLScriptElement HTMLSelectElement
47
+ HTMLSlotElement HTMLSourceElement HTMLSpanElement HTMLStyleElement
48
+ HTMLTableCaptionElement HTMLTableCellElement HTMLTableElement
49
+ HTMLTableRowElement HTMLTableSectionElement HTMLTemplateElement
50
+ HTMLTextAreaElement HTMLTimeElement HTMLTitleElement HTMLTrackElement
51
+ HTMLUListElement
52
+ ].freeze
53
+
54
+ # Base interface chains seeded eagerly on the JS side so `instanceof Node`
55
+ # / `typeof HTMLElement` resolve before an instance of that exact type has
56
+ # crossed. Concrete leaves (HTMLButtonElement, …) are built lazily from
57
+ # #chain_for when an instance crosses. Keep consistent with #chain_for.
58
+ BASE_CHAINS = [
59
+ %w[Node EventTarget],
60
+ %w[Element Node EventTarget],
61
+ %w[HTMLElement Element Node EventTarget],
62
+ %w[SVGElement Element Node EventTarget],
63
+ %w[CharacterData Node EventTarget],
64
+ %w[Text CharacterData Node EventTarget],
65
+ %w[Comment CharacterData Node EventTarget],
66
+ %w[Document Node EventTarget],
67
+ %w[DocumentFragment Node EventTarget],
68
+ %w[DocumentType Node EventTarget],
69
+ %w[Attr Node EventTarget],
70
+ %w[Event],
71
+ %w[CustomEvent Event],
72
+ %w[MessageEvent Event],
73
+ %w[PopStateEvent Event],
74
+ %w[CloseEvent Event],
75
+ %w[MouseEvent Event],
76
+ %w[KeyboardEvent Event],
77
+ %w[DOMException],
78
+ # Window-exposed constructors that frameworks call bare (new X(...)).
79
+ # Seeding them creates the global; construction routes to the window.
80
+ %w[MutationObserver], %w[IntersectionObserver], %w[ResizeObserver],
81
+ %w[PerformanceObserver], %w[AbortController], %w[AbortSignal EventTarget],
82
+ %w[FormData], %w[URL], %w[URLSearchParams], %w[Headers], %w[Request], %w[Response],
83
+ %w[Blob], %w[File], %w[FileList], %w[FileReader], %w[XMLHttpRequest],
84
+ %w[TextEncoder], %w[TextDecoder], %w[DOMParser], %w[XMLSerializer],
85
+ %w[MessageChannel], %w[BroadcastChannel], %w[WebSocket], %w[EventSource],
86
+ %w[Notification], %w[Worker], %w[DataTransfer],
87
+ %w[ReadableStream], %w[WritableStream], %w[TransformStream],
88
+ %w[Range],
89
+ # Collection interfaces, seeded so `result instanceof NodeList` /
90
+ # `instanceof HTMLCollection` resolve (querySelectorAll, children, …).
91
+ %w[NodeList], %w[HTMLCollection],
92
+ # Traversal: NodeFilter exposes only [Constant]s (NodeFilter.SHOW_ELEMENT,
93
+ # .FILTER_ACCEPT, …); TreeWalker/NodeIterator are instances.
94
+ %w[NodeFilter], %w[TreeWalker], %w[NodeIterator],
95
+ # Concrete HTML element interfaces (see HTML_LEAF_INTERFACES) + the media
96
+ # subtree, so bare `instanceof HTMLInputElement` always resolves.
97
+ *HTML_LEAF_INTERFACES.map { |n| [n, "HTMLElement", "Element", "Node", "EventTarget"] },
98
+ %w[HTMLMediaElement HTMLElement Element Node EventTarget],
99
+ %w[HTMLAudioElement HTMLMediaElement HTMLElement Element Node EventTarget],
100
+ %w[HTMLVideoElement HTMLMediaElement HTMLElement Element Node EventTarget]
101
+ ].freeze
102
+
103
+ module_function
104
+
105
+ # { "name" => most-derived interface, "chain" => [...] } for a host object.
106
+ def info(obj)
107
+ chain = chain_for(obj)
108
+ {"name" => chain.first, "chain" => chain}
109
+ end
110
+
111
+ # Walk the Dommy class superclass chain (HTMLDivElement < HTMLElement <
112
+ # Element), then append the module-provided base interfaces (Node ->
113
+ # EventTarget) for nodes, since Dommy models Node as a mixin rather than a
114
+ # superclass. Stops at the first foreign superclass (Object, or
115
+ # StandardError for DOMException) so non-DOM ancestors stay out.
116
+ def chain_for(obj)
117
+ names = []
118
+ klass = obj.class
119
+ while klass && klass.name&.start_with?("Dommy::")
120
+ name = name_for(klass)
121
+ names << name if name && !names.include?(name)
122
+ klass = klass.superclass
123
+ end
124
+ if defined?(Dommy::Node) && obj.is_a?(Dommy::Node)
125
+ names << "Node" unless names.include?("Node")
126
+ names << "EventTarget" unless names.include?("EventTarget")
127
+ end
128
+ names
129
+ end
130
+
131
+ def name_for(klass)
132
+ base = klass.name&.split("::")&.last
133
+ return nil unless base
134
+
135
+ NAME_OVERRIDES.fetch(base, base)
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dommy
4
+ module Js
5
+ # Maps JS-facing integer handles to live Ruby objects. Engine-agnostic.
6
+ #
7
+ # Handles are monotonic — never reused — so a handle can never refer to two
8
+ # different objects over the table's lifetime. That invariant is what makes
9
+ # GC-driven #release race-free: a finalizer releasing handle N can't clobber
10
+ # a different object that happened to get the same id.
11
+ #
12
+ # The same Ruby object reuses its handle while still registered (keyed by
13
+ # object_id), so it maps to a single JS proxy (stable identity). A registry
14
+ # entry also keeps the object reachable while JS holds a proxy for it.
15
+ class HandleTable
16
+ def initialize
17
+ @by_handle = {} # handle (Integer) -> object
18
+ @handle_by_oid = {} # object_id -> handle (for identity reuse)
19
+ @next_handle = 0
20
+ end
21
+
22
+ def register(obj)
23
+ oid = obj.object_id
24
+ existing = @handle_by_oid[oid]
25
+ return existing if existing && @by_handle[existing].equal?(obj)
26
+
27
+ handle = (@next_handle += 1)
28
+ @by_handle[handle] = obj
29
+ @handle_by_oid[oid] = handle
30
+ handle
31
+ end
32
+
33
+ def fetch(handle)
34
+ @by_handle.fetch(handle.to_i)
35
+ end
36
+
37
+ # Forget a handle (called when its JS proxy is garbage-collected). Only
38
+ # trims the mapping; the object itself lives on via its other references.
39
+ def release(handle)
40
+ obj = @by_handle.delete(handle.to_i)
41
+ return unless obj
42
+
43
+ oid = obj.object_id
44
+ @handle_by_oid.delete(oid) if @handle_by_oid[oid] == handle.to_i
45
+ end
46
+
47
+ def size
48
+ @by_handle.size
49
+ end
50
+ end
51
+ end
52
+ end