dommy 0.5.0 → 0.7.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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +31 -13
  3. data/lib/dommy/animation.rb +288 -0
  4. data/lib/dommy/attr.rb +23 -11
  5. data/lib/dommy/backend/nokogiri_adapter.rb +51 -0
  6. data/lib/dommy/backend/nokolexbor_adapter.rb +80 -0
  7. data/lib/dommy/backend.rb +129 -0
  8. data/lib/dommy/blob.rb +2 -2
  9. data/lib/dommy/compression_streams.rb +147 -0
  10. data/lib/dommy/cookie_store.rb +128 -0
  11. data/lib/dommy/crypto.rb +396 -0
  12. data/lib/dommy/css.rb +7 -7
  13. data/lib/dommy/custom_elements.rb +6 -6
  14. data/lib/dommy/document.rb +190 -32
  15. data/lib/dommy/dom_parser.rb +5 -4
  16. data/lib/dommy/element.rb +356 -53
  17. data/lib/dommy/event.rb +431 -25
  18. data/lib/dommy/event_source.rb +131 -0
  19. data/lib/dommy/fetch.rb +76 -6
  20. data/lib/dommy/file_reader.rb +176 -0
  21. data/lib/dommy/form_data.rb +1 -3
  22. data/lib/dommy/history.rb +82 -0
  23. data/lib/dommy/html_collection.rb +4 -4
  24. data/lib/dommy/html_elements.rb +130 -67
  25. data/lib/dommy/internal/cookie_jar.rb +2 -0
  26. data/lib/dommy/internal/css_pseudo_handlers.rb +28 -0
  27. data/lib/dommy/internal/dom_matching.rb +4 -4
  28. data/lib/dommy/internal/idna.rb +443 -0
  29. data/lib/dommy/internal/idna_data.rb +10379 -0
  30. data/lib/dommy/internal/ipv4_parser.rb +78 -0
  31. data/lib/dommy/internal/node_traversal.rb +1 -1
  32. data/lib/dommy/internal/node_wrapper_cache.rb +23 -12
  33. data/lib/dommy/internal/observable_callback.rb +25 -0
  34. data/lib/dommy/internal/punycode.rb +202 -0
  35. data/lib/dommy/internal/range_text_serializer.rb +72 -0
  36. data/lib/dommy/internal/reflected_attributes.rb +45 -0
  37. data/lib/dommy/internal/template_content_registry.rb +6 -6
  38. data/lib/dommy/intersection_observer.rb +82 -0
  39. data/lib/dommy/{router.rb → location.rb} +8 -142
  40. data/lib/dommy/media_query_list.rb +118 -0
  41. data/lib/dommy/message_channel.rb +249 -0
  42. data/lib/dommy/{observer.rb → mutation_observer.rb} +21 -11
  43. data/lib/dommy/navigator.rb +365 -5
  44. data/lib/dommy/node.rb +12 -0
  45. data/lib/dommy/notification.rb +89 -0
  46. data/lib/dommy/parser.rb +13 -13
  47. data/lib/dommy/performance.rb +146 -0
  48. data/lib/dommy/performance_observer.rb +55 -0
  49. data/lib/dommy/range.rb +597 -0
  50. data/lib/dommy/resize_observer.rb +53 -0
  51. data/lib/dommy/shadow_root.rb +10 -8
  52. data/lib/dommy/streams.rb +386 -0
  53. data/lib/dommy/svg_elements.rb +3863 -0
  54. data/lib/dommy/text_codec.rb +175 -0
  55. data/lib/dommy/tree_walker.rb +21 -21
  56. data/lib/dommy/url.rb +274 -29
  57. data/lib/dommy/url_pattern.rb +144 -0
  58. data/lib/dommy/version.rb +1 -1
  59. data/lib/dommy/web_socket.rb +209 -0
  60. data/lib/dommy/window.rb +369 -0
  61. data/lib/dommy/worker.rb +143 -0
  62. data/lib/dommy/xml_http_request.rb +438 -0
  63. data/lib/dommy.rb +43 -5
  64. metadata +44 -29
  65. data/lib/dommy/world.rb +0 -209
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dommy
4
+ # `Worker` — inline-emulated. Dommy does NOT spin up a separate
5
+ # execution context (no JS engine, no Ruby Thread). Instead:
6
+ #
7
+ # - `new Worker("/path/to/worker.js")` records the URL.
8
+ # - The script body is not executed. Tests install message
9
+ # handlers on the worker-side via `worker.__test_on_message__ { ... }`
10
+ # to simulate behavior.
11
+ # - `worker.postMessage(data)` queues a microtask that delivers
12
+ # to the worker-side handler.
13
+ # - The worker-side handler can call `worker.__test_post_to_main__(data)`
14
+ # to deliver a message back to the main side's `message` event.
15
+ #
16
+ # This is enough surface to test "the app correctly posts/receives
17
+ # via Worker" without a real worker runtime.
18
+ #
19
+ # Spec (real): https://html.spec.whatwg.org/multipage/workers.html
20
+ class Worker
21
+ include EventTarget
22
+
23
+ attr_reader :url
24
+
25
+ def initialize(window, url, _options = nil)
26
+ @window = window
27
+ @url = url.to_s
28
+ @inline_handlers = {}
29
+ @worker_side_handlers = []
30
+ @terminated = false
31
+ end
32
+
33
+ # Main-side: post a message to the worker.
34
+ def post_message(data)
35
+ return if @terminated
36
+
37
+ cloned = Dommy.structured_clone(data)
38
+ @window.scheduler.queue_microtask(
39
+ proc do
40
+ @worker_side_handlers.each { |h| invoke(h, [{"data" => cloned}]) }
41
+ end
42
+ )
43
+
44
+ nil
45
+ end
46
+
47
+ alias postMessage post_message
48
+
49
+ def terminate
50
+ @terminated = true
51
+ @worker_side_handlers.clear
52
+ nil
53
+ end
54
+
55
+ # --- Test seams (worker-side) ----------------------------------
56
+
57
+ # Register a callback that runs in the "worker side". Multiple
58
+ # registrations stack.
59
+ def __test_on_message__(&block)
60
+ @worker_side_handlers << block
61
+ end
62
+
63
+ # Worker-side: deliver a message to the main-side `message` event.
64
+ def __test_post_to_main__(data)
65
+ cloned = Dommy.structured_clone(data)
66
+ @window.scheduler.queue_microtask(
67
+ proc do
68
+ dispatch_event(MessageEvent.new("message", "data" => cloned))
69
+ end
70
+ )
71
+
72
+ nil
73
+ end
74
+
75
+ # --- JS bridge -------------------------------------------------
76
+
77
+ def __js_get__(key)
78
+ case key
79
+ when "url"
80
+ @url
81
+ else
82
+ @inline_handlers[inline_event_for(key)]
83
+ end
84
+ end
85
+
86
+ def __js_set__(key, value)
87
+ event = inline_event_for(key)
88
+ set_inline_handler(event, value) if event
89
+ nil
90
+ end
91
+
92
+ def __js_call__(method, args)
93
+ case method
94
+ when "postMessage"
95
+ post_message(args[0])
96
+ when "terminate"
97
+ terminate
98
+ when "addEventListener"
99
+ add_event_listener(args[0], args[1], args[2])
100
+ when "removeEventListener"
101
+ remove_event_listener(args[0], args[1])
102
+ when "dispatchEvent"
103
+ dispatch_event(args[0])
104
+ end
105
+ end
106
+
107
+ def __internal_event_parent__
108
+ nil
109
+ end
110
+
111
+ private
112
+
113
+ INLINE_HANDLERS = %w[message error messageerror].freeze
114
+ INLINE_EVENT_MAP = INLINE_HANDLERS
115
+ .each_with_object({}) do |name, h|
116
+ h["on#{name}"] = name
117
+ end
118
+ .freeze
119
+
120
+ def inline_event_for(key)
121
+ INLINE_EVENT_MAP[key.to_s]
122
+ end
123
+
124
+ def set_inline_handler(event, handler)
125
+ previous = @inline_handlers[event]
126
+ remove_event_listener(event, previous) if previous
127
+ if handler.nil?
128
+ @inline_handlers.delete(event)
129
+ else
130
+ add_event_listener(event, handler)
131
+ @inline_handlers[event] = handler
132
+ end
133
+ end
134
+
135
+ def invoke(callback, args)
136
+ if callback.respond_to?(:__js_call__)
137
+ callback.__js_call__("call", args)
138
+ elsif callback.respond_to?(:call)
139
+ callback.call(*args)
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,438 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Dommy
6
+ # `XMLHttpRequest` polyfill. Consults the same stub maps that
7
+ # `FetchFn` reads (`__fetchy_stub__` / `__resource_fetch_stub__` /
8
+ # `__inject_fetch_stub__`) so a single set of fixtures drives both
9
+ # `fetch(...)` and `new XMLHttpRequest()` style code.
10
+ #
11
+ # State transitions match the spec:
12
+ # UNSENT(0) → OPENED(1) → HEADERS_RECEIVED(2) → LOADING(3) → DONE(4)
13
+ # Each transition fires `readystatechange`. `load` / `loadend` fire
14
+ # on completion; `error` / `timeout` / `abort` fire on the
15
+ # respective failure paths.
16
+ #
17
+ # Async requests resolve via the scheduler (a microtask, or a
18
+ # `setTimeout` for stubs with `delay:`); sync requests
19
+ # (`open(..., false)`) deliver inline so tests can read
20
+ # `xhr.responseText` immediately.
21
+ #
22
+ # Spec: https://xhr.spec.whatwg.org/
23
+ class XMLHttpRequest
24
+ include EventTarget
25
+
26
+ UNSENT = 0
27
+ OPENED = 1
28
+ HEADERS_RECEIVED = 2
29
+ LOADING = 3
30
+ DONE = 4
31
+
32
+ INLINE_HANDLERS = %w[
33
+ readystatechange
34
+ loadstart
35
+ load
36
+ loadend
37
+ progress
38
+ error
39
+ timeout
40
+ abort
41
+ ].freeze
42
+
43
+ attr_reader(
44
+ :ready_state,
45
+ :status,
46
+ :status_text,
47
+ :response_url,
48
+ :response_text,
49
+ :response_xml,
50
+ :response,
51
+ :upload
52
+ )
53
+
54
+ attr_accessor :timeout, :with_credentials, :response_type
55
+
56
+ def initialize(window)
57
+ @window = window
58
+ @timeout = 0
59
+ @with_credentials = false
60
+ @response_type = ""
61
+ @generation = 0
62
+ reset_state
63
+ @inline_handlers = {}
64
+ @upload = XMLHttpRequestUpload.new
65
+ end
66
+
67
+ # XHR §open. `method` is uppercased; `async` defaults to true.
68
+ def open(method, url, async = true, _user = nil, _password = nil)
69
+ reset_state
70
+ @method = method.to_s.upcase
71
+ @url = url.to_s
72
+ @async = async.nil? ? true : !!async
73
+ @request_headers = {}
74
+ transition(OPENED)
75
+ nil
76
+ end
77
+
78
+ def set_request_header(name, value)
79
+ raise Error, "setRequestHeader called before open" if @ready_state != OPENED
80
+
81
+ key = name.to_s
82
+ existing = @request_headers[key]
83
+ @request_headers[key] = existing ? "#{existing}, #{value}" : value.to_s
84
+ nil
85
+ end
86
+
87
+ alias setRequestHeader set_request_header
88
+
89
+ def send(body = nil)
90
+ raise Error, "send called before open" if @ready_state != OPENED
91
+
92
+ @request_body = body
93
+ @sent = true
94
+ @generation += 1
95
+ gen = @generation
96
+ dispatch_event(ProgressEvent.new("loadstart"))
97
+
98
+ entry = lookup_stub
99
+ track_globals
100
+
101
+ if entry.nil?
102
+ deliver(body: "not found", status: 404, status_text: "Not Found", headers: {})
103
+ return nil
104
+ end
105
+
106
+ delay = entry["delay"]
107
+ if delay && @async
108
+ schedule_delivery_with_delay(entry, delay.to_i, gen)
109
+ elsif @async
110
+ @window.scheduler.queue_microtask(proc { deliver_entry(entry) if active?(gen) })
111
+ else
112
+ deliver_entry(entry)
113
+ end
114
+
115
+ nil
116
+ end
117
+
118
+ def abort
119
+ return if @ready_state == UNSENT || @ready_state == DONE
120
+ # WHATWG: abort() is a no-op when in OPENED with the send()
121
+ # flag unset. Without this guard, `xhr.open(); xhr.abort()`
122
+ # would fire abort + loadend even though no request is
123
+ # in flight.
124
+ return if @ready_state == OPENED && !@sent
125
+
126
+ @aborted = true
127
+ @generation += 1
128
+ @status = 0
129
+ @status_text = ""
130
+ transition(DONE)
131
+ dispatch_event(ProgressEvent.new("abort"))
132
+ dispatch_event(ProgressEvent.new("loadend"))
133
+ reset_state(keep_handlers: true, keep_generation: true)
134
+ nil
135
+ end
136
+
137
+ def get_response_header(name)
138
+ return nil if @ready_state < HEADERS_RECEIVED
139
+
140
+ key = name.to_s.downcase
141
+ hit = @response_headers.find { |k, _| k.to_s.downcase == key }
142
+ hit ? hit.last : nil
143
+ end
144
+
145
+ alias getResponseHeader get_response_header
146
+
147
+ def get_all_response_headers
148
+ return "" if @ready_state < HEADERS_RECEIVED
149
+
150
+ @response_headers.map { |k, v| "#{k}: #{v}\r\n" }.join
151
+ end
152
+
153
+ alias getAllResponseHeaders get_all_response_headers
154
+
155
+ def override_mime_type(mime)
156
+ @override_mime = mime.to_s
157
+ nil
158
+ end
159
+
160
+ alias overrideMimeType override_mime_type
161
+
162
+ # --- JS bridge ---------------------------------------------------
163
+
164
+ def __js_get__(key)
165
+ case key
166
+ when "readyState"
167
+ @ready_state
168
+ when "status"
169
+ @status
170
+ when "statusText"
171
+ @status_text
172
+ when "responseURL"
173
+ @response_url
174
+ when "response"
175
+ @response
176
+ when "responseText"
177
+ @response_text
178
+ when "responseXML"
179
+ @response_xml
180
+ when "responseType"
181
+ @response_type
182
+ when "timeout"
183
+ @timeout
184
+ when "withCredentials"
185
+ @with_credentials
186
+ when "upload"
187
+ @upload
188
+ when "UNSENT"
189
+ UNSENT
190
+ when "OPENED"
191
+ OPENED
192
+ when "HEADERS_RECEIVED"
193
+ HEADERS_RECEIVED
194
+ when "LOADING"
195
+ LOADING
196
+ when "DONE"
197
+ DONE
198
+ else
199
+ @inline_handlers[inline_event_for(key)]
200
+ end
201
+ end
202
+
203
+ def __js_set__(key, value)
204
+ case key
205
+ when "responseType"
206
+ @response_type = value.to_s
207
+ when "timeout"
208
+ @timeout = value.to_i
209
+ when "withCredentials"
210
+ @with_credentials = !!value
211
+ else
212
+ event = inline_event_for(key)
213
+ set_inline_handler(event, value) if event
214
+ end
215
+
216
+ nil
217
+ end
218
+
219
+ def __js_call__(method, args)
220
+ case method
221
+ when "open"
222
+ open(args[0], args[1], args[2].nil? ? true : args[2], args[3], args[4])
223
+ when "send"
224
+ send(args[0])
225
+ when "setRequestHeader"
226
+ set_request_header(args[0], args[1])
227
+ when "abort"
228
+ abort
229
+ when "getResponseHeader"
230
+ get_response_header(args[0])
231
+ when "getAllResponseHeaders"
232
+ get_all_response_headers
233
+ when "overrideMimeType"
234
+ override_mime_type(args[0])
235
+ when "addEventListener"
236
+ add_event_listener(args[0], args[1], args[2])
237
+ when "removeEventListener"
238
+ remove_event_listener(args[0], args[1])
239
+ when "dispatchEvent"
240
+ dispatch_event(args[0])
241
+ end
242
+ end
243
+
244
+ def __internal_event_parent__
245
+ nil
246
+ end
247
+
248
+ class Error < StandardError
249
+ end
250
+
251
+ private
252
+
253
+ def reset_state(keep_handlers: false, keep_generation: false)
254
+ @ready_state = UNSENT
255
+ @status = 0
256
+ @status_text = ""
257
+ @response_url = ""
258
+ @response = nil
259
+ @response_text = ""
260
+ @response_xml = nil
261
+ @response_headers = {}
262
+ @request_headers = {}
263
+ @aborted = false unless keep_generation
264
+ @sent = false
265
+ @override_mime = nil
266
+ @method = nil
267
+ @url = nil
268
+ @async = true
269
+ @inline_handlers = {} unless keep_handlers
270
+ @generation = 0 unless keep_generation
271
+ end
272
+
273
+ # A queued delivery is "active" only if no abort / reopen has
274
+ # bumped the generation since its send() call.
275
+ def active?(gen)
276
+ @generation == gen && !@aborted
277
+ end
278
+
279
+ def transition(state)
280
+ @ready_state = state
281
+ dispatch_event(Event.new("readystatechange"))
282
+ end
283
+
284
+ def lookup_stub
285
+ stub_map = @window.globals["__fetchy_stub__"] ||
286
+ @window.globals["__resource_fetch_stub__"] ||
287
+ @window.globals["__inject_fetch_stub__"]
288
+ return nil unless stub_map.is_a?(Hash)
289
+
290
+ stub_map[@url]
291
+ end
292
+
293
+ # Bookkeeping that lets specs assert "fetch was called N times"
294
+ # uniformly across `fetch` and XHR (`FetchFn` writes the same
295
+ # globals; XHR mirrors them).
296
+ def track_globals
297
+ @window.globals["__fetch_count__"] = (@window.globals["__fetch_count__"] || 0).to_i + 1
298
+ @window.globals["__last_url__"] = @url
299
+ @window.globals["__last_method__"] = @method
300
+ @window.globals["__last_body__"] = @request_body
301
+ end
302
+
303
+ def schedule_delivery_with_delay(entry, delay_ms, gen)
304
+ timer = @window.scheduler.set_timeout(
305
+ proc { deliver_entry(entry) if active?(gen) },
306
+ delay_ms
307
+ )
308
+
309
+ return unless @timeout.to_i.positive?
310
+
311
+ @window.scheduler.set_timeout(
312
+ proc {
313
+ next unless active?(gen)
314
+ next if @ready_state == DONE
315
+
316
+ @window.scheduler.clear_timeout(timer)
317
+ fail_with("timeout")
318
+ },
319
+ @timeout
320
+ )
321
+ end
322
+
323
+ def deliver_entry(entry)
324
+ body = entry["body"].to_s
325
+ status = (entry["status"] || 200).to_i
326
+ status_text = entry["statusText"] || ""
327
+ headers = entry["headers"] || {"Content-Type" => entry["contentType"] || "text/plain"}
328
+ deliver(body: body, status: status, status_text: status_text, headers: headers)
329
+ end
330
+
331
+ def deliver(body:, status:, status_text:, headers:)
332
+ return if @aborted
333
+
334
+ @status = status
335
+ @status_text = status_text
336
+ @response_headers = headers
337
+ @response_url = @url
338
+ @response_text = body
339
+ @response = decode_response(body)
340
+
341
+ transition(HEADERS_RECEIVED)
342
+ transition(LOADING)
343
+ transition(DONE)
344
+ dispatch_event(ProgressEvent.new("load"))
345
+ dispatch_event(ProgressEvent.new("loadend"))
346
+ end
347
+
348
+ def fail_with(reason)
349
+ @status = 0
350
+ @status_text = ""
351
+ transition(DONE)
352
+ dispatch_event(ProgressEvent.new(reason))
353
+ dispatch_event(ProgressEvent.new("loadend"))
354
+ end
355
+
356
+ # Decode the body into `response` per `responseType`.
357
+ def decode_response(body)
358
+ case @response_type
359
+ when "", "text"
360
+ body
361
+ when "json"
362
+ begin
363
+ JSON.parse(body)
364
+ rescue JSON::ParserError
365
+ nil
366
+ end
367
+
368
+ when "arraybuffer"
369
+ body.bytes
370
+ when "blob"
371
+ Blob.new([body], "type" => response_content_type)
372
+ when "document"
373
+ parse_document(body)
374
+ else
375
+ body
376
+ end
377
+ end
378
+
379
+ # Content-Type of the received response, read straight from
380
+ # `@response_headers` (not `get_response_header`, which gates on
381
+ # `readyState` — decode runs before that flag advances).
382
+ def response_content_type
383
+ hit = @response_headers.find { |k, _| k.to_s.downcase == "content-type" }
384
+ hit ? hit.last.to_s : ""
385
+ end
386
+
387
+ def parse_document(body)
388
+ DOMParser.new.parse_from_string(body, "text/html")
389
+ rescue StandardError
390
+ nil
391
+ end
392
+
393
+ INLINE_EVENT_MAP = INLINE_HANDLERS
394
+ .each_with_object({}) do |name, h|
395
+ h["on#{name}"] = name
396
+ end
397
+ .freeze
398
+
399
+ def inline_event_for(key)
400
+ INLINE_EVENT_MAP[key.to_s]
401
+ end
402
+
403
+ def set_inline_handler(event, handler)
404
+ previous = @inline_handlers[event]
405
+ remove_event_listener(event, previous) if previous
406
+
407
+ if handler.nil?
408
+ @inline_handlers.delete(event)
409
+ else
410
+ add_event_listener(event, handler)
411
+ @inline_handlers[event] = handler
412
+ end
413
+ end
414
+ end
415
+
416
+ # `XMLHttpRequestUpload` — the upload-side event target. Real
417
+ # browsers fire `progress` here while uploading multipart bodies;
418
+ # dommy doesn't simulate upload, so this is an inert EventTarget
419
+ # the caller can still `addEventListener` against.
420
+ class XMLHttpRequestUpload
421
+ include EventTarget
422
+
423
+ def __js_call__(method, args)
424
+ case method
425
+ when "addEventListener"
426
+ add_event_listener(args[0], args[1], args[2])
427
+ when "removeEventListener"
428
+ remove_event_listener(args[0], args[1])
429
+ when "dispatchEvent"
430
+ dispatch_event(args[0])
431
+ end
432
+ end
433
+
434
+ def __internal_event_parent__
435
+ nil
436
+ end
437
+ end
438
+ end
data/lib/dommy.rb CHANGED
@@ -1,29 +1,58 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "nokogiri"
4
3
  require "set"
