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.
- checksums.yaml +4 -4
- data/README.md +3 -3
- data/lib/dommy/animation.rb +4 -0
- data/lib/dommy/attr.rb +11 -5
- data/lib/dommy/backend/makiri_adapter.rb +330 -0
- data/lib/dommy/backend.rb +114 -33
- data/lib/dommy/blob.rb +2 -0
- data/lib/dommy/bridge.rb +11 -0
- data/lib/dommy/browser.rb +217 -0
- data/lib/dommy/compression_streams.rb +4 -0
- data/lib/dommy/crypto.rb +4 -0
- data/lib/dommy/css.rb +487 -50
- data/lib/dommy/custom_elements.rb +2 -2
- data/lib/dommy/data_transfer.rb +2 -0
- data/lib/dommy/data_uri.rb +35 -0
- data/lib/dommy/deferred_response.rb +59 -0
- data/lib/dommy/document.rb +386 -228
- data/lib/dommy/dom_exception.rb +2 -0
- data/lib/dommy/dom_parser.rb +7 -17
- data/lib/dommy/element.rb +502 -155
- data/lib/dommy/event.rb +240 -9
- data/lib/dommy/fetch.rb +152 -34
- data/lib/dommy/form_data.rb +2 -0
- data/lib/dommy/history.rb +2 -0
- data/lib/dommy/html_canvas_element.rb +230 -0
- data/lib/dommy/html_collection.rb +5 -6
- data/lib/dommy/html_elements.rb +304 -27
- data/lib/dommy/interaction/debug.rb +35 -0
- data/lib/dommy/interaction/dom_summary.rb +131 -0
- data/lib/dommy/interaction/driver.rb +244 -0
- data/lib/dommy/interaction/event_synthesis.rb +56 -0
- data/lib/dommy/interaction/field_interactor.rb +117 -0
- data/lib/dommy/interaction/form_submission.rb +268 -0
- data/lib/dommy/interaction/locator.rb +158 -0
- data/lib/dommy/interaction/role_query.rb +58 -0
- data/lib/dommy/interaction.rb +32 -0
- data/lib/dommy/internal/accessibility_tree.rb +215 -0
- data/lib/dommy/internal/accessible_description.rb +38 -0
- data/lib/dommy/internal/accessible_name.rb +301 -0
- data/lib/dommy/internal/aria_role.rb +252 -0
- data/lib/dommy/internal/aria_snapshot.rb +64 -0
- data/lib/dommy/internal/aria_state.rb +151 -0
- data/lib/dommy/internal/css/calc.rb +242 -0
- data/lib/dommy/internal/css/cascade.rb +430 -0
- data/lib/dommy/internal/css/color.rb +381 -0
- data/lib/dommy/internal/css/computed_style_declaration.rb +130 -0
- data/lib/dommy/internal/css/counters.rb +227 -0
- data/lib/dommy/internal/css/custom_properties.rb +183 -0
- data/lib/dommy/internal/css/media_query.rb +302 -0
- data/lib/dommy/internal/css/parser.rb +265 -0
- data/lib/dommy/internal/css/property_registry.rb +512 -0
- data/lib/dommy/internal/css/rule_index.rb +494 -0
- data/lib/dommy/internal/css/supports.rb +158 -0
- data/lib/dommy/internal/css/ua_stylesheet.rb +53 -0
- data/lib/dommy/internal/css_pseudo_handlers.rb +283 -42
- data/lib/dommy/internal/css_rule_text.rb +160 -0
- data/lib/dommy/internal/dom_matching.rb +80 -9
- data/lib/dommy/internal/element_matching.rb +109 -0
- data/lib/dommy/internal/global_functions.rb +33 -0
- data/lib/dommy/internal/mutation_coordinator.rb +95 -4
- data/lib/dommy/internal/namespaces.rb +49 -5
- data/lib/dommy/internal/node_wrapper_cache.rb +163 -26
- data/lib/dommy/internal/parent_node.rb +82 -5
- data/lib/dommy/internal/selector_ast.rb +124 -0
- data/lib/dommy/internal/selector_index.rb +146 -0
- data/lib/dommy/internal/selector_matcher.rb +756 -0
- data/lib/dommy/internal/selector_parser.rb +283 -131
- data/lib/dommy/internal/shadow_root_registry.rb +9 -2
- data/lib/dommy/internal/template_content_registry.rb +26 -18
- data/lib/dommy/internal/xml_serialization.rb +344 -0
- data/lib/dommy/intersection_observer.rb +2 -0
- data/lib/dommy/js/bridge_conformance.rb +80 -0
- data/lib/dommy/js/constructor_resolver.rb +44 -0
- data/lib/dommy/js/custom_element_bridge.rb +90 -0
- data/lib/dommy/js/dom_interfaces.rb +162 -0
- data/lib/dommy/js/handle_table.rb +60 -0
- data/lib/dommy/js/host_bridge.rb +517 -0
- data/lib/dommy/js/host_runtime.js +1495 -0
- data/lib/dommy/js/import_map.rb +58 -0
- data/lib/dommy/js/marshaller.rb +240 -0
- data/lib/dommy/js/module_loader.rb +99 -0
- data/lib/dommy/js/observable_runtime.js +742 -0
- data/lib/dommy/js/runtime.rb +115 -0
- data/lib/dommy/js/script_boot.rb +221 -0
- data/lib/dommy/js/wire_tags.rb +62 -0
- data/lib/dommy/location.rb +2 -0
- data/lib/dommy/media_query_list.rb +50 -14
- data/lib/dommy/message_channel.rb +22 -6
- data/lib/dommy/minitest/assertions.rb +27 -0
- data/lib/dommy/mutation_observer.rb +89 -4
- data/lib/dommy/navigator.rb +34 -2
- data/lib/dommy/node.rb +24 -14
- data/lib/dommy/notification.rb +2 -0
- data/lib/dommy/parser.rb +1 -1
- data/lib/dommy/performance.rb +21 -1
- data/lib/dommy/promise.rb +94 -10
- data/lib/dommy/range.rb +173 -31
- data/lib/dommy/resources.rb +178 -0
- data/lib/dommy/rspec/capy_style_matchers.rb +126 -0
- data/lib/dommy/scheduler.rb +149 -13
- data/lib/dommy/screen.rb +91 -0
- data/lib/dommy/shadow_root.rb +76 -13
- data/lib/dommy/storage.rb +2 -1
- data/lib/dommy/streams.rb +6 -0
- data/lib/dommy/text_codec.rb +7 -1
- data/lib/dommy/tree_walker.rb +33 -10
- data/lib/dommy/url.rb +13 -1
- data/lib/dommy/version.rb +1 -1
- data/lib/dommy/window.rb +199 -11
- data/lib/dommy/worker.rb +8 -4
- data/lib/dommy/xml_http_request.rb +47 -6
- data/lib/dommy.rb +36 -1
- metadata +96 -10
- data/lib/dommy/backend/nokogiri_adapter.rb +0 -127
- data/lib/dommy/backend/nokolexbor_adapter.rb +0 -117
data/lib/dommy/document.rb
CHANGED
|
@@ -71,6 +71,8 @@ module Dommy
|
|
|
71
71
|
@public_id
|
|
72
72
|
when "systemId"
|
|
73
73
|
@system_id
|
|
74
|
+
when "ownerDocument"
|
|
75
|
+
@owner_document
|
|
74
76
|
end
|
|
75
77
|
end
|
|
76
78
|
|
|
@@ -81,13 +83,16 @@ module Dommy
|
|
|
81
83
|
end
|
|
82
84
|
|
|
83
85
|
include Bridge::Methods
|
|
84
|
-
js_methods %w[isEqualNode isSameNode getRootNode hasChildNodes normalize compareDocumentPosition
|
|
86
|
+
js_methods %w[isEqualNode isSameNode getRootNode hasChildNodes normalize compareDocumentPosition contains
|
|
85
87
|
appendChild insertBefore removeChild replaceChild before after replaceWith remove
|
|
86
88
|
addEventListener removeEventListener dispatchEvent]
|
|
87
89
|
def __js_call__(method, args)
|
|
88
90
|
case method
|
|
89
91
|
when "hasChildNodes"
|
|
90
92
|
false
|
|
93
|
+
when "contains"
|
|
94
|
+
# A DocumentType is a leaf node: it contains only itself.
|
|
95
|
+
!args[0].nil? && is_same_node(args[0])
|
|
91
96
|
when "isEqualNode"
|
|
92
97
|
is_equal_node(args[0])
|
|
93
98
|
when "isSameNode"
|
|
@@ -97,8 +102,12 @@ module Dommy
|
|
|
97
102
|
when "compareDocumentPosition"
|
|
98
103
|
compare_document_position(args[0])
|
|
99
104
|
when "appendChild", "insertBefore"
|
|
105
|
+
raise Bridge::TypeError, "Argument is not a Node." unless args[0].is_a?(Dommy::Node)
|
|
106
|
+
|
|
100
107
|
raise DOMException::HierarchyRequestError, "a DocumentType may not have children"
|
|
101
108
|
when "removeChild", "replaceChild"
|
|
109
|
+
raise Bridge::TypeError, "Argument is not a Node." unless args[0].is_a?(Dommy::Node)
|
|
110
|
+
|
|
102
111
|
raise DOMException::NotFoundError, "the node to be removed is not a child of this node"
|
|
103
112
|
when "before"
|
|
104
113
|
before(*args)
|
|
@@ -120,143 +129,22 @@ module Dommy
|
|
|
120
129
|
end
|
|
121
130
|
end
|
|
122
131
|
|
|
123
|
-
#
|
|
124
|
-
# `target`; created via `document.createProcessingInstruction`.
|
|
125
|
-
class ProcessingInstruction
|
|
126
|
-
include Node
|
|
127
|
-
|
|
128
|
-
attr_reader :target
|
|
129
|
-
|
|
130
|
-
def initialize(target, data)
|
|
131
|
-
@target = target.to_s
|
|
132
|
-
@data = data.to_s
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
include EventTarget
|
|
136
|
-
|
|
137
|
-
def data = @data
|
|
138
|
-
|
|
139
|
-
def __js_get__(key)
|
|
140
|
-
case key
|
|
141
|
-
when "target"
|
|
142
|
-
@target
|
|
143
|
-
when "data", "nodeValue", "textContent"
|
|
144
|
-
@data
|
|
145
|
-
when "nodeName"
|
|
146
|
-
@target
|
|
147
|
-
when "nodeType"
|
|
148
|
-
7
|
|
149
|
-
when "length"
|
|
150
|
-
@data.length
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
def __js_set__(key, value)
|
|
155
|
-
case key
|
|
156
|
-
when "data", "nodeValue", "textContent"
|
|
157
|
-
@data = value.to_s
|
|
158
|
-
else
|
|
159
|
-
return Bridge::UNHANDLED
|
|
160
|
-
end
|
|
161
|
-
nil
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
# A PI is CharacterData: its data methods are string operations on @data.
|
|
165
|
-
def substring_data(offset, count)
|
|
166
|
-
o = offset.to_i
|
|
167
|
-
raise DOMException::IndexSizeError, "offset out of bounds" if o.negative? || o > @data.length
|
|
168
|
-
|
|
169
|
-
@data[o, count.to_i] || ""
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
def append_data(value)
|
|
173
|
-
@data += value.to_s
|
|
174
|
-
nil
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
def insert_data(offset, value)
|
|
178
|
-
o = offset.to_i
|
|
179
|
-
raise DOMException::IndexSizeError, "offset out of bounds" if o.negative? || o > @data.length
|
|
180
|
-
|
|
181
|
-
@data = @data[0, o].to_s + value.to_s + (@data[o..] || "")
|
|
182
|
-
nil
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
def delete_data(offset, count)
|
|
186
|
-
o = offset.to_i
|
|
187
|
-
raise DOMException::IndexSizeError, "offset out of bounds" if o.negative? || o > @data.length
|
|
188
|
-
|
|
189
|
-
@data = @data[0, o].to_s + (@data[(o + count.to_i)..] || "")
|
|
190
|
-
nil
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
def replace_data(offset, count, value)
|
|
194
|
-
delete_data(offset, count)
|
|
195
|
-
insert_data(offset, value)
|
|
196
|
-
nil
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
def __internal_event_parent__
|
|
200
|
-
nil
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
include Bridge::Methods
|
|
204
|
-
js_methods %w[isEqualNode isSameNode getRootNode normalize hasChildNodes
|
|
205
|
-
appendData insertData deleteData replaceData substringData compareDocumentPosition
|
|
206
|
-
appendChild insertBefore removeChild replaceChild before after replaceWith remove
|
|
207
|
-
addEventListener removeEventListener dispatchEvent]
|
|
208
|
-
def __js_call__(method, args)
|
|
209
|
-
case method
|
|
210
|
-
when "hasChildNodes"
|
|
211
|
-
false
|
|
212
|
-
when "isEqualNode"
|
|
213
|
-
is_equal_node(args[0])
|
|
214
|
-
when "isSameNode"
|
|
215
|
-
is_same_node(args[0])
|
|
216
|
-
when "getRootNode"
|
|
217
|
-
get_root_node(args[0])
|
|
218
|
-
when "compareDocumentPosition"
|
|
219
|
-
compare_document_position(args[0])
|
|
220
|
-
when "appendChild", "insertBefore"
|
|
221
|
-
raise DOMException::HierarchyRequestError, "a ProcessingInstruction may not have children"
|
|
222
|
-
when "removeChild", "replaceChild"
|
|
223
|
-
raise DOMException::NotFoundError, "the node to be removed is not a child of this node"
|
|
224
|
-
when "before", "after", "replaceWith", "remove"
|
|
225
|
-
# ChildNode mixin: a created PI has no parent, so these are no-ops per
|
|
226
|
-
# spec (they act only when the node is inserted in a tree).
|
|
227
|
-
nil
|
|
228
|
-
when "normalize"
|
|
229
|
-
nil
|
|
230
|
-
when "substringData"
|
|
231
|
-
substring_data(args[0], args[1])
|
|
232
|
-
when "appendData"
|
|
233
|
-
append_data(args[0])
|
|
234
|
-
when "insertData"
|
|
235
|
-
insert_data(args[0], args[1])
|
|
236
|
-
when "deleteData"
|
|
237
|
-
delete_data(args[0], args[1])
|
|
238
|
-
when "replaceData"
|
|
239
|
-
replace_data(args[0], args[1], args[2])
|
|
240
|
-
when "addEventListener"
|
|
241
|
-
add_event_listener(args[0], args[1], args[2])
|
|
242
|
-
when "removeEventListener"
|
|
243
|
-
remove_event_listener(args[0], args[1], args[2])
|
|
244
|
-
when "dispatchEvent"
|
|
245
|
-
dispatch_event(args[0])
|
|
246
|
-
end
|
|
247
|
-
end
|
|
248
|
-
end
|
|
249
|
-
|
|
250
|
-
# `document.implementation` — the DOMImplementation. Only the node factories
|
|
251
|
-
# WPT exercises are provided; createDocument/createHTMLDocument are not yet
|
|
252
|
-
# implemented (foreign documents).
|
|
132
|
+
# `document.implementation` — the DOMImplementation.
|
|
253
133
|
class DOMImplementation
|
|
254
134
|
def initialize(document)
|
|
255
135
|
@document = document
|
|
256
136
|
end
|
|
257
137
|
|
|
138
|
+
# A created DocumentType's node document is the implementation's document.
|
|
139
|
+
# (Qualified-name validation against the QName production is not enforced —
|
|
140
|
+
# a couple of invalid-name WPT cases stay as documented gaps.)
|
|
258
141
|
def create_document_type(qualified_name, public_id, system_id)
|
|
259
|
-
DocumentType.new(qualified_name, public_id, system_id)
|
|
142
|
+
DocumentType.new(qualified_name, public_id, system_id, owner_document: @document)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# `hasFeature()` is a no-op that always returns true (DOM Standard).
|
|
146
|
+
def has_feature(*)
|
|
147
|
+
true
|
|
260
148
|
end
|
|
261
149
|
|
|
262
150
|
# createDocument(namespace, qualifiedName, doctype?) — a fresh XML document,
|
|
@@ -264,11 +152,21 @@ module Dommy
|
|
|
264
152
|
# non-empty. (The doctype argument is accepted but not stored, as document
|
|
265
153
|
# equality compares only structure that survives wrap_node.)
|
|
266
154
|
def create_document(namespace, qualified_name, _doctype = nil)
|
|
267
|
-
doc = Document.new(nil,
|
|
155
|
+
doc = Document.new(nil, backend_doc: Backend.empty_xml_document)
|
|
156
|
+
# createDocument's content type is keyed off the namespace. None is
|
|
157
|
+
# "text/html", so tagName keeps its case; xhtml+xml still routes
|
|
158
|
+
# createElement to the HTML namespace (so an XHTML document isEqualNode
|
|
159
|
+
# an HTML one).
|
|
160
|
+
doc.content_type =
|
|
161
|
+
case namespace.to_s
|
|
162
|
+
when Internal::Namespaces::HTML then "application/xhtml+xml"
|
|
163
|
+
when Internal::Namespaces::SVG then "image/svg+xml"
|
|
164
|
+
else "application/xml"
|
|
165
|
+
end
|
|
268
166
|
qn = qualified_name.to_s
|
|
269
167
|
unless qn.empty?
|
|
270
168
|
el = doc.send(:create_element_ns, namespace, qualified_name)
|
|
271
|
-
doc.
|
|
169
|
+
Backend.set_document_root(doc.backend_doc, el.__dommy_backend_node__)
|
|
272
170
|
end
|
|
273
171
|
doc
|
|
274
172
|
end
|
|
@@ -276,15 +174,15 @@ module Dommy
|
|
|
276
174
|
# createHTMLDocument(title?) — a fresh HTML document (doctype + html > head,
|
|
277
175
|
# body), with an optional <title>.
|
|
278
176
|
def create_html_document(title = nil)
|
|
279
|
-
doc = Document.new(nil,
|
|
177
|
+
doc = Document.new(nil, backend_doc: Backend.parse("<!DOCTYPE html><html><head></head><body></body></html>"))
|
|
280
178
|
doc.title = title.to_s unless title.nil? || title.equal?(Bridge::UNDEFINED)
|
|
281
179
|
doc
|
|
282
180
|
end
|
|
283
181
|
|
|
284
|
-
def __js_get__(_key) =
|
|
182
|
+
def __js_get__(_key) = Bridge::ABSENT # method-only; any property read is absent
|
|
285
183
|
|
|
286
184
|
include Bridge::Methods
|
|
287
|
-
js_methods %w[createDocumentType createDocument createHTMLDocument]
|
|
185
|
+
js_methods %w[createDocumentType createDocument createHTMLDocument hasFeature]
|
|
288
186
|
def __js_call__(method, args)
|
|
289
187
|
case method
|
|
290
188
|
when "createDocumentType"
|
|
@@ -293,6 +191,8 @@ module Dommy
|
|
|
293
191
|
create_document(args[0], args[1], args[2])
|
|
294
192
|
when "createHTMLDocument"
|
|
295
193
|
create_html_document(args[0])
|
|
194
|
+
when "hasFeature"
|
|
195
|
+
has_feature
|
|
296
196
|
end
|
|
297
197
|
end
|
|
298
198
|
end
|
|
@@ -304,13 +204,108 @@ module Dommy
|
|
|
304
204
|
include EventTarget
|
|
305
205
|
include Node
|
|
306
206
|
|
|
307
|
-
attr_reader :
|
|
207
|
+
attr_reader :backend_doc
|
|
308
208
|
attr_accessor :default_view
|
|
209
|
+
# --- CSS cascade support (Internal::CSS) ---
|
|
210
|
+
# Monotonic counter bumped on every DOM mutation; the CSS layer
|
|
211
|
+
# invalidates its per-document style cache wholesale when it moves.
|
|
212
|
+
# The cache slot itself is owned by Internal::CSS::Cascade.
|
|
213
|
+
attr_accessor :__css_style_cache__
|
|
214
|
+
|
|
215
|
+
def style_generation
|
|
216
|
+
@style_generation || 0
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def __internal_bump_style_generation__
|
|
220
|
+
@style_generation = style_generation + 1
|
|
221
|
+
nil
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# A by-id/class/tag index of the backend element tree, memoized per DOM
|
|
225
|
+
# generation, for SelectorMatcher's document-scoped fast path (or nil to tell
|
|
226
|
+
# the caller to walk). Rebuilt lazily only after a mutation bumps
|
|
227
|
+
# style_generation, so it costs one tree walk per generation and pays off when
|
|
228
|
+
# several queries run before the next mutation.
|
|
229
|
+
#
|
|
230
|
+
# Adaptive bypass: if the index keeps getting invalidated after serving only a
|
|
231
|
+
# handful of queries (a mutation-between-every-query workload, where building
|
|
232
|
+
# it never pays back), stop building it and just walk — re-testing
|
|
233
|
+
# periodically. This keeps the worst case at walk speed rather than the ~15%
|
|
234
|
+
# regression an always-on index would add.
|
|
235
|
+
SELECTOR_INDEX_MIN_REUSE = 3 # queries an index must serve to have paid for its build
|
|
236
|
+
SELECTOR_INDEX_LOW_RUN_LIMIT = 8 # consecutive low-reuse generations before bypassing
|
|
237
|
+
SELECTOR_INDEX_RETEST_GAP = 64 # generations to wait before re-testing a bypass
|
|
238
|
+
|
|
239
|
+
def __internal_selector_index__
|
|
240
|
+
gen = style_generation
|
|
241
|
+
if @__sel_idx_gen != gen
|
|
242
|
+
if @__sel_idx
|
|
243
|
+
if @__sel_idx_served.to_i < SELECTOR_INDEX_MIN_REUSE
|
|
244
|
+
@__sel_idx_low = @__sel_idx_low.to_i + 1
|
|
245
|
+
@__sel_idx_bypass = true if @__sel_idx_low >= SELECTOR_INDEX_LOW_RUN_LIMIT
|
|
246
|
+
else
|
|
247
|
+
@__sel_idx_low = 0
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
if @__sel_idx_bypass && (@__sel_idx_retest = @__sel_idx_retest.to_i + 1) >= SELECTOR_INDEX_RETEST_GAP
|
|
251
|
+
@__sel_idx_bypass = false
|
|
252
|
+
@__sel_idx_low = 0
|
|
253
|
+
@__sel_idx_retest = 0
|
|
254
|
+
end
|
|
255
|
+
@__sel_idx = nil
|
|
256
|
+
@__sel_idx_served = 0
|
|
257
|
+
@__sel_idx_gen = gen
|
|
258
|
+
end
|
|
259
|
+
return nil if @__sel_idx_bypass
|
|
260
|
+
|
|
261
|
+
@__sel_idx ||= Internal::SelectorIndex.build(@backend_doc)
|
|
262
|
+
@__sel_idx_served += 1
|
|
263
|
+
@__sel_idx
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# An element-scoped querySelector(All) result cache (the document-rooted one
|
|
267
|
+
# lives in NodeWrapperCache). jQuery `$(el).find(sel)` re-queries the same
|
|
268
|
+
# (element, selector) constantly between mutations; this memoizes the match
|
|
269
|
+
# set, keyed by [scope object_id, kind, selector] and tagged with the DOM
|
|
270
|
+
# generation, so a hit skips the whole combinator match. Capped, and a
|
|
271
|
+
# mutation (style_generation bump) makes every entry stale at once.
|
|
272
|
+
SCOPED_QUERY_CACHE_CAP = 4096
|
|
273
|
+
|
|
274
|
+
def __internal_scoped_query_get(key)
|
|
275
|
+
entry = (@__scoped_query_cache ||= {})[key]
|
|
276
|
+
entry && entry[0] == style_generation ? entry[1] : nil
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def __internal_scoped_query_set(key, value)
|
|
280
|
+
cache = (@__scoped_query_cache ||= {})
|
|
281
|
+
cache.clear if cache.size >= SCOPED_QUERY_CACHE_CAP
|
|
282
|
+
cache[key] = [style_generation, value]
|
|
283
|
+
value
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# A host-supplied `->(url) { css_text_or_nil }` resolving @import URLs to
|
|
287
|
+
# CSS (Dommy has no network of its own — same idea as <link> filling).
|
|
288
|
+
# Setting it invalidates cached styles so the next cascade picks up imports.
|
|
289
|
+
attr_reader :css_import_resolver
|
|
290
|
+
|
|
291
|
+
def css_import_resolver=(resolver)
|
|
292
|
+
@css_import_resolver = resolver
|
|
293
|
+
__internal_bump_style_generation__
|
|
294
|
+
end
|
|
309
295
|
# content_type defaults to "text/html"; settable so an integration layer
|
|
310
296
|
# can reflect the response Content-Type. Read-only over the JS bridge.
|
|
311
297
|
attr_accessor :content_type
|
|
312
|
-
|
|
313
|
-
|
|
298
|
+
# A `->(source_text) {}` set by the JS layer to execute a classic <script>'s
|
|
299
|
+
# body when it's connected (Dommy has no JS engine of its own). nil = inert
|
|
300
|
+
# scripts (the default for a standalone DOM).
|
|
301
|
+
attr_accessor :script_runner
|
|
302
|
+
# A `->(element, src) {}` set by the integration layer to fetch + execute a
|
|
303
|
+
# classic `<script src>` that's dynamically inserted into the document (e.g.
|
|
304
|
+
# webpack/Vite loading an on-demand chunk via document.head.appendChild). It
|
|
305
|
+
# owns firing the element's load / error event. nil = such scripts are inert.
|
|
306
|
+
attr_accessor :external_script_runner
|
|
307
|
+
|
|
308
|
+
def initialize(host = nil, backend_doc: nil, default_view: nil)
|
|
314
309
|
@host = host
|
|
315
310
|
@default_view = default_view
|
|
316
311
|
@node_wrapper_cache = Internal::NodeWrapperCache.new(self)
|
|
@@ -320,8 +315,15 @@ module Dommy
|
|
|
320
315
|
@template_content_registry = Internal::TemplateContentRegistry.new(self)
|
|
321
316
|
@mutation_coordinator = Internal::MutationCoordinator.new(self, @observer_manager)
|
|
322
317
|
@node_iterators = []
|
|
323
|
-
@
|
|
318
|
+
@backend_doc = backend_doc || Backend.parse("<!doctype html><html><head></head><body></body></html>")
|
|
324
319
|
@content_type = "text/html"
|
|
320
|
+
# The document is fully parsed before scripts run (no incremental network
|
|
321
|
+
# parse), so it defaults to "complete" — ready-gated code takes the
|
|
322
|
+
# already-loaded path. An embedder can replay the real lifecycle
|
|
323
|
+
# ("loading" → "interactive" → "complete") via #__internal_set_ready_state__
|
|
324
|
+
# to drive code that waits on DOMContentLoaded / load.
|
|
325
|
+
@ready_state = "complete"
|
|
326
|
+
@__current_script__ = nil
|
|
325
327
|
end
|
|
326
328
|
|
|
327
329
|
# Whether this is an "HTML document" in the DOM sense (created by the HTML
|
|
@@ -339,7 +341,7 @@ module Dommy
|
|
|
339
341
|
# public/system identifier) is no-quirks. (The full quirks algorithm keys off
|
|
340
342
|
# specific legacy public ids; this covers the common cases.)
|
|
341
343
|
def compat_mode
|
|
342
|
-
dt = @
|
|
344
|
+
dt = @backend_doc.internal_subset
|
|
343
345
|
return "BackCompat" unless dt
|
|
344
346
|
return "CSS1Compat" if dt.name.to_s.downcase == "html" && dt.external_id.nil?
|
|
345
347
|
|
|
@@ -358,11 +360,11 @@ module Dommy
|
|
|
358
360
|
|
|
359
361
|
def document_element
|
|
360
362
|
# The document's root element — `<html>` for HTML, the actual root for XML.
|
|
361
|
-
wrap_node(@
|
|
363
|
+
wrap_node(@backend_doc.root)
|
|
362
364
|
end
|
|
363
365
|
|
|
364
366
|
def head
|
|
365
|
-
wrap_node(@
|
|
367
|
+
wrap_node(@backend_doc.at_css("head"))
|
|
366
368
|
end
|
|
367
369
|
|
|
368
370
|
# Resolve `body` fresh from the tree (not memoized) so it tracks a swapped
|
|
@@ -371,22 +373,34 @@ module Dommy
|
|
|
371
373
|
# wrapper would keep returning the detached old body. wrap_node caches by
|
|
372
374
|
# node, so identity (`document.body === document.body`) still holds.
|
|
373
375
|
def body
|
|
374
|
-
wrap_node(@
|
|
376
|
+
wrap_node(@backend_doc.at_css("body"))
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# The document's accessibility tree (built from <body>; the document itself
|
|
380
|
+
# has no accessible node). See Internal::AccessibilityTree.
|
|
381
|
+
def accessibility_tree
|
|
382
|
+
Internal::AccessibilityTree.build(self)
|
|
383
|
+
end
|
|
384
|
+
alias_method :aria_tree, :accessibility_tree
|
|
385
|
+
|
|
386
|
+
# A Playwright-compatible ARIA snapshot of the document.
|
|
387
|
+
def aria_snapshot
|
|
388
|
+
Internal::AriaSnapshot.serialize(accessibility_tree)
|
|
375
389
|
end
|
|
376
390
|
|
|
377
391
|
# Serialize the whole document to HTML (including the doctype).
|
|
378
392
|
def to_html
|
|
379
|
-
@
|
|
393
|
+
@backend_doc.to_html
|
|
380
394
|
end
|
|
381
395
|
|
|
382
396
|
# XPath queries returning wrapped nodes (Element / TextNode / etc).
|
|
383
397
|
def at_xpath(expression)
|
|
384
|
-
node = @
|
|
398
|
+
node = @backend_doc.at_xpath(expression)
|
|
385
399
|
node && wrap_node(node)
|
|
386
400
|
end
|
|
387
401
|
|
|
388
402
|
def xpath(expression)
|
|
389
|
-
@
|
|
403
|
+
@backend_doc.xpath(expression).map { |node| wrap_node(node) }
|
|
390
404
|
end
|
|
391
405
|
|
|
392
406
|
# `document.URL` / `documentURI` — both return location.href in
|
|
@@ -404,7 +418,7 @@ module Dommy
|
|
|
404
418
|
# ignore subsequent <base> elements; we mirror that.
|
|
405
419
|
def base_uri
|
|
406
420
|
doc_url = url
|
|
407
|
-
base_el = @
|
|
421
|
+
base_el = @backend_doc.at_css("base[href]")
|
|
408
422
|
return doc_url unless base_el
|
|
409
423
|
|
|
410
424
|
href = base_el["href"].to_s
|
|
@@ -445,25 +459,25 @@ module Dommy
|
|
|
445
459
|
# document so post-mutation reads reflect the current state.
|
|
446
460
|
def links
|
|
447
461
|
HTMLCollection.new do
|
|
448
|
-
@
|
|
462
|
+
@backend_doc.css("a[href], area[href]").map { |n| wrap_node(n) }.compact
|
|
449
463
|
end
|
|
450
464
|
end
|
|
451
465
|
|
|
452
466
|
def forms
|
|
453
467
|
HTMLCollection.new do
|
|
454
|
-
@
|
|
468
|
+
@backend_doc.css("form").map { |n| wrap_node(n) }.compact
|
|
455
469
|
end
|
|
456
470
|
end
|
|
457
471
|
|
|
458
472
|
def scripts
|
|
459
473
|
HTMLCollection.new do
|
|
460
|
-
@
|
|
474
|
+
@backend_doc.css("script").map { |n| wrap_node(n) }.compact
|
|
461
475
|
end
|
|
462
476
|
end
|
|
463
477
|
|
|
464
478
|
def images
|
|
465
479
|
HTMLCollection.new do
|
|
466
|
-
@
|
|
480
|
+
@backend_doc.css("img").map { |n| wrap_node(n) }.compact
|
|
467
481
|
end
|
|
468
482
|
end
|
|
469
483
|
|
|
@@ -471,7 +485,7 @@ module Dommy
|
|
|
471
485
|
# in practice the `<html>` root).
|
|
472
486
|
def children
|
|
473
487
|
HTMLCollection.new do
|
|
474
|
-
root = @
|
|
488
|
+
root = @backend_doc.root
|
|
475
489
|
root ? [wrap_node(root)].compact : []
|
|
476
490
|
end
|
|
477
491
|
end
|
|
@@ -481,7 +495,7 @@ module Dommy
|
|
|
481
495
|
# `document.childNodes === document.childNodes` and mutations are reflected.
|
|
482
496
|
def child_nodes
|
|
483
497
|
@live_child_nodes ||= LiveNodeList.new do
|
|
484
|
-
@
|
|
498
|
+
@backend_doc.children.map { |n| wrap_node(n) }.compact
|
|
485
499
|
end
|
|
486
500
|
end
|
|
487
501
|
|
|
@@ -490,11 +504,11 @@ module Dommy
|
|
|
490
504
|
end
|
|
491
505
|
|
|
492
506
|
def first_element_child
|
|
493
|
-
wrap_node(@
|
|
507
|
+
wrap_node(@backend_doc.root)
|
|
494
508
|
end
|
|
495
509
|
|
|
496
510
|
def last_element_child
|
|
497
|
-
wrap_node(@
|
|
511
|
+
wrap_node(@backend_doc.root)
|
|
498
512
|
end
|
|
499
513
|
|
|
500
514
|
# Currently-focused element (or body if none). Updated via
|
|
@@ -511,13 +525,37 @@ module Dommy
|
|
|
511
525
|
return false unless other.respond_to?(:__dommy_backend_node__)
|
|
512
526
|
|
|
513
527
|
node = other.__dommy_backend_node__
|
|
514
|
-
node.document == @
|
|
528
|
+
node.document == @backend_doc && node.ancestors.include?(@backend_doc)
|
|
515
529
|
end
|
|
516
530
|
|
|
517
531
|
def __internal_set_active_element__(el)
|
|
532
|
+
# Focus is selector-observable state (:focus / :focus-within rules), so
|
|
533
|
+
# a change invalidates computed styles.
|
|
534
|
+
__internal_bump_style_generation__ unless @active_element.equal?(el)
|
|
518
535
|
@active_element = el
|
|
519
536
|
end
|
|
520
537
|
|
|
538
|
+
# The explicitly focused element (nil when nothing holds focus) — what
|
|
539
|
+
# :focus matches. Distinct from #active_element, which falls back to
|
|
540
|
+
# <body> per spec.
|
|
541
|
+
def __internal_focused_element__
|
|
542
|
+
@active_element
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
# The element the (virtual) pointer hovers — :hover matches it and its
|
|
546
|
+
# ancestors. Set from tests or capybara-dommy's Node#hover; nil clears.
|
|
547
|
+
def __internal_hovered_element__
|
|
548
|
+
@hovered_element
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
def __internal_set_hovered_element__(el)
|
|
552
|
+
return if @hovered_element.equal?(el)
|
|
553
|
+
|
|
554
|
+
@hovered_element = el
|
|
555
|
+
__internal_bump_style_generation__
|
|
556
|
+
nil
|
|
557
|
+
end
|
|
558
|
+
|
|
521
559
|
# Create a detached Attr. `setAttributeNode` attaches it to an
|
|
522
560
|
# element. Per spec, name must match the XML Name production —
|
|
523
561
|
# invalid names throw InvalidCharacterError.
|
|
@@ -534,7 +572,15 @@ module Dommy
|
|
|
534
572
|
# Ruby Proc, a JS-bridge callable, or an object with
|
|
535
573
|
# `accept_node` / `acceptNode`.
|
|
536
574
|
def create_tree_walker(root, what_to_show = NodeFilter::SHOW_ALL, filter = nil)
|
|
537
|
-
TreeWalker.new(root, what_to_show, filter)
|
|
575
|
+
TreeWalker.new(require_node_root(root), what_to_show, filter)
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
# The `root` of a TreeWalker / NodeIterator is a non-nullable WebIDL `Node`:
|
|
579
|
+
# a null or non-Node argument is a TypeError before construction.
|
|
580
|
+
def require_node_root(root)
|
|
581
|
+
return root if root.is_a?(Dommy::Node)
|
|
582
|
+
|
|
583
|
+
raise Bridge::TypeError, "createTreeWalker/createNodeIterator root must be a Node"
|
|
538
584
|
end
|
|
539
585
|
|
|
540
586
|
# WebIDL `unsigned long whatToShow = 0xFFFFFFFF`: an omitted or `undefined`
|
|
@@ -576,25 +622,22 @@ module Dommy
|
|
|
576
622
|
src.unlink if src.parent
|
|
577
623
|
|
|
578
624
|
# Same document: just return the wrapper after the detach above.
|
|
579
|
-
return wrap_node(src) if src.document == @
|
|
625
|
+
return wrap_node(src) if src.document == @backend_doc
|
|
580
626
|
|
|
581
|
-
# Cross-document:
|
|
582
|
-
#
|
|
583
|
-
#
|
|
584
|
-
#
|
|
585
|
-
#
|
|
627
|
+
# Cross-document: hand the detached source to the backend, which
|
|
628
|
+
# returns the node now owned by this document — an imported copy for
|
|
629
|
+
# Makiri (a node can't move between arenas). Drop the stale source
|
|
630
|
+
# wrapper, then reseat the caller's Dommy wrapper onto the adopted
|
|
631
|
+
# node so `adopt_node(x).equal?(x)` stays true across documents.
|
|
586
632
|
src_doc_wrapper = node.instance_variable_get(:@document)
|
|
587
|
-
|
|
588
|
-
src.unlink
|
|
633
|
+
adopted = Backend.adopt(src, @backend_doc)
|
|
589
634
|
|
|
590
|
-
# Move the caller's Dommy wrapper from the source document's
|
|
591
|
-
# wrapper cache into ours, and re-point its @document. This
|
|
592
|
-
# keeps `adopt_node(x).equal?(x)` true across documents.
|
|
593
|
-
node.instance_variable_set(:@document, self)
|
|
594
635
|
if src_doc_wrapper.respond_to?(:__internal_reset_wrapper__)
|
|
595
636
|
src_doc_wrapper.__internal_reset_wrapper__(src)
|
|
596
637
|
end
|
|
597
|
-
|
|
638
|
+
node.instance_variable_set(:@document, self)
|
|
639
|
+
node.instance_variable_set(:@__node__, adopted)
|
|
640
|
+
@node_wrapper_cache.register(adopted, node)
|
|
598
641
|
node
|
|
599
642
|
end
|
|
600
643
|
|
|
@@ -670,13 +713,31 @@ module Dommy
|
|
|
670
713
|
# `document.createNodeIterator(root, whatToShow?, filter?)` —
|
|
671
714
|
# flat depth-first iteration.
|
|
672
715
|
def create_node_iterator(root, what_to_show = NodeFilter::SHOW_ALL, filter = nil)
|
|
716
|
+
root = require_node_root(root)
|
|
673
717
|
iterator = NodeIterator.new(root, what_to_show, filter)
|
|
674
|
-
#
|
|
675
|
-
#
|
|
676
|
-
|
|
718
|
+
# The "NodeIterator pre-removing steps" run for iterators whose root's node
|
|
719
|
+
# document is the removed node's document. Track the iterator on the root's
|
|
720
|
+
# document — which is `self` for a same-document root, but a different
|
|
721
|
+
# document when the root came from elsewhere (e.g.
|
|
722
|
+
# implementation.createHTMLDocument), where the removal fires.
|
|
723
|
+
node_iterator_document(root).__internal_track_node_iterator__(iterator)
|
|
677
724
|
iterator
|
|
678
725
|
end
|
|
679
726
|
|
|
727
|
+
# The document that owns `root`'s subtree (where its removals fire), so a
|
|
728
|
+
# NodeIterator is tracked where its pre-removing steps will run. Falls back
|
|
729
|
+
# to `self` for a root with no resolvable document.
|
|
730
|
+
def node_iterator_document(root)
|
|
731
|
+
return root if root.is_a?(Dommy::Document)
|
|
732
|
+
|
|
733
|
+
doc = root.instance_variable_get(:@document)
|
|
734
|
+
doc.is_a?(Dommy::Document) ? doc : self
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
def __internal_track_node_iterator__(iterator)
|
|
738
|
+
@node_iterators << iterator
|
|
739
|
+
end
|
|
740
|
+
|
|
680
741
|
# Minimal DocumentType — represents the `<!doctype html>` line.
|
|
681
742
|
# Always present in HTML5 documents we parse, so we synthesize a
|
|
682
743
|
# stub object whose only useful field is `name`. Tests just need
|
|
@@ -692,7 +753,7 @@ module Dommy
|
|
|
692
753
|
end
|
|
693
754
|
|
|
694
755
|
def create_processing_instruction(target, data)
|
|
695
|
-
|
|
756
|
+
@node_wrapper_cache.create_processing_instruction(target, data)
|
|
696
757
|
end
|
|
697
758
|
|
|
698
759
|
# Append a node as a child of the document itself (e.g. a comment alongside
|
|
@@ -700,37 +761,38 @@ module Dommy
|
|
|
700
761
|
def append_child(node)
|
|
701
762
|
return node unless node.respond_to?(:__dommy_backend_node__)
|
|
702
763
|
|
|
703
|
-
|
|
764
|
+
# appendChild adopts a node from another document (per spec). Only needed on
|
|
765
|
+
# a backend that can't move a node across documents (Makiri).
|
|
766
|
+
if !Backend.moves_nodes_across_documents? && node.respond_to?(:document) && !node.document.equal?(self)
|
|
767
|
+
node = adopt_node(node)
|
|
768
|
+
end
|
|
769
|
+
@backend_doc.add_child(node.__dommy_backend_node__)
|
|
704
770
|
node
|
|
705
771
|
end
|
|
706
772
|
|
|
707
773
|
# ParentNode / Node mutation on the document's direct children (the doctype
|
|
708
|
-
# and the document element).
|
|
709
|
-
# arguments (which would need a text child the document can't hold) are
|
|
710
|
-
# ignored rather than raising.
|
|
774
|
+
# and the document element).
|
|
711
775
|
def document_insert(args, prepend:)
|
|
712
776
|
nodes = args.filter_map { |a| backend_node(a) }
|
|
713
|
-
if prepend && (first = @
|
|
777
|
+
if prepend && (first = @backend_doc.children.first)
|
|
714
778
|
nodes.reverse_each { |n| first.add_previous_sibling(n) }
|
|
715
779
|
else
|
|
716
|
-
nodes.each { |n| @
|
|
780
|
+
nodes.each { |n| @backend_doc.add_child(n) }
|
|
717
781
|
end
|
|
718
782
|
nil
|
|
719
783
|
end
|
|
720
784
|
|
|
721
785
|
def document_replace_children(args)
|
|
722
|
-
@
|
|
723
|
-
args.filter_map { |a| backend_node(a) }.each { |n| @
|
|
786
|
+
@backend_doc.children.each(&:unlink)
|
|
787
|
+
args.filter_map { |a| backend_node(a) }.each { |n| @backend_doc.add_child(n) }
|
|
724
788
|
nil
|
|
725
789
|
end
|
|
726
790
|
|
|
727
791
|
def document_remove_child(node)
|
|
728
|
-
# The doctype is synthesized from the Nokogiri DTD rather than wrapped as a
|
|
729
|
-
# tree node, so remove the internal subset directly.
|
|
730
792
|
return __internal_remove_doctype__(node) if node.is_a?(DocumentType)
|
|
731
793
|
|
|
732
794
|
bn = backend_node(node)
|
|
733
|
-
raise DOMException::NotFoundError, "node is not a child of this document" unless bn && bn.parent == @
|
|
795
|
+
raise DOMException::NotFoundError, "node is not a child of this document" unless bn && bn.parent == @backend_doc
|
|
734
796
|
|
|
735
797
|
bn.unlink
|
|
736
798
|
node
|
|
@@ -741,17 +803,17 @@ module Dommy
|
|
|
741
803
|
return node unless bn
|
|
742
804
|
|
|
743
805
|
ref_node = ref && backend_node(ref)
|
|
744
|
-
if ref_node && ref_node.parent == @
|
|
806
|
+
if ref_node && ref_node.parent == @backend_doc
|
|
745
807
|
ref_node.add_previous_sibling(bn)
|
|
746
808
|
else
|
|
747
|
-
@
|
|
809
|
+
@backend_doc.add_child(bn)
|
|
748
810
|
end
|
|
749
811
|
node
|
|
750
812
|
end
|
|
751
813
|
|
|
752
814
|
def document_replace_child(new_child, old_child)
|
|
753
815
|
old_bn = backend_node(old_child)
|
|
754
|
-
raise DOMException::NotFoundError, "node is not a child of this document" unless old_bn && old_bn.parent == @
|
|
816
|
+
raise DOMException::NotFoundError, "node is not a child of this document" unless old_bn && old_bn.parent == @backend_doc
|
|
755
817
|
|
|
756
818
|
new_bn = backend_node(new_child)
|
|
757
819
|
old_bn.add_previous_sibling(new_bn) if new_bn
|
|
@@ -763,7 +825,7 @@ module Dommy
|
|
|
763
825
|
# remove the internal subset and mark it gone.
|
|
764
826
|
def __internal_remove_doctype__(_doctype)
|
|
765
827
|
@doctype_removed = true
|
|
766
|
-
@
|
|
828
|
+
@backend_doc.internal_subset&.unlink
|
|
767
829
|
nil
|
|
768
830
|
end
|
|
769
831
|
|
|
@@ -772,20 +834,20 @@ module Dommy
|
|
|
772
834
|
def __internal_insert_at_doctype__(nodes, after:)
|
|
773
835
|
bns = nodes.filter_map { |n| backend_node(n) }
|
|
774
836
|
if after
|
|
775
|
-
root = @
|
|
776
|
-
root ? bns.each { |n| root.add_previous_sibling(n) } : bns.each { |n| @
|
|
837
|
+
root = @backend_doc.root
|
|
838
|
+
root ? bns.each { |n| root.add_previous_sibling(n) } : bns.each { |n| @backend_doc.add_child(n) }
|
|
777
839
|
else
|
|
778
|
-
first = @
|
|
779
|
-
first ? bns.reverse_each { |n| first.add_previous_sibling(n) } : bns.each { |n| @
|
|
840
|
+
first = @backend_doc.children.first
|
|
841
|
+
first ? bns.reverse_each { |n| first.add_previous_sibling(n) } : bns.each { |n| @backend_doc.add_child(n) }
|
|
780
842
|
end
|
|
781
843
|
nil
|
|
782
844
|
end
|
|
783
845
|
|
|
784
846
|
# `document.cloneNode(deep)` → a fresh Document over a (deep) copy of the
|
|
785
|
-
#
|
|
847
|
+
# Makiri tree, preserving the content type.
|
|
786
848
|
def clone_node(deep)
|
|
787
|
-
copy = deep ? @
|
|
788
|
-
Document.new(nil,
|
|
849
|
+
copy = deep ? Backend.clone_document(@backend_doc) : Backend.empty_document_like(@backend_doc)
|
|
850
|
+
Document.new(nil, backend_doc: copy).tap { |d| d.content_type = @content_type }
|
|
789
851
|
end
|
|
790
852
|
|
|
791
853
|
def backend_node(node)
|
|
@@ -816,7 +878,7 @@ module Dommy
|
|
|
816
878
|
end
|
|
817
879
|
|
|
818
880
|
def get_elements_by_tag_name_ns(namespace, local_name)
|
|
819
|
-
HTMLCollection.elements_by_tag_name_ns(@
|
|
881
|
+
HTMLCollection.elements_by_tag_name_ns(@backend_doc, self, namespace, local_name)
|
|
820
882
|
end
|
|
821
883
|
|
|
822
884
|
# `document.write(html)` — legacy API. Appends parsed nodes to the
|
|
@@ -824,7 +886,7 @@ module Dommy
|
|
|
824
886
|
# this stub is enough for tests that fire write() during teardown.
|
|
825
887
|
def write(*args)
|
|
826
888
|
html = args.join
|
|
827
|
-
fragment = Parser.fragment(html, owner_doc: @
|
|
889
|
+
fragment = Parser.fragment(html, owner_doc: @backend_doc)
|
|
828
890
|
removed = []
|
|
829
891
|
added = fragment.children.to_a
|
|
830
892
|
body_node = body.__dommy_backend_node__
|
|
@@ -851,14 +913,22 @@ module Dommy
|
|
|
851
913
|
__js_set__(key.to_s, value)
|
|
852
914
|
end
|
|
853
915
|
|
|
854
|
-
# Create a Comment node. Wraps the
|
|
916
|
+
# Create a Comment node. Wraps the Makiri comment so it flows
|
|
855
917
|
# through the same wrap_node identity machinery as Element / TextNode.
|
|
856
918
|
def create_comment(text)
|
|
857
919
|
@node_wrapper_cache.create_comment(text)
|
|
858
920
|
end
|
|
859
921
|
|
|
860
922
|
def create_cdata_section(text)
|
|
861
|
-
|
|
923
|
+
# WHATWG: createCDATASection throws NotSupportedError on an HTML document
|
|
924
|
+
# (CDATA sections exist only in XML). This also sidesteps Lexbor's HTML
|
|
925
|
+
# serializer, which can't emit a CDATA node.
|
|
926
|
+
raise DOMException::NotSupportedError, "createCDATASection is not supported on an HTML document" if html_document?
|
|
927
|
+
|
|
928
|
+
str = text.to_s
|
|
929
|
+
raise DOMException::InvalidCharacterError, "CDATA section data must not contain ']]>'" if str.include?("]]>")
|
|
930
|
+
|
|
931
|
+
@node_wrapper_cache.create_cdata_section(str)
|
|
862
932
|
end
|
|
863
933
|
|
|
864
934
|
def create_document_fragment
|
|
@@ -886,10 +956,10 @@ module Dommy
|
|
|
886
956
|
when "fullscreenEnabled"
|
|
887
957
|
true
|
|
888
958
|
when "scrollingElement"
|
|
889
|
-
wrap_node(@
|
|
959
|
+
wrap_node(@backend_doc.at_css("html"))
|
|
890
960
|
when "documentElement"
|
|
891
961
|
# The document's root element — `<html>` for HTML, the actual root for XML.
|
|
892
|
-
wrap_node(@
|
|
962
|
+
wrap_node(@backend_doc.root)
|
|
893
963
|
when "title"
|
|
894
964
|
read_title
|
|
895
965
|
when "cookie"
|
|
@@ -921,11 +991,10 @@ module Dommy
|
|
|
921
991
|
when "lastModified"
|
|
922
992
|
@last_modified || "01/01/1970 00:00:00"
|
|
923
993
|
when "readyState"
|
|
924
|
-
#
|
|
925
|
-
#
|
|
926
|
-
#
|
|
927
|
-
|
|
928
|
-
"complete"
|
|
994
|
+
# "complete" by default (the document is fully parsed before scripts
|
|
995
|
+
# run); an embedder can replay "loading" → "interactive" → "complete"
|
|
996
|
+
# via #__internal_set_ready_state__.
|
|
997
|
+
@ready_state
|
|
929
998
|
when "visibilityState"
|
|
930
999
|
# There's no real viewport/tab; the document is treated as the visible,
|
|
931
1000
|
# foreground page (so `nextRepaint`-style code uses requestAnimationFrame,
|
|
@@ -943,18 +1012,23 @@ module Dommy
|
|
|
943
1012
|
forms
|
|
944
1013
|
when "scripts"
|
|
945
1014
|
scripts
|
|
1015
|
+
when "currentScript"
|
|
1016
|
+
# The <script> currently executing, set by the host around each script
|
|
1017
|
+
# run (see #__internal_set_current_script__); null outside execution.
|
|
1018
|
+
@__current_script__
|
|
946
1019
|
when "images"
|
|
947
1020
|
images
|
|
948
1021
|
when "embeds", "plugins"
|
|
949
1022
|
# Both reflect the same list of <embed> elements.
|
|
950
|
-
HTMLCollection.new { @
|
|
1023
|
+
HTMLCollection.new { @backend_doc.css("embed").map { |n| wrap_node(n) }.compact }
|
|
1024
|
+
when "applets"
|
|
1025
|
+
# `<applet>` was removed from HTML, so this collection is always empty.
|
|
1026
|
+
HTMLCollection.new { [] }
|
|
951
1027
|
when "anchors"
|
|
952
1028
|
# Historically `<a name>` (with a name attribute), not every link.
|
|
953
|
-
HTMLCollection.new { @
|
|
1029
|
+
HTMLCollection.new { @backend_doc.css("a[name]").map { |n| wrap_node(n) }.compact }
|
|
954
1030
|
when "styleSheets"
|
|
955
|
-
|
|
956
|
-
# so `document.styleSheets.length` / iteration don't blow up.
|
|
957
|
-
NodeList.new
|
|
1031
|
+
style_sheets
|
|
958
1032
|
when "children"
|
|
959
1033
|
children
|
|
960
1034
|
when "childNodes"
|
|
@@ -976,7 +1050,7 @@ module Dommy
|
|
|
976
1050
|
when "nodeName"
|
|
977
1051
|
"#document"
|
|
978
1052
|
else
|
|
979
|
-
|
|
1053
|
+
Bridge::ABSENT # unknown property: JS undefined, `in` reports absent
|
|
980
1054
|
end
|
|
981
1055
|
end
|
|
982
1056
|
|
|
@@ -1012,12 +1086,20 @@ module Dommy
|
|
|
1012
1086
|
adoptNode hasFocus getSelection elementFromPoint queryCommandSupported addEventListener
|
|
1013
1087
|
removeEventListener dispatchEvent write writeln open close isEqualNode appendChild
|
|
1014
1088
|
hasChildNodes contains append prepend replaceChildren removeChild insertBefore replaceChild
|
|
1015
|
-
cloneNode normalize
|
|
1089
|
+
cloneNode normalize compareDocumentPosition getRootNode
|
|
1016
1090
|
]
|
|
1017
1091
|
def __js_call__(method, args)
|
|
1018
1092
|
case method
|
|
1093
|
+
when "getRootNode"
|
|
1094
|
+
# A document is its own root (no shadow tree above it), for any options.
|
|
1095
|
+
# Exposing this is load-bearing: React's resource hoisting computes its
|
|
1096
|
+
# "resource root" as `container.getRootNode()` and throws (#446) if the
|
|
1097
|
+
# document lacks it, falling back to the document's null ownerDocument.
|
|
1098
|
+
self
|
|
1019
1099
|
when "hasChildNodes"
|
|
1020
|
-
@
|
|
1100
|
+
@backend_doc.children.any?
|
|
1101
|
+
when "compareDocumentPosition"
|
|
1102
|
+
compare_document_position(args[0])
|
|
1021
1103
|
when "contains"
|
|
1022
1104
|
contains?(args[0])
|
|
1023
1105
|
when "isEqualNode"
|
|
@@ -1129,6 +1211,36 @@ module Dommy
|
|
|
1129
1211
|
@default_view
|
|
1130
1212
|
end
|
|
1131
1213
|
|
|
1214
|
+
# Replay a document-lifecycle transition: set readyState, fire
|
|
1215
|
+
# `readystatechange`, then the milestone event for the new state —
|
|
1216
|
+
# `DOMContentLoaded` (bubbles, dispatched on the document) when it becomes
|
|
1217
|
+
# "interactive", and `load` (on the window) when it becomes "complete". Lets
|
|
1218
|
+
# an embedder drive code that waits on the document lifecycle (Stimulus /
|
|
1219
|
+
# Turbo startup, jQuery `ready`, …). No-op when already in `state`.
|
|
1220
|
+
def __internal_set_ready_state__(state)
|
|
1221
|
+
state = state.to_s
|
|
1222
|
+
return if @ready_state == state
|
|
1223
|
+
|
|
1224
|
+
@ready_state = state
|
|
1225
|
+
dispatch_event(Event.new("readystatechange"))
|
|
1226
|
+
case state
|
|
1227
|
+
when "interactive"
|
|
1228
|
+
dispatch_event(Event.new("DOMContentLoaded", "bubbles" => true))
|
|
1229
|
+
when "complete"
|
|
1230
|
+
@default_view&.dispatch_event(Event.new("load"))
|
|
1231
|
+
end
|
|
1232
|
+
nil
|
|
1233
|
+
end
|
|
1234
|
+
|
|
1235
|
+
# Set `document.currentScript` to the <script> element being executed (and
|
|
1236
|
+
# back to nil afterward). The host (script boot) brackets each classic
|
|
1237
|
+
# script run with this so code reading `document.currentScript` sees its own
|
|
1238
|
+
# element, matching browser behavior.
|
|
1239
|
+
def __internal_set_current_script__(element)
|
|
1240
|
+
@__current_script__ = element
|
|
1241
|
+
nil
|
|
1242
|
+
end
|
|
1243
|
+
|
|
1132
1244
|
# Delegate node wrapping to NodeWrapperCache
|
|
1133
1245
|
def wrap_node(node)
|
|
1134
1246
|
@node_wrapper_cache.wrap(node)
|
|
@@ -1159,6 +1271,12 @@ module Dommy
|
|
|
1159
1271
|
@shadow_registry.find_enclosing(node)
|
|
1160
1272
|
end
|
|
1161
1273
|
|
|
1274
|
+
# Every ShadowRoot attached in this document — the cascade collects each
|
|
1275
|
+
# one's <style> sheets and scopes them to that shadow tree.
|
|
1276
|
+
def __internal_all_shadow_roots__
|
|
1277
|
+
@shadow_registry.all
|
|
1278
|
+
end
|
|
1279
|
+
|
|
1162
1280
|
# Lifecycle callback dispatchers. Errors raised inside user
|
|
1163
1281
|
# callbacks are swallowed so a single buggy custom element can't
|
|
1164
1282
|
# break the whole mutation pipeline.
|
|
@@ -1273,6 +1391,16 @@ module Dommy
|
|
|
1273
1391
|
@node_wrapper_cache.query_selector_all(selector)
|
|
1274
1392
|
end
|
|
1275
1393
|
|
|
1394
|
+
# `document.styleSheets` — the CSSStyleSheet of each <style> and
|
|
1395
|
+
# <link rel=stylesheet> in document order (CSSOM). Computed on access so
|
|
1396
|
+
# it reflects the current tree.
|
|
1397
|
+
def style_sheets
|
|
1398
|
+
sheets = query_selector_all("style, link").filter_map do |element|
|
|
1399
|
+
element.sheet if element.respond_to?(:sheet)
|
|
1400
|
+
end
|
|
1401
|
+
NodeList.new(sheets)
|
|
1402
|
+
end
|
|
1403
|
+
|
|
1276
1404
|
def get_element_by_id(id)
|
|
1277
1405
|
@node_wrapper_cache.get_element_by_id(id)
|
|
1278
1406
|
end
|
|
@@ -1301,25 +1429,38 @@ module Dommy
|
|
|
1301
1429
|
|
|
1302
1430
|
private
|
|
1303
1431
|
|
|
1304
|
-
# Build a Nokogiri copy of the given node inside our @
|
|
1432
|
+
# Build a Nokogiri copy of the given node inside our @backend_doc.
|
|
1305
1433
|
# `deep: true` recurses into children. Used by importNode and
|
|
1306
1434
|
# adoptNode for cross-document transfer.
|
|
1307
1435
|
def clone_into_doc(source, deep)
|
|
1308
1436
|
copy = if source.element?
|
|
1309
|
-
new_el = Backend.create_element(source.name, @
|
|
1437
|
+
new_el = Backend.create_element(source.name, @backend_doc)
|
|
1310
1438
|
Backend.attribute_nodes(source).each { |a| new_el[a.name] = a.value }
|
|
1311
1439
|
new_el
|
|
1312
1440
|
elsif source.text?
|
|
1313
|
-
Backend.create_text(source.content, @
|
|
1441
|
+
Backend.create_text(source.content, @backend_doc)
|
|
1314
1442
|
elsif source.is_a?(Backend.comment_class)
|
|
1315
|
-
Backend.create_comment(source.content, @
|
|
1443
|
+
Backend.create_comment(source.content, @backend_doc)
|
|
1444
|
+
elsif source.is_a?(Backend.document_fragment_class)
|
|
1445
|
+
# A DocumentFragment clones to a fragment (its children are appended by
|
|
1446
|
+
# the deep pass below), NOT to its first child — `importNode(<template>
|
|
1447
|
+
# .content, true)` must return a fragment so `.firstElementChild` works
|
|
1448
|
+
# (Vue/Alpine x-for clone template content this way). Built via the
|
|
1449
|
+
# document's own `fragment` (as TemplateContentRegistry does) rather than
|
|
1450
|
+
# `document_fragment_class.new`, so it works on backends whose fragment
|
|
1451
|
+
# class isn't directly instantiable (Makiri).
|
|
1452
|
+
@backend_doc.fragment("")
|
|
1316
1453
|
else
|
|
1317
1454
|
# Fallback: serialize + reparse via fragment for unusual types.
|
|
1318
|
-
fragment = Parser.fragment(source.to_html, owner_doc: @
|
|
1319
|
-
fragment.children.first || Backend.create_text("", @
|
|
1455
|
+
fragment = Parser.fragment(source.to_html, owner_doc: @backend_doc)
|
|
1456
|
+
fragment.children.first || Backend.create_text("", @backend_doc)
|
|
1320
1457
|
end
|
|
1321
1458
|
|
|
1322
|
-
if
|
|
1459
|
+
if source.element? && source.name == "template"
|
|
1460
|
+
# A <template>'s contents live in a separate content fragment, not its
|
|
1461
|
+
# child list, so the generic deep pass over `children` misses them.
|
|
1462
|
+
clone_template_content(source, copy) if deep
|
|
1463
|
+
elsif deep && source.respond_to?(:children)
|
|
1323
1464
|
source.children.each do |child|
|
|
1324
1465
|
copy.add_child(clone_into_doc(child, true))
|
|
1325
1466
|
end
|
|
@@ -1328,24 +1469,39 @@ module Dommy
|
|
|
1328
1469
|
copy
|
|
1329
1470
|
end
|
|
1330
1471
|
|
|
1472
|
+
# Clone a <template>'s content into a fragment registered as `copy`'s
|
|
1473
|
+
# template content. The source content lives backend-dependently — Makiri
|
|
1474
|
+
# keeps it in a native content fragment, Nokogiri keeps it as direct children
|
|
1475
|
+
# before migration and in the registry after — so source it from the registry
|
|
1476
|
+
# fragment when migrated, else from Backend.template_content_nodes.
|
|
1477
|
+
def clone_template_content(source, copy)
|
|
1478
|
+
src_frag = @template_content_registry.raw_fragment_for(source)
|
|
1479
|
+
content_nodes = src_frag ? src_frag.children.to_a : Backend.template_content_nodes(source)
|
|
1480
|
+
return if content_nodes.empty?
|
|
1481
|
+
|
|
1482
|
+
frag = @backend_doc.fragment("")
|
|
1483
|
+
content_nodes.each { |n| frag.add_child(clone_into_doc(n, true)) }
|
|
1484
|
+
@template_content_registry.store(copy, frag)
|
|
1485
|
+
end
|
|
1486
|
+
|
|
1331
1487
|
def read_title
|
|
1332
|
-
head = @
|
|
1488
|
+
head = @backend_doc.at_css("head")
|
|
1333
1489
|
title = head&.at_css("title")
|
|
1334
1490
|
title ? title.text : ""
|
|
1335
1491
|
end
|
|
1336
1492
|
|
|
1337
1493
|
def write_title(value)
|
|
1338
|
-
head = @
|
|
1494
|
+
head = @backend_doc.at_css("head")
|
|
1339
1495
|
return unless head
|
|
1340
1496
|
|
|
1341
1497
|
title = head.at_css("title")
|
|
1342
1498
|
unless title
|
|
1343
|
-
title = Backend.create_element("title", @
|
|
1499
|
+
title = Backend.create_element("title", @backend_doc)
|
|
1344
1500
|
head.add_child(title)
|
|
1345
1501
|
end
|
|
1346
1502
|
|
|
1347
1503
|
title.children.each(&:unlink)
|
|
1348
|
-
title.add_child(Backend.create_text(value, @
|
|
1504
|
+
title.add_child(Backend.create_text(value, @backend_doc))
|
|
1349
1505
|
end
|
|
1350
1506
|
|
|
1351
1507
|
end
|
|
@@ -1384,6 +1540,8 @@ module Dommy
|
|
|
1384
1540
|
@ready
|
|
1385
1541
|
when "updateCallbackDone"
|
|
1386
1542
|
@update_callback_done
|
|
1543
|
+
else
|
|
1544
|
+
Bridge::ABSENT
|
|
1387
1545
|
end
|
|
1388
1546
|
end
|
|
1389
1547
|
|