dommy 0.8.0 → 0.9.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 (115) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -3
  3. data/lib/dommy/animation.rb +4 -0
  4. data/lib/dommy/attr.rb +11 -5
  5. data/lib/dommy/backend/makiri_adapter.rb +330 -0
  6. data/lib/dommy/backend.rb +114 -33
  7. data/lib/dommy/blob.rb +2 -0
  8. data/lib/dommy/bridge.rb +11 -0
  9. data/lib/dommy/browser.rb +217 -0
  10. data/lib/dommy/compression_streams.rb +4 -0
  11. data/lib/dommy/crypto.rb +4 -0
  12. data/lib/dommy/css.rb +487 -50
  13. data/lib/dommy/custom_elements.rb +2 -2
  14. data/lib/dommy/data_transfer.rb +2 -0
  15. data/lib/dommy/data_uri.rb +35 -0
  16. data/lib/dommy/deferred_response.rb +59 -0
  17. data/lib/dommy/document.rb +386 -228
  18. data/lib/dommy/dom_exception.rb +2 -0
  19. data/lib/dommy/dom_parser.rb +7 -17
  20. data/lib/dommy/element.rb +502 -155
  21. data/lib/dommy/event.rb +240 -9
  22. data/lib/dommy/fetch.rb +152 -34
  23. data/lib/dommy/form_data.rb +2 -0
  24. data/lib/dommy/history.rb +2 -0
  25. data/lib/dommy/html_canvas_element.rb +230 -0
  26. data/lib/dommy/html_collection.rb +5 -6
  27. data/lib/dommy/html_elements.rb +304 -27
  28. data/lib/dommy/interaction/debug.rb +35 -0
  29. data/lib/dommy/interaction/dom_summary.rb +131 -0
  30. data/lib/dommy/interaction/driver.rb +244 -0
  31. data/lib/dommy/interaction/event_synthesis.rb +56 -0
  32. data/lib/dommy/interaction/field_interactor.rb +117 -0
  33. data/lib/dommy/interaction/form_submission.rb +268 -0
  34. data/lib/dommy/interaction/locator.rb +158 -0
  35. data/lib/dommy/interaction/role_query.rb +58 -0
  36. data/lib/dommy/interaction.rb +32 -0
  37. data/lib/dommy/internal/accessibility_tree.rb +215 -0
  38. data/lib/dommy/internal/accessible_description.rb +38 -0
  39. data/lib/dommy/internal/accessible_name.rb +301 -0
  40. data/lib/dommy/internal/aria_role.rb +252 -0
  41. data/lib/dommy/internal/aria_snapshot.rb +64 -0
  42. data/lib/dommy/internal/aria_state.rb +151 -0
  43. data/lib/dommy/internal/css/calc.rb +242 -0
  44. data/lib/dommy/internal/css/cascade.rb +430 -0
  45. data/lib/dommy/internal/css/color.rb +381 -0
  46. data/lib/dommy/internal/css/computed_style_declaration.rb +130 -0
  47. data/lib/dommy/internal/css/counters.rb +227 -0
  48. data/lib/dommy/internal/css/custom_properties.rb +183 -0
  49. data/lib/dommy/internal/css/media_query.rb +302 -0
  50. data/lib/dommy/internal/css/parser.rb +265 -0
  51. data/lib/dommy/internal/css/property_registry.rb +512 -0
  52. data/lib/dommy/internal/css/rule_index.rb +494 -0
  53. data/lib/dommy/internal/css/supports.rb +158 -0
  54. data/lib/dommy/internal/css/ua_stylesheet.rb +53 -0
  55. data/lib/dommy/internal/css_pseudo_handlers.rb +283 -42
  56. data/lib/dommy/internal/css_rule_text.rb +160 -0
  57. data/lib/dommy/internal/dom_matching.rb +80 -9
  58. data/lib/dommy/internal/element_matching.rb +109 -0
  59. data/lib/dommy/internal/global_functions.rb +33 -0
  60. data/lib/dommy/internal/mutation_coordinator.rb +95 -4
  61. data/lib/dommy/internal/namespaces.rb +49 -5
  62. data/lib/dommy/internal/node_wrapper_cache.rb +163 -26
  63. data/lib/dommy/internal/parent_node.rb +82 -5
  64. data/lib/dommy/internal/selector_ast.rb +124 -0
  65. data/lib/dommy/internal/selector_index.rb +146 -0
  66. data/lib/dommy/internal/selector_matcher.rb +756 -0
  67. data/lib/dommy/internal/selector_parser.rb +283 -131
  68. data/lib/dommy/internal/shadow_root_registry.rb +9 -2
  69. data/lib/dommy/internal/template_content_registry.rb +26 -18
  70. data/lib/dommy/internal/xml_serialization.rb +344 -0
  71. data/lib/dommy/intersection_observer.rb +2 -0
  72. data/lib/dommy/js/bridge_conformance.rb +80 -0
  73. data/lib/dommy/js/constructor_resolver.rb +44 -0
  74. data/lib/dommy/js/custom_element_bridge.rb +90 -0
  75. data/lib/dommy/js/dom_interfaces.rb +162 -0
  76. data/lib/dommy/js/handle_table.rb +60 -0
  77. data/lib/dommy/js/host_bridge.rb +517 -0
  78. data/lib/dommy/js/host_runtime.js +1495 -0
  79. data/lib/dommy/js/import_map.rb +58 -0
  80. data/lib/dommy/js/marshaller.rb +240 -0
  81. data/lib/dommy/js/module_loader.rb +99 -0
  82. data/lib/dommy/js/observable_runtime.js +742 -0
  83. data/lib/dommy/js/runtime.rb +115 -0
  84. data/lib/dommy/js/script_boot.rb +221 -0
  85. data/lib/dommy/js/wire_tags.rb +62 -0
  86. data/lib/dommy/location.rb +2 -0
  87. data/lib/dommy/media_query_list.rb +50 -14
  88. data/lib/dommy/message_channel.rb +22 -6
  89. data/lib/dommy/minitest/assertions.rb +27 -0
  90. data/lib/dommy/mutation_observer.rb +89 -4
  91. data/lib/dommy/navigator.rb +34 -2
  92. data/lib/dommy/node.rb +24 -14
  93. data/lib/dommy/notification.rb +2 -0
  94. data/lib/dommy/parser.rb +1 -1
  95. data/lib/dommy/performance.rb +21 -1
  96. data/lib/dommy/promise.rb +94 -10
  97. data/lib/dommy/range.rb +173 -31
  98. data/lib/dommy/resources.rb +178 -0
  99. data/lib/dommy/rspec/capy_style_matchers.rb +126 -0
  100. data/lib/dommy/scheduler.rb +149 -13
  101. data/lib/dommy/screen.rb +91 -0
  102. data/lib/dommy/shadow_root.rb +76 -13
  103. data/lib/dommy/storage.rb +2 -1
  104. data/lib/dommy/streams.rb +6 -0
  105. data/lib/dommy/text_codec.rb +7 -1
  106. data/lib/dommy/tree_walker.rb +33 -10
  107. data/lib/dommy/url.rb +13 -1
  108. data/lib/dommy/version.rb +1 -1
  109. data/lib/dommy/window.rb +199 -11
  110. data/lib/dommy/worker.rb +8 -4
  111. data/lib/dommy/xml_http_request.rb +47 -6
  112. data/lib/dommy.rb +36 -1
  113. metadata +96 -10
  114. data/lib/dommy/backend/nokogiri_adapter.rb +0 -127
  115. data/lib/dommy/backend/nokolexbor_adapter.rb +0 -117