5
4
 
6
5
  require_relative "dommy/version"
6
+ require_relative "dommy/backend"
7
7
  require_relative "dommy/dom_exception"
8
8
  require_relative "dommy/node"
9
9
  require_relative "dommy/html_collection"
10
10
  require_relative "dommy/event"
11
11
  require_relative "dommy/scheduler"
12
- require_relative "dommy/observer"
12
+ require_relative "dommy/mutation_observer"
13
13
  require_relative "dommy/promise"
14
14
  require_relative "dommy/blob"
15
15
  require_relative "dommy/data_transfer"
16
+ require_relative "dommy/crypto"
17
+ require_relative "dommy/text_codec"
18
+ require_relative "dommy/internal/observable_callback"
19
+ require_relative "dommy/internal/css_pseudo_handlers"
20
+ require_relative "dommy/internal/punycode"
21
+ require_relative "dommy/internal/idna"
22
+ require_relative "dommy/internal/ipv4_parser"
23
+ require_relative "dommy/intersection_observer"
24
+ require_relative "dommy/resize_observer"
25
+ require_relative "dommy/performance_observer"
26
+ require_relative "dommy/internal/range_text_serializer"
27
+ require_relative "dommy/range"
28
+ require_relative "dommy/animation"
16
29
  require_relative "dommy/bridge"