@@ -0,0 +1,90 @@
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
+ #
12
+ # Named distinctly from Dommy::CustomElementRegistry (the DOM
13
+ # window.customElements registry); this is the JS<->Dommy wiring, not the
14
+ # registry itself.
15
+ class CustomElementBridge
16
+ attr_writer :window
17
+
18
+ def initialize(bridge)
19
+ @bridge = bridge
20
+ @window = nil
21
+ end
22
+
23
+ def define(name, observed)
24
+ return unless @window.respond_to?(:custom_elements)
25
+
26
+ @window.custom_elements.define(name, build_class(name, observed))
27
+ nil
28
+ end
29
+
30
+ # customElements.upgrade(root): delegate to Dommy's registry so a subtree
31
+ # attached without firing reactions gets its registered elements upgraded.
32
+ def upgrade(root)
33
+ return unless @window.respond_to?(:custom_elements)
34
+
35
+ @window.custom_elements.upgrade(root)
36
+ nil
37
+ end
38
+
39
+ private
40
+
41
+ # A Dommy custom element class for `name` whose reactions forward to the JS
42
+ # instance. A fresh subclass per tag carries the bridge / name / observed
43
+ # set as class-level attributes; the reaction methods are defined once on
44
+ # the base (BridgedCustomElement), reading them via `self.class` — so no
45
+ # per-registration method definition is needed.
46
+ def build_class(name, observed)
47
+ klass = Class.new(BridgedCustomElement)
48
+ klass.js_bridge = @bridge
49
+ klass.js_name = name
50
+ klass.js_observed = observed
51
+ klass
52
+ end
53
+ end
54
+
55
+ # Base for a JS-defined custom element. Each registered tag is a subclass
56
+ # carrying its bridge / tag name / observed attributes; the lifecycle
57
+ # reactions (defined once here) forward to the JS instance through the
58
+ # bridge. `__js_custom_element_name__` marks the node so the bridge upgrades
59
+ # it on first crossing (see HostBridge interface info).
60
+ class BridgedCustomElement < Dommy::HTMLElement
61
+ class << self
62
+ attr_accessor :js_bridge, :js_name, :js_observed
63
+
64
+ def observed_attributes
65
+ js_observed
66
+ end
67
+ end
68
+
69
+ def __js_custom_element_name__
70
+ self.class.js_name
71
+ end
72
+
73
+ def connected_callback
74
+ self.class.js_bridge.invoke_lifecycle(self, "connectedCallback", [])
75
+ end
76
+
77
+ def disconnected_callback
78
+ self.class.js_bridge.invoke_lifecycle(self, "disconnectedCallback", [])
79
+ end
80
+
81
+ def adopted_callback
82
+ self.class.js_bridge.invoke_lifecycle(self, "adoptedCallback", [])
83
+ end
84
+
85
+ def attribute_changed_callback(attr, old_value, new_value)
86
+ self.class.js_bridge.invoke_lifecycle(self, "attributeChangedCallback", [attr, old_value, new_value])
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,162 @@
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 HTMLCanvasElement 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
+ # ShadowRoot is a DocumentFragment subclass; seeded so bare `node
69
+ # instanceof ShadowRoot` (Alpine.js walks the tree with this) resolves.
70
+ %w[ShadowRoot DocumentFragment Node EventTarget],
71
+ %w[DocumentType Node EventTarget],
72
+ %w[Attr Node EventTarget],
73
+ %w[Event],
74
+ %w[CustomEvent Event],
75
+ %w[MessageEvent Event],
76
+ %w[PopStateEvent Event],
77
+ %w[HashChangeEvent Event],
78
+ %w[CloseEvent Event],
79
+ %w[MouseEvent Event],
80
+ %w[KeyboardEvent Event],
81
+ %w[PromiseRejectionEvent Event],
82
+ %w[ErrorEvent Event],
83
+ %w[DOMException],
84
+ # Window-exposed constructors that frameworks call bare (new X(...)).
85
+ # Seeding them creates the global; construction routes to the window.
86
+ %w[MutationObserver], %w[IntersectionObserver], %w[ResizeObserver],
87
+ %w[PerformanceObserver], %w[AbortController], %w[AbortSignal EventTarget],
88
+ %w[FormData], %w[URL], %w[URLSearchParams], %w[Headers], %w[Request], %w[Response],
89
+ %w[Blob], %w[File], %w[FileList], %w[FileReader], %w[XMLHttpRequest],
90
+ %w[TextEncoder], %w[TextDecoder], %w[DOMParser], %w[XMLSerializer],
91
+ %w[MessageChannel], %w[BroadcastChannel], %w[WebSocket], %w[EventSource],
92
+ %w[Notification], %w[Worker], %w[DataTransfer],
93
+ %w[ReadableStream], %w[WritableStream], %w[TransformStream],
94
+ %w[Range],
95
+ # CSSOM stylesheet interfaces. Seeded so `style instanceof CSSStyleSheet`
96
+ # resolves: Lit's css-tag runs that check while deciding whether to use
97
+ # constructable stylesheets; with the interface present but
98
+ # `adoptedStyleSheets` unsupported it falls back to injecting a <style>
99
+ # element, which Dommy handles.
100
+ %w[CSSStyleSheet StyleSheet], %w[StyleSheet],
101
+ # Collection interfaces, seeded so `result instanceof NodeList` /
102
+ # `instanceof HTMLCollection` resolve (querySelectorAll, children, …).
103
+ %w[NodeList], %w[HTMLCollection],
104
+ # Traversal: NodeFilter exposes only [Constant]s (NodeFilter.SHOW_ELEMENT,
105
+ # .FILTER_ACCEPT, …); TreeWalker/NodeIterator are instances.
106
+ %w[NodeFilter], %w[TreeWalker], %w[NodeIterator],
107
+ # Concrete HTML element interfaces (see HTML_LEAF_INTERFACES) + the media
108
+ # subtree, so bare `instanceof HTMLInputElement` always resolves.
109
+ *HTML_LEAF_INTERFACES.map { |n| [n, "HTMLElement", "Element", "Node", "EventTarget"] },
110
+ # createElementNS with an unrecognized HTML-namespace local name yields an
111
+ # HTMLUnknownElement; seed it so a bare `instanceof HTMLUnknownElement`
112
+ # resolves even before such an element crosses.
113
+ %w[HTMLUnknownElement HTMLElement Element Node EventTarget],
114
+ %w[HTMLMediaElement HTMLElement Element Node EventTarget],
115
+ %w[HTMLAudioElement HTMLMediaElement HTMLElement Element Node EventTarget],
116
+ %w[HTMLVideoElement HTMLMediaElement HTMLElement Element Node EventTarget]
117
+ ].freeze
118
+
119
+ module_function
120
+
121
+ # { "name" => most-derived interface, "chain" => [...] } for a host object.
122
+ def info(obj)
123
+ chain = chain_for(obj)
124
+ {"name" => chain.first, "chain" => chain}
125
+ end
126
+
127
+ # Walk the Dommy class superclass chain (HTMLDivElement < HTMLElement <
128
+ # Element), then append the module-provided base interfaces (Node ->
129
+ # EventTarget) for nodes, since Dommy models Node as a mixin rather than a
130
+ # superclass. Stops at the first foreign superclass (Object, or
131
+ # StandardError for DOMException) so non-DOM ancestors stay out.
132
+ def chain_for(obj)
133
+ # WHATWG models DOMException as a single interface distinguished by its
134
+ # `name` property — there are no per-name subclasses. Collapse Dommy's
135
+ # convenience subclasses (AbortError < DOMException) to the one interface
136
+ # so a DOMException crossing as a value (e.g. `signal.reason`) reports
137
+ # `constructor === DOMException`, which assert_throws_dom checks.
138
+ return ["DOMException"] if defined?(Dommy::DOMException) && obj.is_a?(Dommy::DOMException)
139
+
140
+ names = []
141
+ klass = obj.class
142
+ while klass && klass.name&.start_with?("Dommy::")
143
+ name = name_for(klass)
144
+ names << name if name && !names.include?(name)
145
+ klass = klass.superclass
146
+ end
147
+ if defined?(Dommy::Node) && obj.is_a?(Dommy::Node)
148
+ names << "Node" unless names.include?("Node")
149
+ names << "EventTarget" unless names.include?("EventTarget")
150
+ end
151
+ names
152
+ end
153
+
154
+ def name_for(klass)
155
+ base = klass.name&.split("::")&.last
156
+ return nil unless base
157
+
158
+ NAME_OVERRIDES.fetch(base, base)
159
+ end
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,60 @@
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
+ # Tolerant lookup: nil when the handle is unknown (its object was released
38
+ # by GC, or a framework passed an invalid/never-allocated handle). Used for
39
+ # call ARGUMENTS, where a reference to a vanished object is simply absent —
40
+ # unlike a receiver handle, where a miss is a real error (#fetch).
41
+ def lookup(handle)
42
+ @by_handle[handle.to_i]
43
+ end
44
+
45
+ # Forget a handle (called when its JS proxy is garbage-collected). Only
46
+ # trims the mapping; the object itself lives on via its other references.
47
+ def release(handle)
48
+ obj = @by_handle.delete(handle.to_i)
49
+ return unless obj
50
+
51
+ oid = obj.object_id
52
+ @handle_by_oid.delete(oid) if @handle_by_oid[oid] == handle.to_i
53
+ end
54
+
55
+ def size
56
+ @by_handle.size
57
+ end
58
+ end
59
+ end
60
+ end