17
30
  require_relative "dommy/storage"
18
31
  require_relative "dommy/fetch"
19
- require_relative "dommy/router"
32
+ require_relative "dommy/xml_http_request"
33
+ require_relative "dommy/file_reader"
34
+ require_relative "dommy/media_query_list"
35
+ require_relative "dommy/notification"
36
+ require_relative "dommy/message_channel"
37
+ require_relative "dommy/web_socket"
38
+ require_relative "dommy/event_source"
39
+ require_relative "dommy/performance"
40
+ require_relative "dommy/cookie_store"
41
+ require_relative "dommy/url_pattern"
42
+ require_relative "dommy/streams"
43
+ require_relative "dommy/compression_streams"
44
+ require_relative "dommy/worker"
45
+ require_relative "dommy/location"
46
+ require_relative "dommy/history"
20
47
  require_relative "dommy/navigator"
21
48
  require_relative "dommy/parser"
22
49
  require_relative "dommy/attr"
23
- require_relative "dommy/world"
50
+ require_relative "dommy/window"
24
51
  require_relative "dommy/document"
25
52
  require_relative "dommy/element"
53
+ require_relative "dommy/internal/reflected_attributes"
26
54
  require_relative "dommy/html_elements"
55
+ require_relative "dommy/svg_elements"
27
56
  require_relative "dommy/shadow_root"
28
57
  require_relative "dommy/custom_elements"
29
58
  require_relative "dommy/tree_walker"
@@ -45,7 +74,7 @@ module Dommy
45
74
  def self.parse(html)
46
75
  s = html.to_s
47
76
  if s.match?(/\A\s*(<!doctype|<html\b)/i)
48
- Window.new(nil, nokogiri_doc: Nokogiri::HTML5(s))
77
+ Window.new(nil, nokogiri_doc: Backend.parse(s))
49
78
  else
50
79
  window = Window.new
51
80
  window.document.body.inner_html = s
@@ -53,6 +82,15 @@ module Dommy
53
82
  end
54
83
  end
55
84
 
85
+ # Convenience accessor: `Dommy.backend` / `Dommy.backend=`.
86
+ def self.backend
87
+ Backend.current
88
+ end
89
+
90
+ def self.backend=(new_backend)
91
+ Backend.current = new_backend
92
+ end
93
+
56
94
  # Build a fresh, empty Window (no host). Equivalent to opening a
57
95
  # blank document.
58
96
  def self.new_window