dommy 0.14.0 → 0.16.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 +15 -1
- data/lib/dommy/attr.rb +56 -32
- data/lib/dommy/backend.rb +251 -208
- data/lib/dommy/blob.rb +24 -0
- data/lib/dommy/bridge/methods.rb +12 -2
- data/lib/dommy/bridge/values.rb +29 -2
- data/lib/dommy/bridge/wire_tags.rb +8 -4
- data/lib/dommy/browser.rb +328 -53
- data/lib/dommy/callable_invoker.rb +27 -31
- data/lib/dommy/character_data.rb +21 -28
- data/lib/dommy/class_list.rb +55 -23
- data/lib/dommy/cookie_jar.rb +408 -0
- data/lib/dommy/cookie_store.rb +104 -49
- data/lib/dommy/css.rb +37 -8
- data/lib/dommy/custom_elements.rb +360 -116
- data/lib/dommy/data_transfer.rb +22 -2
- data/lib/dommy/document.rb +1258 -346
- data/lib/dommy/dom_exception.rb +6 -0
- data/lib/dommy/dom_parser.rb +46 -14
- data/lib/dommy/dom_string_list.rb +36 -0
- data/lib/dommy/element.rb +408 -569
- data/lib/dommy/element_internals.rb +215 -0
- data/lib/dommy/encodings.rb +10 -0
- data/lib/dommy/event.rb +382 -148
- data/lib/dommy/event_source.rb +8 -34
- data/lib/dommy/fetch.rb +8 -12
- data/lib/dommy/file_reader.rb +5 -29
- data/lib/dommy/form_data.rb +53 -16
- data/lib/dommy/form_entry_list.rb +107 -138
- data/lib/dommy/fragment.rb +14 -16
- data/lib/dommy/history.rb +247 -90
- data/lib/dommy/html_canvas_element.rb +15 -35
- data/lib/dommy/html_collection.rb +76 -50
- data/lib/dommy/html_elements/base.rb +377 -71
- data/lib/dommy/html_elements/embedded.rb +350 -106
- data/lib/dommy/html_elements/forms/form.rb +424 -383
- data/lib/dommy/html_elements/forms/input.rb +351 -189
- data/lib/dommy/html_elements/forms/select.rb +97 -81
- data/lib/dommy/html_elements/interactive.rb +285 -72
- data/lib/dommy/html_elements/media.rb +73 -32
- data/lib/dommy/html_elements/metadata.rb +226 -78
- data/lib/dommy/html_elements/miscellaneous.rb +58 -14
- data/lib/dommy/html_elements/tables.rb +20 -42
- data/lib/dommy/html_elements.rb +28 -3
- data/lib/dommy/hyperlink_utils.rb +19 -8
- data/lib/dommy/interaction/drag_and_drop.rb +187 -0
- data/lib/dommy/interaction/event_synthesis.rb +182 -24
- data/lib/dommy/interaction/field_interactor.rb +18 -6
- data/lib/dommy/interaction/form_submission.rb +12 -6
- data/lib/dommy/interaction/key_sender.rb +102 -17
- data/lib/dommy/interaction.rb +1 -0
- data/lib/dommy/internal/accessibility_tree.rb +19 -7
- data/lib/dommy/internal/accessibility_visibility.rb +2 -2
- data/lib/dommy/internal/accessible_description.rb +3 -3
- data/lib/dommy/internal/accessible_name.rb +33 -45
- data/lib/dommy/internal/an_plus_b.rb +125 -0
- data/lib/dommy/internal/aria_role.rb +27 -18
- data/lib/dommy/internal/aria_state.rb +7 -7
- data/lib/dommy/internal/autofill.rb +2 -2
- data/lib/dommy/internal/backend_prefilter.rb +21 -18
- data/lib/dommy/internal/ce_reactions.rb +285 -0
- data/lib/dommy/internal/ce_reactions_table.rb +296 -0
- data/lib/dommy/internal/child_list.rb +39 -0
- data/lib/dommy/internal/child_navigable.rb +155 -0
- data/lib/dommy/internal/child_node.rb +44 -41
- data/lib/dommy/internal/close_watcher_manager.rb +238 -0
- data/lib/dommy/internal/complex_match.rb +129 -0
- data/lib/dommy/internal/constraint_validation.rb +130 -0
- data/lib/dommy/internal/css/ancestor_filter.rb +107 -0
- data/lib/dommy/internal/css/cascade.rb +7 -7
- data/lib/dommy/internal/css/cascaded_declarations.rb +14 -20
- data/lib/dommy/internal/css/computed_style_builder.rb +30 -3
- data/lib/dommy/internal/css/custom_properties.rb +63 -80
- data/lib/dommy/internal/css/parser.rb +53 -63
- data/lib/dommy/internal/css/renderability.rb +6 -0
- data/lib/dommy/internal/css/rule_index.rb +170 -108
- data/lib/dommy/internal/css/rule_index_inputs.rb +135 -0
- data/lib/dommy/internal/css/selector_dependencies.rb +8 -2
- data/lib/dommy/internal/css/style_cache.rb +9 -6
- data/lib/dommy/internal/css/ua_stylesheet.rb +37 -2
- data/lib/dommy/internal/css_source.rb +223 -0
- data/lib/dommy/internal/css_syntax.rb +153 -0
- data/lib/dommy/internal/declarative_shadow_roots.rb +182 -0
- data/lib/dommy/internal/directionality.rb +2 -2
- data/lib/dommy/internal/document_generations.rb +91 -18
- data/lib/dommy/internal/document_interaction_state.rb +293 -9
- data/lib/dommy/internal/dom_matching.rb +7 -7
- data/lib/dommy/internal/element_aria.rb +65 -23
- data/lib/dommy/internal/element_css_inline_style.rb +26 -0
- data/lib/dommy/internal/element_editing.rb +41 -0
- data/lib/dommy/internal/element_nonce.rb +28 -0
- data/lib/dommy/internal/element_popover.rb +310 -0
- data/lib/dommy/internal/element_shadow.rb +107 -33
- data/lib/dommy/internal/element_state.rb +131 -26
- data/lib/dommy/internal/element_top_layer.rb +21 -51
- data/lib/dommy/internal/enumerated_keyword_sets.rb +40 -0
- data/lib/dommy/internal/event_handler_tables.rb +241 -0
- data/lib/dommy/internal/event_handlers.rb +149 -0
- data/lib/dommy/internal/exception_report.rb +1 -1
- data/lib/dommy/internal/focusability.rb +267 -0
- data/lib/dommy/internal/form_associated_custom_elements.rb +127 -0
- data/lib/dommy/internal/html_or_svg_or_mathml_element.rb +86 -0
- data/lib/dommy/internal/html_serialization.rb +292 -0
- data/lib/dommy/internal/idl_reflection.rb +202 -0
- data/lib/dommy/internal/idl_reflection_table.rb +589 -0
- data/lib/dommy/internal/infra.rb +36 -0
- data/lib/dommy/internal/input_type.rb +90 -20
- data/lib/dommy/internal/insertion_point.rb +27 -3
- data/lib/dommy/internal/js_number.rb +60 -0
- data/lib/dommy/internal/light_dismiss.rb +178 -0
- data/lib/dommy/internal/literal_lookup.rb +103 -0
- data/lib/dommy/internal/mime_type.rb +8 -0
- data/lib/dommy/internal/mutation_coordinator.rb +94 -60
- data/lib/dommy/internal/node_adopter.rb +165 -97
- data/lib/dommy/internal/node_factory.rb +111 -49
- data/lib/dommy/internal/node_identity.rb +1 -1
- data/lib/dommy/internal/node_traversal.rb +30 -13
- data/lib/dommy/internal/node_wrapper_cache.rb +34 -148
- data/lib/dommy/internal/observable_callback.rb +2 -5
- data/lib/dommy/internal/observer_options.rb +24 -14
- data/lib/dommy/internal/origin.rb +86 -0
- data/lib/dommy/internal/parent_node.rb +12 -10
- data/lib/dommy/internal/popover_invoker_element.rb +67 -0
- data/lib/dommy/internal/popover_stack.rb +120 -0
- data/lib/dommy/internal/post_insertion_steps.rb +146 -77
- data/lib/dommy/internal/reflected_attributes.rb +173 -43
- data/lib/dommy/internal/rendered_text/collector.rb +12 -9
- data/lib/dommy/internal/rendered_text/fragment.rb +6 -2
- data/lib/dommy/internal/selector_arguments.rb +2 -16
- data/lib/dommy/internal/selector_index.rb +19 -10
- data/lib/dommy/internal/selector_match.rb +190 -0
- data/lib/dommy/internal/selector_matcher.rb +91 -256
- data/lib/dommy/internal/selector_parser.rb +176 -100
- data/lib/dommy/internal/sequential_focus_navigation.rb +210 -0
- data/lib/dommy/internal/shadow_root_registry.rb +14 -6
- data/lib/dommy/internal/sibling_positions.rb +59 -0
- data/lib/dommy/internal/svg_uri_reference.rb +33 -0
- data/lib/dommy/internal/template_content_registry.rb +11 -5
- data/lib/dommy/internal/text_selection.rb +185 -32
- data/lib/dommy/internal/toggle_events.rb +6 -4
- data/lib/dommy/internal/unsafe_html.rb +107 -0
- data/lib/dommy/internal/url_parser.rb +84 -8
- data/lib/dommy/internal/user_activation.rb +192 -0
- data/lib/dommy/internal/web_idl.rb +160 -16
- data/lib/dommy/internal/window_constructors.rb +34 -14
- data/lib/dommy/internal/xml_serialization.rb +9 -45
- data/lib/dommy/js/constructor_resolver.rb +18 -0
- data/lib/dommy/js/custom_element_bridge.rb +57 -82
- data/lib/dommy/js/dom_interfaces.rb +51 -15
- data/lib/dommy/js/host_bridge.rb +189 -40
- data/lib/dommy/js/host_callbacks.rb +10 -0
- data/lib/dommy/js/host_runtime.js +1921 -416
- data/lib/dommy/js/marshaller.rb +13 -4
- data/lib/dommy/js/module_loader.rb +46 -5
- data/lib/dommy/js/module_preload.rb +194 -0
- data/lib/dommy/js/runtime.rb +9 -0
- data/lib/dommy/js/script_boot.rb +200 -114
- data/lib/dommy/js/serialized_record.rb +32 -0
- data/lib/dommy/js/webidl_event_handlers.js +45 -0
- data/lib/dommy/js/webidl_members.js +196 -0
- data/lib/dommy/js/webidl_signatures.js +2055 -0
- data/lib/dommy/js/webidl_tables.js +72 -101
- data/lib/dommy/js/window_wiring.rb +9 -5
- data/lib/dommy/location.rb +129 -76
- data/lib/dommy/mathml_elements.rb +13 -0
- data/lib/dommy/media_query_list.rb +4 -13
- data/lib/dommy/message_channel.rb +137 -64
- data/lib/dommy/mutation_observer.rb +3 -7
- data/lib/dommy/navigation.rb +61 -11
- data/lib/dommy/navigator.rb +132 -10
- data/lib/dommy/node.rb +81 -44
- data/lib/dommy/notification.rb +10 -6
- data/lib/dommy/parser.rb +5 -16
- data/lib/dommy/promise.rb +20 -26
- data/lib/dommy/range.rb +20 -47
- data/lib/dommy/resources.rb +38 -3
- data/lib/dommy/scheduler.rb +176 -48
- data/lib/dommy/shadow_root.rb +125 -25
- data/lib/dommy/storage.rb +135 -25
- data/lib/dommy/streams.rb +2 -6
- data/lib/dommy/structured_data.rb +45 -0
- data/lib/dommy/style_declaration.rb +38 -6
- data/lib/dommy/svg_elements.rb +25 -11
- data/lib/dommy/text_codec.rb +2 -14
- data/lib/dommy/tree_walker.rb +12 -26
- data/lib/dommy/url.rb +12 -9
- data/lib/dommy/url_pattern.rb +1 -9
- data/lib/dommy/validity_state.rb +109 -107
- data/lib/dommy/version.rb +1 -1
- data/lib/dommy/web_socket.rb +10 -33
- data/lib/dommy/window.rb +817 -105
- data/lib/dommy/worker.rb +4 -28
- data/lib/dommy/xml_http_request.rb +32 -45
- data/lib/dommy.rb +43 -10
- metadata +52 -5
- data/lib/dommy/backend/makiri_adapter.rb +0 -408
- data/lib/dommy/internal/cookie_jar.rb +0 -29
data/lib/dommy/document.rb
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "uri"
|
|
4
|
+
require "time"
|
|
4
5
|
|
|
5
6
|
require_relative "internal/node_wrapper_cache"
|
|
6
7
|
require_relative "internal/directionality"
|
|
7
8
|
require_relative "internal/node_factory"
|
|
8
9
|
require_relative "internal/mutation_coordinator"
|
|
9
10
|
require_relative "internal/shadow_root_registry"
|
|
10
|
-
require_relative "
|
|
11
|
+
require_relative "cookie_jar"
|
|
11
12
|
require_relative "internal/node_traversal"
|
|
12
13
|
require_relative "internal/node_adopter"
|
|
13
14
|
require_relative "internal/observer_manager"
|
|
@@ -35,9 +36,8 @@ module Dommy
|
|
|
35
36
|
# insert steps, neither of which the old doctype-specific path did.
|
|
36
37
|
include Internal::ChildNode
|
|
37
38
|
|
|
38
|
-
# Mixed into a node-backed doctype only, so a synthetic one
|
|
39
|
-
#
|
|
40
|
-
# `respond_to?(:__dommy_backend_node__)`) to treat it as disconnected.
|
|
39
|
+
# Mixed into a node-backed doctype only, so a synthetic one keeps Node's nil
|
|
40
|
+
# `__dommy_backend_node__` and is treated as disconnected.
|
|
41
41
|
module NodeBacked
|
|
42
42
|
def __dommy_backend_node__ = @__node__
|
|
43
43
|
end
|
|
@@ -178,6 +178,14 @@ module Dommy
|
|
|
178
178
|
nil
|
|
179
179
|
when "isConnected"
|
|
180
180
|
is_connected?
|
|
181
|
+
when "textContent", "nodeValue"
|
|
182
|
+
nil
|
|
183
|
+
when "baseURI"
|
|
184
|
+
@owner_document&.base_uri
|
|
185
|
+
else
|
|
186
|
+
# A doctype is a Node and a ChildNode only: Element's and ParentNode's
|
|
187
|
+
# getters (tagName, children, ...) are not its members.
|
|
188
|
+
Bridge::ABSENT
|
|
181
189
|
end
|
|
182
190
|
end
|
|
183
191
|
|
|
@@ -322,7 +330,18 @@ module Dommy
|
|
|
322
330
|
# with, in tree order, the doctype (when given) then a document element
|
|
323
331
|
# (namespace, qualifiedName) when qualifiedName is non-empty.
|
|
324
332
|
def create_document(namespace, qualified_name, doctype = nil)
|
|
333
|
+
# WebIDL converts the interface argument before running the DOM steps
|
|
334
|
+
# (including qualified-name validation).
|
|
335
|
+
unless doctype.nil? || doctype.equal?(Bridge::UNDEFINED) || doctype.is_a?(DocumentType)
|
|
336
|
+
raise Bridge::TypeError, "doctype must be a DocumentType"
|
|
337
|
+
end
|
|
338
|
+
|
|
325
339
|
doc = Document.new(nil, backend_doc: Backend.empty_xml_document)
|
|
340
|
+
# The result is an XMLDocument (DOM's createDocument), unlike a DOMParser
|
|
341
|
+
# result of the same content type — so the interface is pinned here.
|
|
342
|
+
doc.__internal_xml_document__ = true
|
|
343
|
+
# Its origin is the associated document's (DOM createDocument step 7).
|
|
344
|
+
doc.__internal_set_creator__(@document)
|
|
326
345
|
# createDocument's content type is keyed off the namespace. None is
|
|
327
346
|
# "text/html", so tagName keeps its case; xhtml+xml still routes
|
|
328
347
|
# createElement to the HTML namespace (so an XHTML document isEqualNode
|
|
@@ -336,39 +355,24 @@ module Dommy
|
|
|
336
355
|
qn = qualified_name.to_s
|
|
337
356
|
unless qn.empty?
|
|
338
357
|
el = doc.send(:create_element_ns, namespace, qualified_name)
|
|
339
|
-
|
|
358
|
+
doc.backend_doc.add_child(el.__dommy_backend_node__)
|
|
340
359
|
end
|
|
341
360
|
adopt_doctype_into(doc, doctype)
|
|
342
361
|
doc
|
|
343
362
|
end
|
|
344
363
|
|
|
345
|
-
private
|
|
346
|
-
|
|
347
|
-
# Place `doctype` (a DocumentType passed to createDocument) as `doc`'s first
|
|
348
|
-
# child. createDocument appends the doctype node itself (step 5, before
|
|
349
|
-
# step 6 appends the element), so it goes through the ordinary insert: the
|
|
350
|
-
# adoption re-binds the caller's wrapper, and `xmlDoc.firstChild ===
|
|
351
|
-
# doctype`, its parentNode and ownerDocument all follow.
|
|
352
|
-
#
|
|
353
|
-
# No-op for nil/undefined or a non-DocumentType. A doctype the XML backend
|
|
354
|
-
# cannot hold is left unplaced rather than thrown: createDocument itself
|
|
355
|
-
# validates none of it — only an XML serialization would.
|
|
356
|
-
def adopt_doctype_into(doc, doctype)
|
|
357
|
-
return if doctype.nil? || doctype.equal?(Bridge::UNDEFINED)
|
|
358
|
-
return unless doctype.is_a?(DocumentType)
|
|
359
|
-
|
|
360
|
-
doc.insert_before(doctype, doc.document_element)
|
|
361
|
-
rescue DOMException::NotSupportedError
|
|
362
|
-
nil
|
|
363
|
-
end
|
|
364
|
-
|
|
365
|
-
public
|
|
366
|
-
|
|
367
364
|
# createHTMLDocument(title?) — a fresh HTML document (doctype + html > head,
|
|
368
|
-
# body), with an optional <title>.
|
|
365
|
+
# body), with an optional <title>. A given title is a title element in the
|
|
366
|
+
# head holding one Text node of exactly that data, "" included — which
|
|
367
|
+
# the title setter's string-replace-all would leave empty.
|
|
369
368
|
def create_html_document(title = nil)
|
|
370
369
|
doc = Document.new(nil, backend_doc: Backend.parse("<!DOCTYPE html><html><head></head><body></body></html>"))
|
|
371
|
-
|
|
370
|
+
# Its origin is the associated document's (DOM createHTMLDocument step 8).
|
|
371
|
+
doc.__internal_set_creator__(@document)
|
|
372
|
+
unless title.nil? || title.equal?(Bridge::UNDEFINED)
|
|
373
|
+
element = doc.head.append_child(doc.create_element("title"))
|
|
374
|
+
element.append_child(doc.create_text_node(title.to_s))
|
|
375
|
+
end
|
|
372
376
|
doc
|
|
373
377
|
end
|
|
374
378
|
|
|
@@ -381,6 +385,10 @@ module Dommy
|
|
|
381
385
|
when "createDocumentType"
|
|
382
386
|
create_document_type(args[0], args[1], args[2])
|
|
383
387
|
when "createDocument"
|
|
388
|
+
# namespace and qualifiedName are required (either may be null, but
|
|
389
|
+
# must be present); doctype is optional.
|
|
390
|
+
raise Bridge::TypeError, "createDocument requires 2 arguments." if args.length < 2
|
|
391
|
+
|
|
384
392
|
create_document(args[0], args[1], args[2])
|
|
385
393
|
when "createHTMLDocument"
|
|
386
394
|
# title is an OPTIONAL DOMString: a missing or undefined argument leaves
|
|
@@ -394,6 +402,27 @@ module Dommy
|
|
|
394
402
|
has_feature
|
|
395
403
|
end
|
|
396
404
|
end
|
|
405
|
+
|
|
406
|
+
private
|
|
407
|
+
|
|
408
|
+
# Place `doctype` (a DocumentType passed to createDocument) as `doc`'s first
|
|
409
|
+
# child. createDocument appends the doctype node itself (step 5, before
|
|
410
|
+
# step 6 appends the element), so it goes through the ordinary insert: the
|
|
411
|
+
# adoption re-binds the caller's wrapper, and `xmlDoc.firstChild ===
|
|
412
|
+
# doctype`, its parentNode and ownerDocument all follow.
|
|
413
|
+
#
|
|
414
|
+
# No-op for nil/undefined. Any other non-DocumentType is a TypeError: the
|
|
415
|
+
# IDL converts `doctype` to `DocumentType?`, so `createDocument(ns, name,
|
|
416
|
+
# false)` throws rather than ignoring the value. A doctype the XML backend
|
|
417
|
+
# cannot hold is left unplaced rather than thrown: only an XML
|
|
418
|
+
# serialization would validate its contents.
|
|
419
|
+
def adopt_doctype_into(doc, doctype)
|
|
420
|
+
return if doctype.nil? || doctype.equal?(Bridge::UNDEFINED)
|
|
421
|
+
|
|
422
|
+
doc.insert_before(doctype, doc.document_element)
|
|
423
|
+
rescue DOMException::NotSupportedError
|
|
424
|
+
nil
|
|
425
|
+
end
|
|
397
426
|
end
|
|
398
427
|
|
|
399
428
|
# `document` — the entry point for DOM construction and querying.
|
|
@@ -409,6 +438,7 @@ module Dommy
|
|
|
409
438
|
include Internal::DocumentLiveRanges
|
|
410
439
|
include Internal::DocumentInteractionState
|
|
411
440
|
include EventTarget
|
|
441
|
+
extend Internal::EventHandlers::AnswersIdlAttributes
|
|
412
442
|
include Node
|
|
413
443
|
|
|
414
444
|
attr_reader :backend_doc
|
|
@@ -435,7 +465,7 @@ module Dommy
|
|
|
435
465
|
# edit only when it's inside a <style> or flips emptiness while a sheet
|
|
436
466
|
# uses `:empty`. The coordinator reports mutations through the
|
|
437
467
|
# `__internal_note_*` seams below, which decide what to bump.
|
|
438
|
-
attr_accessor :
|
|
468
|
+
attr_accessor :__internal_css_style_cache__
|
|
439
469
|
|
|
440
470
|
|
|
441
471
|
|
|
@@ -463,11 +493,12 @@ module Dommy
|
|
|
463
493
|
|
|
464
494
|
|
|
465
495
|
|
|
466
|
-
# A by-id/class/tag index of the backend element tree, memoized per
|
|
467
|
-
#
|
|
468
|
-
# the caller to walk). Rebuilt
|
|
469
|
-
#
|
|
470
|
-
#
|
|
496
|
+
# A by-id/class/tag index of the backend element tree, memoized per
|
|
497
|
+
# #__internal_selector_index_generation__, for SelectorMatcher's
|
|
498
|
+
# document-scoped fast path (or nil to tell the caller to walk). Rebuilt
|
|
499
|
+
# lazily only after a mutation that can change it (the tree, or an `id` /
|
|
500
|
+
# `class`), so it costs one tree walk per such mutation and pays off when
|
|
501
|
+
# several queries run before the next one.
|
|
471
502
|
#
|
|
472
503
|
# Adaptive bypass: if the index keeps getting invalidated after serving only a
|
|
473
504
|
# handful of queries (a mutation-between-every-query workload, where building
|
|
@@ -479,7 +510,7 @@ module Dommy
|
|
|
479
510
|
SELECTOR_INDEX_RETEST_GAP = 64 # generations to wait before re-testing a bypass
|
|
480
511
|
|
|
481
512
|
def __internal_selector_index__
|
|
482
|
-
gen =
|
|
513
|
+
gen = __internal_selector_index_generation__
|
|
483
514
|
if @__sel_idx_gen != gen
|
|
484
515
|
if @__sel_idx
|
|
485
516
|
if @__sel_idx_served.to_i < SELECTOR_INDEX_MIN_REUSE
|
|
@@ -500,7 +531,7 @@ module Dommy
|
|
|
500
531
|
end
|
|
501
532
|
return nil if @__sel_idx_bypass
|
|
502
533
|
|
|
503
|
-
@__sel_idx ||= Internal::SelectorIndex.build(@backend_doc)
|
|
534
|
+
@__sel_idx ||= Internal::SelectorIndex.build(@backend_doc, quirks: quirks_mode?)
|
|
504
535
|
@__sel_idx_served += 1
|
|
505
536
|
@__sel_idx
|
|
506
537
|
end
|
|
@@ -536,7 +567,20 @@ module Dommy
|
|
|
536
567
|
end
|
|
537
568
|
# content_type defaults to "text/html"; settable so an integration layer
|
|
538
569
|
# can reflect the response Content-Type. Read-only over the JS bridge.
|
|
539
|
-
|
|
570
|
+
attr_reader :content_type
|
|
571
|
+
|
|
572
|
+
# Whether the document is an HTML one rides on its type, and with it its
|
|
573
|
+
# mode — only an HTML document can be in quirks mode — and how selectors
|
|
574
|
+
# fold id and class case. A layer that sets the type after the parse
|
|
575
|
+
# (dommy-rack does) has the mode worked out again and the selector
|
|
576
|
+
# results retired.
|
|
577
|
+
def content_type=(value)
|
|
578
|
+
return if value == @content_type
|
|
579
|
+
|
|
580
|
+
@content_type = value
|
|
581
|
+
@quirks_mode = nil
|
|
582
|
+
__internal_note_selector_state_change__
|
|
583
|
+
end
|
|
540
584
|
# A `->(source_text) {}` set by the JS layer to execute a classic <script>'s
|
|
541
585
|
# body when it's connected (Dommy has no JS engine of its own). nil = inert
|
|
542
586
|
# scripts (the default for a standalone DOM).
|
|
@@ -558,6 +602,25 @@ module Dommy
|
|
|
558
602
|
nil
|
|
559
603
|
end
|
|
560
604
|
|
|
605
|
+
# A `->(element, name, body, window_handler) {}` set by the JS layer that
|
|
606
|
+
# compiles an event handler content attribute's body into a function (the
|
|
607
|
+
# element, its form owner and the document in its scope; none for a
|
|
608
|
+
# Window's handler, whose `onerror` takes five arguments), raising the
|
|
609
|
+
# SyntaxError of a body that does not parse. nil = no engine, nothing runs.
|
|
610
|
+
attr_accessor :event_handler_compiler
|
|
611
|
+
|
|
612
|
+
# Activate the event handler content attributes of every element the
|
|
613
|
+
# parser built (it runs no attribute change steps), as though each had
|
|
614
|
+
# just been set. Run at boot, before any script, and safe to repeat.
|
|
615
|
+
def __internal_activate_parsed_event_handlers__
|
|
616
|
+
selector = (Internal::EventHandlers::GLOBAL | Internal::EventHandlers::WINDOW).map { |name| "[#{name}]" }.join(",")
|
|
617
|
+
parsed_css(selector).each do |node|
|
|
618
|
+
element = wrap_node(node)
|
|
619
|
+
element.__internal_wire_inline_handler__(nil) if element.respond_to?(:__internal_wire_inline_handler__)
|
|
620
|
+
end
|
|
621
|
+
nil
|
|
622
|
+
end
|
|
623
|
+
|
|
561
624
|
def initialize(host = nil, backend_doc: nil, default_view: nil)
|
|
562
625
|
@host = host
|
|
563
626
|
@default_view = default_view
|
|
@@ -565,7 +628,6 @@ module Dommy
|
|
|
565
628
|
@node_factory = Internal::NodeFactory.new(self, @node_wrapper_cache)
|
|
566
629
|
@observer_manager = Internal::ObserverManager.new
|
|
567
630
|
@shadow_registry = Internal::ShadowRootRegistry.new
|
|
568
|
-
@cookie_jar = Internal::CookieJar.new
|
|
569
631
|
@template_content_registry = Internal::TemplateContentRegistry.new(self)
|
|
570
632
|
@mutation_coordinator = Internal::MutationCoordinator.new(self, @observer_manager)
|
|
571
633
|
# Weak, like @live_ranges: a NodeIterator is consulted by every removal for
|
|
@@ -585,30 +647,50 @@ module Dommy
|
|
|
585
647
|
@__current_script__ = nil
|
|
586
648
|
end
|
|
587
649
|
|
|
588
|
-
# Whether this is an "HTML document" in the DOM sense (
|
|
589
|
-
#
|
|
590
|
-
#
|
|
591
|
-
#
|
|
592
|
-
#
|
|
593
|
-
#
|
|
650
|
+
# Whether this is an "HTML document" in the DOM sense (DOM's type "html"),
|
|
651
|
+
# as opposed to an XML document. It drives the case-folding rules:
|
|
652
|
+
# `createElement` lowercases names and `Element#tagName` uppercases
|
|
653
|
+
# HTML-namespace names only in an HTML document. An XML or XHTML document
|
|
654
|
+
# (e.g. an `application/xhtml+xml` / `text/xml` resource) preserves case.
|
|
655
|
+
#
|
|
656
|
+
# The type is not the content type: a text document (a text/plain or
|
|
657
|
+
# text/css response shown in a <pre>) is an HTML document too. Every way of
|
|
658
|
+
# making an XML document gives it an XML MIME type, so that is what decides.
|
|
594
659
|
def html_document?
|
|
595
|
-
@content_type
|
|
660
|
+
!Internal::MimeType.xml?(@content_type)
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
# Whether this document is DOM's XMLDocument — the interface
|
|
664
|
+
# `implementation.createDocument` returns (and a clone of one keeps). A
|
|
665
|
+
# DOMParser result is the base Document even when parsed from XML, so this
|
|
666
|
+
# rides on the instance rather than the content type. Together with
|
|
667
|
+
# #html_document? it decides the document's most-derived interface.
|
|
668
|
+
def xml_document? = @xml_document == true
|
|
669
|
+
|
|
670
|
+
def __internal_xml_document__=(value)
|
|
671
|
+
@xml_document = value
|
|
596
672
|
end
|
|
597
673
|
|
|
598
|
-
# `document.compatMode` — "
|
|
599
|
-
# quirks
|
|
600
|
-
# public/system identifier) is no-quirks. (The full quirks algorithm keys off
|
|
601
|
-
# specific legacy public ids; this covers the common cases.)
|
|
674
|
+
# `document.compatMode` — "BackCompat" in quirks mode, "CSS1Compat" in
|
|
675
|
+
# no-quirks and limited-quirks mode alike.
|
|
602
676
|
def compat_mode
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
return "CSS1Compat" unless html_document?
|
|
677
|
+
quirks_mode? ? "BackCompat" : "CSS1Compat"
|
|
678
|
+
end
|
|
606
679
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
680
|
+
# Whether the document is in quirks mode (DOM's "mode"). Only the HTML
|
|
681
|
+
# parser puts a document in it, from the doctype it read — a missing one,
|
|
682
|
+
# or one of HTML's legacy public identifiers (§13.2.6.4.1); the backend's
|
|
683
|
+
# parser has already run that algorithm. Every other document is in
|
|
684
|
+
# no-quirks mode, and a clone takes its original's mode. The mode is fixed
|
|
685
|
+
# once known: changing the doctype later does not change it.
|
|
686
|
+
def quirks_mode?
|
|
687
|
+
return @quirks_mode unless @quirks_mode.nil?
|
|
688
|
+
|
|
689
|
+
@quirks_mode = html_document? && Backend.quirks_mode?(@backend_doc)
|
|
690
|
+
end
|
|
610
691
|
|
|
611
|
-
|
|
692
|
+
def __internal_quirks_mode__=(value)
|
|
693
|
+
@quirks_mode = value
|
|
612
694
|
end
|
|
613
695
|
|
|
614
696
|
# The document's character encoding, as an Encoding Standard name. Dommy
|
|
@@ -634,9 +716,35 @@ module Dommy
|
|
|
634
716
|
end
|
|
635
717
|
|
|
636
718
|
def dir=(value)
|
|
637
|
-
html_element&.
|
|
719
|
+
html_element&.__internal_set_attribute_value__("dir", value.to_s)
|
|
720
|
+
end
|
|
721
|
+
|
|
722
|
+
# HTML §16 (obsolete features), the partial Document interface: fgColor,
|
|
723
|
+
# linkColor, vlinkColor, alinkColor and bgColor reflect the body element's
|
|
724
|
+
# text, link, vlink, alink and bgcolor content attributes — "if the body
|
|
725
|
+
# element is a body element (as opposed to a frameset element). When there
|
|
726
|
+
# is no body element or if it is a frameset element, the attributes must
|
|
727
|
+
# instead return the empty string on getting and do nothing on setting."
|
|
728
|
+
# The setters are [LegacyNullToEmptyString] (null writes "").
|
|
729
|
+
LEGACY_BODY_COLORS = {
|
|
730
|
+
fg_color: "text", link_color: "link", vlink_color: "vlink", alink_color: "alink", bg_color: "bgcolor"
|
|
731
|
+
}.freeze
|
|
732
|
+
|
|
733
|
+
LEGACY_BODY_COLORS.each do |name, attr|
|
|
734
|
+
define_method(name) do
|
|
735
|
+
element = body
|
|
736
|
+
element.is_a?(HTMLBodyElement) ? element.__internal_attribute_value__(attr).to_s : ""
|
|
737
|
+
end
|
|
738
|
+
|
|
739
|
+
define_method(:"#{name}=") do |value|
|
|
740
|
+
element = body
|
|
741
|
+
element.__internal_set_attribute_value__(attr, value.to_s) if element.is_a?(HTMLBodyElement)
|
|
742
|
+
end
|
|
638
743
|
end
|
|
639
744
|
|
|
745
|
+
# Whether designMode is "on", which makes the whole document editable.
|
|
746
|
+
def __internal_design_mode__? = @design_mode == "on"
|
|
747
|
+
|
|
640
748
|
def title=(value)
|
|
641
749
|
write_title(value.to_s)
|
|
642
750
|
end
|
|
@@ -654,33 +762,49 @@ module Dommy
|
|
|
654
762
|
# answers null: the backend's `root` falls back to the doctype once the
|
|
655
763
|
# element is gone (`document.removeChild(documentElement)`, or a
|
|
656
764
|
# replaceChild that swaps it for a comment), and head / body / title
|
|
657
|
-
# resolve through this.
|
|
658
|
-
root =
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
wrap_node(root)
|
|
765
|
+
# resolve through this (#backend_document_element).
|
|
766
|
+
root = backend_document_element
|
|
767
|
+
root && wrap_node(root)
|
|
662
768
|
end
|
|
663
769
|
|
|
770
|
+
# HTML's "the head element": the first `head` child, in the HTML
|
|
771
|
+
# namespace, of the html element (readonly — assignment is a no-op, see
|
|
772
|
+
# __js_set__).
|
|
773
|
+
#
|
|
774
|
+
# Spec: https://html.spec.whatwg.org/multipage/dom.html#the-head-element-2
|
|
664
775
|
def head
|
|
665
|
-
|
|
666
|
-
# namespace (readonly — assignment is a no-op, see __js_set__). Not just
|
|
667
|
-
# `at_css("head")`, which searches the whole tree and ignores namespace.
|
|
668
|
-
root = document_element
|
|
669
|
-
return nil unless root
|
|
670
|
-
|
|
671
|
-
root.child_nodes.to_a.find do |c|
|
|
672
|
-
c.respond_to?(:local_name) && c.local_name == "head" &&
|
|
673
|
-
c.respond_to?(:namespace_uri) && c.namespace_uri == Internal::Namespaces::HTML
|
|
674
|
-
end
|
|
776
|
+
wrap_node(html_element_child(%w[head]))
|
|
675
777
|
end
|
|
676
778
|
|
|
677
|
-
#
|
|
678
|
-
#
|
|
679
|
-
# `
|
|
680
|
-
#
|
|
681
|
-
#
|
|
779
|
+
# HTML's "the body element": the first child of the html element — the
|
|
780
|
+
# document element, if it is the HTML namespace's `html` — that is a
|
|
781
|
+
# `body` or a `frameset` in the HTML namespace; nil without one. Resolved
|
|
782
|
+
# fresh from the tree (not memoized) so it tracks a swapped `<body>` — e.g.
|
|
783
|
+
# Turbo's page render does `documentElement.replaceChild(newBody, body)`.
|
|
784
|
+
# wrap_node caches by node, so `document.body === document.body` holds.
|
|
785
|
+
#
|
|
786
|
+
# Spec: https://html.spec.whatwg.org/multipage/dom.html#the-body-element-2
|
|
682
787
|
def body
|
|
683
|
-
wrap_node(
|
|
788
|
+
wrap_node(html_element_child(%w[body frameset]))
|
|
789
|
+
end
|
|
790
|
+
|
|
791
|
+
# The body setter: the new value must be a `body` or a `frameset` in the
|
|
792
|
+
# HTML namespace. It replaces the body element there is, or else is
|
|
793
|
+
# appended to the document element; without either, there is nowhere to
|
|
794
|
+
# put it.
|
|
795
|
+
#
|
|
796
|
+
# Spec: https://html.spec.whatwg.org/multipage/dom.html#dom-document-body
|
|
797
|
+
def body=(element)
|
|
798
|
+
raise DOMException::HierarchyRequestError, "body must be a body or frameset element" unless body_or_frameset?(element)
|
|
799
|
+
|
|
800
|
+
current = body
|
|
801
|
+
return if element.equal?(current)
|
|
802
|
+
return current.parent_node.replace_child(element, current) if current
|
|
803
|
+
|
|
804
|
+
root = document_element
|
|
805
|
+
raise DOMException::HierarchyRequestError, "the document has no document element" unless root
|
|
806
|
+
|
|
807
|
+
root.append_child(element)
|
|
684
808
|
end
|
|
685
809
|
|
|
686
810
|
# The document's accessibility tree (built from <body>; the document itself
|
|
@@ -716,7 +840,18 @@ module Dommy
|
|
|
716
840
|
# "about:blank", not the empty string.
|
|
717
841
|
def url
|
|
718
842
|
view = @default_view
|
|
719
|
-
|
|
843
|
+
return view.location.href if view&.location
|
|
844
|
+
|
|
845
|
+
@creator_url || "about:blank"
|
|
846
|
+
end
|
|
847
|
+
|
|
848
|
+
# A document made by a parsing or creation API (DOMParser,
|
|
849
|
+
# DOMImplementation) takes its origin from `document` — the relevant
|
|
850
|
+
# global object's associated Document — and, for DOMParser, its URL too.
|
|
851
|
+
def __internal_set_creator__(document, url: nil)
|
|
852
|
+
@origin_document = document
|
|
853
|
+
@creator_url = url
|
|
854
|
+
nil
|
|
720
855
|
end
|
|
721
856
|
|
|
722
857
|
alias document_uri url
|
|
@@ -730,7 +865,7 @@ module Dommy
|
|
|
730
865
|
base_el = @backend_doc.at_css("base[href]")
|
|
731
866
|
return doc_url unless base_el
|
|
732
867
|
|
|
733
|
-
href = base_el
|
|
868
|
+
href = Backend.no_namespace_attribute_value(base_el, "href").to_s
|
|
734
869
|
return doc_url if href.empty?
|
|
735
870
|
|
|
736
871
|
# HTML "document base URL": the frozen base URL is the href parsed
|
|
@@ -749,32 +884,88 @@ module Dommy
|
|
|
749
884
|
nil
|
|
750
885
|
end
|
|
751
886
|
|
|
752
|
-
|
|
753
|
-
|
|
887
|
+
# `document.domain` — the effective domain of the document's origin
|
|
888
|
+
# serialized: the domain a `document.domain =` set, or else the origin's
|
|
889
|
+
# host; "" for an opaque origin.
|
|
890
|
+
def domain
|
|
891
|
+
return @domain_override if @domain_override
|
|
892
|
+
|
|
893
|
+
host = Internal::Origin.host_of(origin)
|
|
894
|
+
host.nil? ? "" : host.to_s
|
|
754
895
|
end
|
|
755
896
|
|
|
756
|
-
# `document.domain`
|
|
757
|
-
#
|
|
758
|
-
|
|
897
|
+
# The `document.domain` setter. It needs a browsing context and a tuple
|
|
898
|
+
# origin, and accepts only the current effective domain or a registrable
|
|
899
|
+
# suffix of it (everything else is a SecurityError). The agent cluster is
|
|
900
|
+
# site-keyed (no Origin-Agent-Cluster header), so the domain is then set.
|
|
901
|
+
#
|
|
902
|
+
# Without a public suffix list, a single label (a bare TLD such as "com")
|
|
903
|
+
# stands in for the public suffix.
|
|
904
|
+
def domain=(value)
|
|
759
905
|
view = @default_view
|
|
760
|
-
|
|
906
|
+
raise DOMException::SecurityError, "The document has no browsing context" unless view&.navigable?
|
|
907
|
+
|
|
908
|
+
current = Internal::Origin.host_of(origin)
|
|
909
|
+
raise DOMException::SecurityError, "The document's origin is opaque" if current.nil?
|
|
910
|
+
|
|
911
|
+
candidate = value.to_s
|
|
912
|
+
unless registrable_domain_suffix_or_equal?(candidate, current.to_s)
|
|
913
|
+
raise DOMException::SecurityError, "'#{candidate}' is not a suffix of '#{current}'"
|
|
914
|
+
end
|
|
761
915
|
|
|
762
|
-
|
|
916
|
+
@domain_override = Internal::UrlParser.parse("http://#{candidate}/").host.to_s
|
|
763
917
|
end
|
|
764
918
|
|
|
765
|
-
# `document.origin` —
|
|
766
|
-
# `
|
|
919
|
+
# `document.origin` — this document's origin, serialized (the same answer as
|
|
920
|
+
# `self.origin`). Empty when there is no associated window.
|
|
767
921
|
def origin
|
|
768
922
|
view = @default_view
|
|
769
|
-
return
|
|
923
|
+
return @origin_document&.origin.to_s unless view&.location
|
|
770
924
|
|
|
771
925
|
view.origin
|
|
772
926
|
end
|
|
773
927
|
|
|
774
|
-
# `document.
|
|
775
|
-
#
|
|
928
|
+
# `document.lastModified` — the source's last modification (the response's
|
|
929
|
+
# Last-Modified header, when the embedder passed one on), else the current
|
|
930
|
+
# time, in local time as "MM/DD/YYYY hh:mm:ss".
|
|
931
|
+
#
|
|
932
|
+
# Spec: https://html.spec.whatwg.org/#dom-document-lastmodified
|
|
933
|
+
def last_modified
|
|
934
|
+
(@last_modified_time || Time.now).getlocal.strftime("%m/%d/%Y %H:%M:%S")
|
|
935
|
+
end
|
|
936
|
+
|
|
937
|
+
# Record the document's source last-modified time: a Time, or an HTTP date
|
|
938
|
+
# string (a Last-Modified header value). One that does not parse leaves it
|
|
939
|
+
# unknown.
|
|
940
|
+
def __internal_set_last_modified__(value)
|
|
941
|
+
@last_modified_time =
|
|
942
|
+
case value
|
|
943
|
+
when Time then value
|
|
944
|
+
when nil then nil
|
|
945
|
+
else
|
|
946
|
+
begin
|
|
947
|
+
Time.httpdate(value.to_s.strip)
|
|
948
|
+
rescue ArgumentError
|
|
949
|
+
nil
|
|
950
|
+
end
|
|
951
|
+
end
|
|
952
|
+
nil
|
|
953
|
+
end
|
|
954
|
+
|
|
955
|
+
# `document.referrer` — the URL of the document that navigated here, as the
|
|
956
|
+
# embedder reports it (`__internal_referrer__=`); empty when there was none.
|
|
776
957
|
def referrer
|
|
777
|
-
|
|
958
|
+
@referrer.to_s
|
|
959
|
+
end
|
|
960
|
+
|
|
961
|
+
def __internal_referrer__=(url)
|
|
962
|
+
@referrer = url
|
|
963
|
+
end
|
|
964
|
+
|
|
965
|
+
# Whether the document has completely finished loading: its `load` event
|
|
966
|
+
# has been fired. A document whose lifecycle no one replays is born loaded.
|
|
967
|
+
def __internal_completely_loaded__?
|
|
968
|
+
!@loading_lifecycle
|
|
778
969
|
end
|
|
779
970
|
|
|
780
971
|
# Live HTMLCollection helpers — each call re-queries the
|
|
@@ -817,7 +1008,7 @@ module Dommy
|
|
|
817
1008
|
# in practice the `<html>` root).
|
|
818
1009
|
def children
|
|
819
1010
|
@live_children ||= HTMLCollection.new do
|
|
820
|
-
root =
|
|
1011
|
+
root = backend_document_element
|
|
821
1012
|
root ? [wrap_node(root)].compact : []
|
|
822
1013
|
end
|
|
823
1014
|
end
|
|
@@ -835,13 +1026,10 @@ module Dommy
|
|
|
835
1026
|
children.size
|
|
836
1027
|
end
|
|
837
1028
|
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
end
|
|
1029
|
+
# A document has at most one element child, the document element.
|
|
1030
|
+
def first_element_child = document_element
|
|
841
1031
|
|
|
842
|
-
def last_element_child
|
|
843
|
-
wrap_node(@backend_doc.root)
|
|
844
|
-
end
|
|
1032
|
+
def last_element_child = document_element
|
|
845
1033
|
|
|
846
1034
|
|
|
847
1035
|
# `document.contains(node)` — true if `node` is the document itself or any
|
|
@@ -849,14 +1037,13 @@ module Dommy
|
|
|
849
1037
|
# document expose). Per spec, false for null / a non-Node.
|
|
850
1038
|
def contains?(other)
|
|
851
1039
|
return true if other.equal?(self)
|
|
852
|
-
return false unless other.
|
|
1040
|
+
return false unless other.is_a?(Node)
|
|
853
1041
|
|
|
854
|
-
#
|
|
1042
|
+
# Whose root the backend document node is. (The backend's #ancestors
|
|
855
1043
|
# stops below the document, so it can't test document membership; the
|
|
856
1044
|
# doctype in particular reports an empty ancestor list.)
|
|
857
1045
|
node = other.__dommy_backend_node__
|
|
858
|
-
node
|
|
859
|
-
!node.nil?
|
|
1046
|
+
!node.nil? && Internal::NodeTraversal.root_of(node).equal?(@backend_doc)
|
|
860
1047
|
end
|
|
861
1048
|
|
|
862
1049
|
|
|
@@ -915,39 +1102,47 @@ module Dommy
|
|
|
915
1102
|
# it here with the same qualified name, namespace, prefix and value, owned
|
|
916
1103
|
# by no element (importNode never attaches the copy to anything).
|
|
917
1104
|
return import_attribute(node) if node.is_a?(Attr)
|
|
918
|
-
return nil unless node.
|
|
1105
|
+
return nil unless node.is_a?(Node) && node.__dommy_backend_node__
|
|
1106
|
+
|
|
1107
|
+
# `(boolean or ImportNodeOptions) options = false`: missing or undefined
|
|
1108
|
+
# is the default, false. Null and an object convert to the dictionary,
|
|
1109
|
+
# whose `selfOnly` negates `deep` (so null clones deep) and whose
|
|
1110
|
+
# `customElementRegistry` (a scoped one, or this document's) is the
|
|
1111
|
+
# registry the copies fall back to. Anything else is a boolean, read
|
|
1112
|
+
# with ToBoolean.
|
|
1113
|
+
registry = nil
|
|
1114
|
+
deep = false if deep.equal?(Bridge::UNDEFINED)
|
|
1115
|
+
if deep.nil? || deep.is_a?(Hash)
|
|
1116
|
+
options = deep || {}
|
|
1117
|
+
deep = !Internal::WebIDL.boolean(options["selfOnly"])
|
|
1118
|
+
given = options.fetch("customElementRegistry", Bridge::UNDEFINED)
|
|
1119
|
+
unless given.equal?(Bridge::UNDEFINED)
|
|
1120
|
+
raise Bridge::TypeError, "customElementRegistry is not a CustomElementRegistry" unless given.is_a?(CustomElementRegistry)
|
|
1121
|
+
if !given.scoped? && !given.equal?(__internal_custom_element_registry__)
|
|
1122
|
+
raise DOMException::NotSupportedError, "a global registry other than the document's"
|
|
1123
|
+
end
|
|
919
1124
|
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
1125
|
+
registry = given
|
|
1126
|
+
end
|
|
1127
|
+
else
|
|
1128
|
+
deep = Internal::WebIDL.boolean(deep)
|
|
1129
|
+
end
|
|
1130
|
+
# "Clone a single node": the node's own registry — a global one standing
|
|
1131
|
+
# for this document's — and the fallback only for a node without one.
|
|
1132
|
+
fallback = registry || __internal_custom_element_registry__
|
|
1133
|
+
source = node.is_a?(Element) ? node.__internal_ce_registry__ : fallback
|
|
1134
|
+
registry = if source.nil? then fallback
|
|
1135
|
+
elsif source.scoped? then source
|
|
1136
|
+
else __internal_custom_element_registry__
|
|
1137
|
+
end
|
|
923
1138
|
source_document = node.respond_to?(:document) ? node.document : self
|
|
924
1139
|
copy = clone_into_doc(node.__dommy_backend_node__, deep, source_document)
|
|
925
1140
|
apply_imported_cloning_steps(node.__dommy_backend_node__, copy, deep, source_document)
|
|
1141
|
+
__internal_enqueue_created_upgrades__(copy, registry) if copy.respond_to?(:element?)
|
|
1142
|
+
__internal_clone_shadow_roots__(node.__dommy_backend_node__, copy, deep, source_document) if copy.respond_to?(:element?)
|
|
926
1143
|
wrap_node(copy)
|
|
927
1144
|
end
|
|
928
1145
|
|
|
929
|
-
# importNode is a clone, so the HTML cloning steps run for it as they do
|
|
930
|
-
# for cloneNode (#__internal_apply_cloning_steps__): the live state Dommy
|
|
931
|
-
# keeps on a wrapper — an input's dirty value, a script's "already
|
|
932
|
-
# started" — is copied onto the new node. The originals' wrappers belong
|
|
933
|
-
# to the source document, so they are looked up there.
|
|
934
|
-
def apply_imported_cloning_steps(src_root, copy_root, deep, source_document)
|
|
935
|
-
return unless source_document.respond_to?(:__internal_peek_wrapper__)
|
|
936
|
-
|
|
937
|
-
src_nodes = deep ? Internal::NodeTraversal.subtree_nodes(src_root) : [src_root]
|
|
938
|
-
copy_nodes = deep ? Internal::NodeTraversal.subtree_nodes(copy_root) : [copy_root]
|
|
939
|
-
return unless src_nodes.length == copy_nodes.length
|
|
940
|
-
|
|
941
|
-
src_nodes.zip(copy_nodes).each do |orig, copy|
|
|
942
|
-
state = source_document.__internal_peek_wrapper__(orig)&.then { |w| w.respond_to?(:__cloning_state__) && w.__cloning_state__ }
|
|
943
|
-
next unless state
|
|
944
|
-
|
|
945
|
-
wrapper = wrap_node(copy)
|
|
946
|
-
wrapper.__apply_cloning_state__(state) if wrapper.respond_to?(:__apply_cloning_state__)
|
|
947
|
-
end
|
|
948
|
-
end
|
|
949
|
-
private :apply_imported_cloning_steps
|
|
950
|
-
|
|
951
1146
|
def import_attribute(attr)
|
|
952
1147
|
Attr.new(
|
|
953
1148
|
attr.name,
|
|
@@ -984,11 +1179,6 @@ module Dommy
|
|
|
984
1179
|
node_adopter.adopt_backend_node(node, source_document)
|
|
985
1180
|
end
|
|
986
1181
|
|
|
987
|
-
def node_adopter
|
|
988
|
-
@node_adopter ||= Internal::NodeAdopter.new(self)
|
|
989
|
-
end
|
|
990
|
-
private :node_adopter
|
|
991
|
-
|
|
992
1182
|
# HTML "cloning steps": a cloned node copies interface-specific live state
|
|
993
1183
|
# that the content attributes don't capture — an input's dirty value and
|
|
994
1184
|
# checkedness, a textarea's dirty value, etc. Dommy keeps that state on the
|
|
@@ -1009,15 +1199,69 @@ module Dommy
|
|
|
1009
1199
|
clone_template_content(orig, copy) if deep && @template_content_registry.detached?(orig)
|
|
1010
1200
|
|
|
1011
1201
|
wrapper = @node_wrapper_cache.peek(orig)
|
|
1012
|
-
next unless wrapper.respond_to?(:
|
|
1202
|
+
next unless wrapper.respond_to?(:__internal_cloning_state__)
|
|
1013
1203
|
|
|
1014
|
-
state = wrapper.
|
|
1204
|
+
state = wrapper.__internal_cloning_state__
|
|
1015
1205
|
next if state.nil?
|
|
1016
1206
|
|
|
1017
|
-
@node_wrapper_cache.wrap(copy).
|
|
1207
|
+
@node_wrapper_cache.wrap(copy).__internal_apply_cloning_state__(state)
|
|
1018
1208
|
end
|
|
1019
1209
|
end
|
|
1020
1210
|
|
|
1211
|
+
# DOM "clone a node" step 6, for `copy_root` — a clone made in this
|
|
1212
|
+
# document of `src_root` (of `source_document`), with or without its
|
|
1213
|
+
# subtree — and, when `deep`, every element of the copy: the clone of a
|
|
1214
|
+
# shadow host whose shadow root is clonable gets a shadow root of its own
|
|
1215
|
+
# with the same mode, delegates focus, serializable, slot assignment,
|
|
1216
|
+
# declarative and keep-custom-element-registry-null, clonable, holding
|
|
1217
|
+
# clones of the shadow root's children. A shallow clone still does this
|
|
1218
|
+
# for the host itself. The backend copied the subtree (and template
|
|
1219
|
+
# contents) in one go, so the two trees are walked in lockstep.
|
|
1220
|
+
def __internal_clone_shadow_roots__(src_root, copy_root, deep, source_document = self)
|
|
1221
|
+
return unless source_document.__internal_any_shadow_roots__?
|
|
1222
|
+
|
|
1223
|
+
clone_shadow_roots_walk(src_root, copy_root, deep, source_document)
|
|
1224
|
+
end
|
|
1225
|
+
|
|
1226
|
+
def clone_shadow_roots_walk(src, copy, deep, source_document)
|
|
1227
|
+
return unless src.element? || src.is_a?(Backend.document_fragment_class)
|
|
1228
|
+
|
|
1229
|
+
if src.element?
|
|
1230
|
+
shadow = source_document.__internal_shadow_root_for_host__(src)
|
|
1231
|
+
clone_shadow_root(shadow, copy, source_document) if shadow&.clonable
|
|
1232
|
+
end
|
|
1233
|
+
return unless deep
|
|
1234
|
+
|
|
1235
|
+
src_children = src.children.to_a
|
|
1236
|
+
copy_children = copy.children.to_a
|
|
1237
|
+
return unless src_children.length == copy_children.length
|
|
1238
|
+
|
|
1239
|
+
src_children.zip(copy_children).each { |s, c| clone_shadow_roots_walk(s, c, true, source_document) }
|
|
1240
|
+
return unless src.element? && @template_content_registry.template_node?(src)
|
|
1241
|
+
|
|
1242
|
+
src_contents = source_document.__internal_template_registry__.existing_contents(src)
|
|
1243
|
+
copy_contents = @template_content_registry.existing_contents(copy)
|
|
1244
|
+
clone_shadow_roots_walk(src_contents, copy_contents, true, source_document) if src_contents && copy_contents
|
|
1245
|
+
end
|
|
1246
|
+
|
|
1247
|
+
def clone_shadow_root(shadow, copy, source_document)
|
|
1248
|
+
host = wrap_node(copy)
|
|
1249
|
+
# The shadow root's registry; a global one stands for this document's.
|
|
1250
|
+
registry = shadow.__internal_registry_set__? ? shadow.__internal_custom_element_registry__ : :document
|
|
1251
|
+
registry = :document if registry && registry != :document && !registry.scoped?
|
|
1252
|
+
root = host.__internal_attach_shadow_root__(
|
|
1253
|
+
mode: shadow.mode, delegates_focus: shadow.delegates_focus, serializable: shadow.serializable,
|
|
1254
|
+
slot_assignment: shadow.slot_assignment, clonable: true, registry: registry
|
|
1255
|
+
)
|
|
1256
|
+
root.__internal_declarative__ = shadow.__internal_declarative__?
|
|
1257
|
+
root.__internal_keep_registry_null__ = shadow.__internal_keep_registry_null__?
|
|
1258
|
+
shadow.child_nodes.to_a.each do |child|
|
|
1259
|
+
child_copy = source_document.equal?(self) ? child.clone_node(true) : import_node(child, true)
|
|
1260
|
+
root.append_child(child_copy)
|
|
1261
|
+
end
|
|
1262
|
+
end
|
|
1263
|
+
private :clone_shadow_roots_walk, :clone_shadow_root
|
|
1264
|
+
|
|
1021
1265
|
# Legacy `document.createEvent("EventName")` factory. The DOM Standard
|
|
1022
1266
|
# matches the type ASCII case-insensitively against a fixed alias table, and
|
|
1023
1267
|
# throws NotSupportedError for anything else — including the plural forms it
|
|
@@ -1062,8 +1306,22 @@ module Dommy
|
|
|
1062
1306
|
# don't apply to a layout-less DOM. They exist so callers don't
|
|
1063
1307
|
# hit NoMethodError; semantics are documented as no-op.
|
|
1064
1308
|
|
|
1309
|
+
# `document.hasFocus()` — HTML's "has focus steps", with the top-level
|
|
1310
|
+
# page always holding system focus: a document with no browsing context
|
|
1311
|
+
# has no focus, a top-level one has it, and a nested one has it when its
|
|
1312
|
+
# frame is the focused element of a parent document that has it.
|
|
1313
|
+
#
|
|
1314
|
+
# Spec: https://html.spec.whatwg.org/#has-focus-steps
|
|
1065
1315
|
def has_focus?
|
|
1066
|
-
|
|
1316
|
+
view = @default_view
|
|
1317
|
+
return false unless view
|
|
1318
|
+
|
|
1319
|
+
frame = view.frame_element
|
|
1320
|
+
return true unless frame
|
|
1321
|
+
return false unless frame.is_connected?
|
|
1322
|
+
|
|
1323
|
+
parent = frame.owner_document
|
|
1324
|
+
parent.has_focus? && parent.__internal_focused_element__.equal?(frame)
|
|
1067
1325
|
end
|
|
1068
1326
|
|
|
1069
1327
|
alias has_focus has_focus?
|
|
@@ -1080,6 +1338,12 @@ module Dommy
|
|
|
1080
1338
|
Range.new(self)
|
|
1081
1339
|
end
|
|
1082
1340
|
|
|
1341
|
+
# The showing popovers and the state that orders their showing and
|
|
1342
|
+
# hiding (Internal::PopoverStack).
|
|
1343
|
+
def __internal_popover_stack__
|
|
1344
|
+
@popover_stack ||= Internal::PopoverStack.new
|
|
1345
|
+
end
|
|
1346
|
+
|
|
1083
1347
|
# Fullscreen API — no actual fullscreen mode, just track which
|
|
1084
1348
|
# element claimed it. `element.requestFullscreen()` sets it; this
|
|
1085
1349
|
# is the read side.
|
|
@@ -1090,6 +1354,8 @@ module Dommy
|
|
|
1090
1354
|
@fullscreen_element = element
|
|
1091
1355
|
return if previous == element
|
|
1092
1356
|
|
|
1357
|
+
# :fullscreen and :modal match the fullscreen element.
|
|
1358
|
+
__internal_note_selector_state_change__
|
|
1093
1359
|
dispatch_event(Event.new("fullscreenchange"))
|
|
1094
1360
|
end
|
|
1095
1361
|
|
|
@@ -1097,6 +1363,7 @@ module Dommy
|
|
|
1097
1363
|
return PromiseValue.resolve(@default_view, nil) if @fullscreen_element.nil?
|
|
1098
1364
|
|
|
1099
1365
|
@fullscreen_element = nil
|
|
1366
|
+
__internal_note_selector_state_change__
|
|
1100
1367
|
dispatch_event(Event.new("fullscreenchange"))
|
|
1101
1368
|
PromiseValue.resolve(@default_view, nil)
|
|
1102
1369
|
end
|
|
@@ -1215,10 +1482,18 @@ module Dommy
|
|
|
1215
1482
|
end
|
|
1216
1483
|
end
|
|
1217
1484
|
|
|
1485
|
+
# Pre-insert refuses an element before a doctype `child` or one that
|
|
1486
|
+
# follows it; replace (`exclude` is the child replaced) only one that
|
|
1487
|
+
# follows it, since the doctype being replaced goes.
|
|
1488
|
+
doctype_in_the_way =
|
|
1489
|
+
if exclude
|
|
1490
|
+
doctype_after_child?(existing, child_bn, inclusive: false)
|
|
1491
|
+
else
|
|
1492
|
+
(child_bn && child_bn.node_type == 10) || doctype_after_child?(existing, child_bn)
|
|
1493
|
+
end
|
|
1218
1494
|
if elements > 1
|
|
1219
1495
|
raise DOMException::HierarchyRequestError, "Only one element may be a child of a document."
|
|
1220
|
-
elsif elements == 1 &&
|
|
1221
|
-
(has_element || (child_bn && child_bn.node_type == 10) || doctype_after_child?(existing, child_bn))
|
|
1496
|
+
elsif elements == 1 && (has_element || doctype_in_the_way)
|
|
1222
1497
|
raise DOMException::HierarchyRequestError, "An element cannot be inserted here."
|
|
1223
1498
|
end
|
|
1224
1499
|
|
|
@@ -1233,7 +1508,7 @@ module Dommy
|
|
|
1233
1508
|
# createDocument, which the DOM never lets throw here, catches this and
|
|
1234
1509
|
# leaves the doctype out.
|
|
1235
1510
|
def ensure_doctypes_have_nodes!(args)
|
|
1236
|
-
return unless args.any? { |a| a.is_a?(Dommy::DocumentType) &&
|
|
1511
|
+
return unless args.any? { |a| a.is_a?(Dommy::DocumentType) && a.__dommy_backend_node__.nil? }
|
|
1237
1512
|
|
|
1238
1513
|
raise DOMException::NotSupportedError, "This doctype cannot be inserted: the backend could not create it."
|
|
1239
1514
|
end
|
|
@@ -1271,19 +1546,20 @@ module Dommy
|
|
|
1271
1546
|
existing[0...idx].any? { |c| c.node_type == 1 }
|
|
1272
1547
|
end
|
|
1273
1548
|
|
|
1274
|
-
# Whether
|
|
1275
|
-
|
|
1549
|
+
# Whether a doctype child is at `child_bn` (when `inclusive`) or follows
|
|
1550
|
+
# it in the document's child list.
|
|
1551
|
+
def doctype_after_child?(existing, child_bn, inclusive: true)
|
|
1276
1552
|
idx = child_bn && existing.index { |c| c == child_bn }
|
|
1277
1553
|
return false unless idx
|
|
1278
1554
|
|
|
1279
|
-
existing[idx..].any? { |c| c.node_type == 10 }
|
|
1555
|
+
existing[(inclusive ? idx : idx + 1)..].any? { |c| c.node_type == 10 }
|
|
1280
1556
|
end
|
|
1281
1557
|
|
|
1282
1558
|
# Append a node as a child of the document itself (e.g. a comment alongside
|
|
1283
1559
|
# the document element). Adopts the node into this document.
|
|
1284
1560
|
def append_child(node)
|
|
1285
1561
|
ensure_document_insertion_validity!([node], nil)
|
|
1286
|
-
return node unless node.
|
|
1562
|
+
return node unless node.is_a?(Node) && node.__dommy_backend_node__
|
|
1287
1563
|
|
|
1288
1564
|
# An append has a null reference child, so insert step 5 shifts nothing.
|
|
1289
1565
|
nodes = document_insertion_nodes([node])
|
|
@@ -1299,6 +1575,8 @@ module Dommy
|
|
|
1299
1575
|
# a doctype only ahead of the document element), so each forwards to the
|
|
1300
1576
|
# `document_*` implementation that carries them.
|
|
1301
1577
|
def insert_before(node, reference)
|
|
1578
|
+
Internal::WebIDL.node!(node)
|
|
1579
|
+
Internal::WebIDL.nullable_node!(reference)
|
|
1302
1580
|
document_insert_before(node, reference)
|
|
1303
1581
|
end
|
|
1304
1582
|
|
|
@@ -1343,7 +1621,7 @@ module Dommy
|
|
|
1343
1621
|
detach_node(bn, moving: true) # steps 10-11, 14
|
|
1344
1622
|
ref_bn = nil if ref_bn && ref_bn.parent != @backend_doc
|
|
1345
1623
|
__internal_ranges_will_insert__(@backend_doc, ref_bn, 1) # step 16
|
|
1346
|
-
new_previous = ref_bn ? ref_bn.previous_sibling : @backend_doc.
|
|
1624
|
+
new_previous = ref_bn ? ref_bn.previous_sibling : @backend_doc.last_child
|
|
1347
1625
|
ref_bn ? ref_bn.add_previous_sibling(bn) : @backend_doc.add_child(bn) # step 18
|
|
1348
1626
|
if old_parent
|
|
1349
1627
|
notify_child_list_mutation(
|
|
@@ -1358,22 +1636,20 @@ module Dommy
|
|
|
1358
1636
|
nil
|
|
1359
1637
|
end
|
|
1360
1638
|
|
|
1361
|
-
# The backend node an argument to `moveBefore` stands for. A
|
|
1362
|
-
#
|
|
1363
|
-
# has to see (step 2 rejects it, step 3 measures its parentage).
|
|
1639
|
+
# The backend node an argument to `moveBefore` stands for. A
|
|
1640
|
+
# Dommy::Document's `__dommy_backend_node__` is nil, but it is a node the
|
|
1641
|
+
# algorithm has to see (step 2 rejects it, step 3 measures its parentage).
|
|
1364
1642
|
def move_backend_node(value)
|
|
1365
1643
|
return value.backend_doc if value.is_a?(Dommy::Document)
|
|
1366
|
-
return nil unless value.respond_to?(:__dommy_backend_node__)
|
|
1367
1644
|
|
|
1368
|
-
value.__dommy_backend_node__
|
|
1645
|
+
value.__dommy_backend_node__ if value.is_a?(Node)
|
|
1369
1646
|
end
|
|
1370
1647
|
|
|
1371
1648
|
# "Move" steps 1-6 with a document new parent.
|
|
1372
1649
|
def ensure_document_move_validity!(node, bn, ref_bn)
|
|
1373
1650
|
# Step 1 — the same root. A document is its own root, so this says the
|
|
1374
1651
|
# node must already be somewhere in this document.
|
|
1375
|
-
root = bn
|
|
1376
|
-
root = root.parent while root.respond_to?(:parent) && root.parent
|
|
1652
|
+
root = bn && Internal::NodeTraversal.root_of(bn)
|
|
1377
1653
|
# `==` and not `equal?`: a backend may hand back a fresh Ruby object for
|
|
1378
1654
|
# the same underlying node on every `parent` call.
|
|
1379
1655
|
unless bn && root == @backend_doc
|
|
@@ -1416,11 +1692,12 @@ module Dommy
|
|
|
1416
1692
|
# ParentNode / Node mutation on the document's direct children (the doctype
|
|
1417
1693
|
# and the document element).
|
|
1418
1694
|
def document_insert(args, prepend:)
|
|
1419
|
-
|
|
1695
|
+
args = Internal::InsertionPoint.convert_nodes_into_a_node(self, args)
|
|
1696
|
+
ref_bn = prepend ? @backend_doc.first_child : nil
|
|
1420
1697
|
ensure_document_insertion_validity!(args, ref_bn)
|
|
1421
1698
|
__internal_ranges_will_insert__(@backend_doc, ref_bn, document_insertion_count(args))
|
|
1422
1699
|
nodes = document_insertion_nodes(args)
|
|
1423
|
-
if prepend && (first = @backend_doc.
|
|
1700
|
+
if prepend && (first = @backend_doc.first_child)
|
|
1424
1701
|
nodes.reverse_each { |n| first.add_previous_sibling(n) }
|
|
1425
1702
|
else
|
|
1426
1703
|
nodes.each { |n| @backend_doc.add_child(n) }
|
|
@@ -1430,6 +1707,7 @@ module Dommy
|
|
|
1430
1707
|
end
|
|
1431
1708
|
|
|
1432
1709
|
def document_replace_children(args)
|
|
1710
|
+
args = Internal::InsertionPoint.convert_nodes_into_a_node(self, args)
|
|
1433
1711
|
# replaceChildren removes the current children first, so the validity
|
|
1434
1712
|
# checks ignore them (whatwg/dom#1045).
|
|
1435
1713
|
ensure_document_insertion_validity!(args, nil, ignore_existing: true)
|
|
@@ -1442,7 +1720,13 @@ module Dommy
|
|
|
1442
1720
|
end
|
|
1443
1721
|
|
|
1444
1722
|
def document_remove_child(node)
|
|
1445
|
-
|
|
1723
|
+
if node.is_a?(DocumentType)
|
|
1724
|
+
bn = backend_node(node)
|
|
1725
|
+
raise DOMException::NotFoundError, "node is not a child of this document" if bn && bn.parent != @backend_doc
|
|
1726
|
+
|
|
1727
|
+
__internal_remove_doctype__(node)
|
|
1728
|
+
return node
|
|
1729
|
+
end
|
|
1446
1730
|
|
|
1447
1731
|
bn = backend_node(node)
|
|
1448
1732
|
raise DOMException::NotFoundError, "node is not a child of this document" unless bn && bn.parent == @backend_doc
|
|
@@ -1500,8 +1784,7 @@ module Dommy
|
|
|
1500
1784
|
# read before the adopt below removes anything.
|
|
1501
1785
|
record_previous = old_bn.previous && wrap_node(old_bn.previous)
|
|
1502
1786
|
record_next = ref && wrap_node(ref)
|
|
1503
|
-
cross_document =
|
|
1504
|
-
new_child.respond_to?(:document) && !new_child.document.equal?(self)
|
|
1787
|
+
cross_document = new_child.respond_to?(:document) && !new_child.document.equal?(self)
|
|
1505
1788
|
fragment = new_child.is_a?(Dommy::Fragment)
|
|
1506
1789
|
|
|
1507
1790
|
# Same document: WHATWG replace adopts the incoming node — which removes
|
|
@@ -1557,7 +1840,7 @@ module Dommy
|
|
|
1557
1840
|
# (at the document start) or after it (just before the document element).
|
|
1558
1841
|
def __internal_insert_at_doctype__(nodes, after:)
|
|
1559
1842
|
bns = nodes.filter_map { |n| backend_node(n) }
|
|
1560
|
-
anchor = after ?
|
|
1843
|
+
anchor = after ? backend_document_element : @backend_doc.first_child
|
|
1561
1844
|
__internal_ranges_will_insert__(@backend_doc, anchor, bns.size)
|
|
1562
1845
|
if after
|
|
1563
1846
|
anchor ? bns.each { |n| anchor.add_previous_sibling(n) } : bns.each { |n| @backend_doc.add_child(n) }
|
|
@@ -1572,9 +1855,13 @@ module Dommy
|
|
|
1572
1855
|
# deep clone then copies the original's children into it — so nothing
|
|
1573
1856
|
# sprouts an html/head/body of its own, and a document with no children
|
|
1574
1857
|
# clones to one with no children.
|
|
1575
|
-
def clone_node(deep)
|
|
1858
|
+
def clone_node(deep = false)
|
|
1859
|
+
deep = Internal::WebIDL.boolean(deep)
|
|
1576
1860
|
copy = Document.new(nil, backend_doc: Backend.empty_document_like(@backend_doc))
|
|
1577
1861
|
copy.content_type = @content_type
|
|
1862
|
+
copy.__internal_xml_document__ = @xml_document
|
|
1863
|
+
copy.__internal_quirks_mode__ = quirks_mode?
|
|
1864
|
+
copy.__internal_allow_declarative_shadow_roots__ = __internal_allow_declarative_shadow_roots__?
|
|
1578
1865
|
return copy unless deep
|
|
1579
1866
|
|
|
1580
1867
|
child_nodes.each { |child| copy.append_child(copy.import_node(child, true)) }
|
|
@@ -1582,7 +1869,7 @@ module Dommy
|
|
|
1582
1869
|
end
|
|
1583
1870
|
|
|
1584
1871
|
def backend_node(node)
|
|
1585
|
-
node.
|
|
1872
|
+
node.__dommy_backend_node__ if node.is_a?(Node)
|
|
1586
1873
|
end
|
|
1587
1874
|
|
|
1588
1875
|
# Like `backend_node`, but first adopts a node that belongs to another
|
|
@@ -1624,9 +1911,9 @@ module Dommy
|
|
|
1624
1911
|
def document_insertion_count(args) = Internal::InsertionPoint.count(args)
|
|
1625
1912
|
|
|
1626
1913
|
def adopted_backend_node(node)
|
|
1627
|
-
return nil unless node.
|
|
1914
|
+
return nil unless node.is_a?(Node) && node.__dommy_backend_node__
|
|
1628
1915
|
|
|
1629
|
-
if
|
|
1916
|
+
if node.respond_to?(:document) && !node.document.equal?(self)
|
|
1630
1917
|
return adopt_node(node)&.__dommy_backend_node__
|
|
1631
1918
|
end
|
|
1632
1919
|
|
|
@@ -1635,19 +1922,42 @@ module Dommy
|
|
|
1635
1922
|
bn
|
|
1636
1923
|
end
|
|
1637
1924
|
|
|
1638
|
-
#
|
|
1639
|
-
|
|
1925
|
+
# `document.cookie` (HTML §3.1.5): the cookie-string of the window's
|
|
1926
|
+
# cookie jar for this document's URL, as a "non-HTTP" API sees it (no
|
|
1927
|
+
# HttpOnly cookies). A cookie-averse document — no browsing context, or a
|
|
1928
|
+
# URL that is not http(s) — reads "" and ignores writes; one with an
|
|
1929
|
+
# opaque origin throws SecurityError.
|
|
1640
1930
|
def cookie
|
|
1641
|
-
|
|
1931
|
+
return "" if __internal_cookie_averse__?
|
|
1932
|
+
|
|
1933
|
+
raise_if_opaque_for_cookies!
|
|
1934
|
+
@default_view.cookie_jar.cookie_string(url, http: false)
|
|
1642
1935
|
end
|
|
1643
1936
|
|
|
1644
1937
|
def cookie=(value)
|
|
1645
|
-
|
|
1938
|
+
return nil if __internal_cookie_averse__?
|
|
1939
|
+
|
|
1940
|
+
raise_if_opaque_for_cookies!
|
|
1941
|
+
@default_view.cookie_jar.store(value.to_s, url, http: false)
|
|
1646
1942
|
nil
|
|
1647
1943
|
end
|
|
1648
1944
|
|
|
1649
|
-
|
|
1650
|
-
|
|
1945
|
+
# HTML "cookie-averse Document object".
|
|
1946
|
+
def __internal_cookie_averse__?
|
|
1947
|
+
return true unless @default_view.respond_to?(:cookie_jar)
|
|
1948
|
+
|
|
1949
|
+
!url.to_s.match?(/\Ahttps?:/i)
|
|
1950
|
+
end
|
|
1951
|
+
|
|
1952
|
+
def raise_if_opaque_for_cookies!
|
|
1953
|
+
return unless @default_view.origin == "null"
|
|
1954
|
+
|
|
1955
|
+
raise DOMException::SecurityError, "the document's origin is opaque"
|
|
1956
|
+
end
|
|
1957
|
+
private :raise_if_opaque_for_cookies!
|
|
1958
|
+
|
|
1959
|
+
def create_element_ns(namespace_uri, qualified_name, options = nil)
|
|
1960
|
+
@node_factory.create_element_ns(namespace_uri, qualified_name, options)
|
|
1651
1961
|
end
|
|
1652
1962
|
|
|
1653
1963
|
def get_elements_by_tag_name(name)
|
|
@@ -1662,27 +1972,68 @@ module Dommy
|
|
|
1662
1972
|
HTMLCollection.elements_by_tag_name_ns(@backend_doc, self, namespace, local_name)
|
|
1663
1973
|
end
|
|
1664
1974
|
|
|
1665
|
-
#
|
|
1666
|
-
#
|
|
1667
|
-
#
|
|
1975
|
+
# ----- Dynamic markup insertion (document.open / write / close) -----
|
|
1976
|
+
#
|
|
1977
|
+
# Dommy has no incremental parser, so this models the spec's input stream
|
|
1978
|
+
# without one:
|
|
1979
|
+
#
|
|
1980
|
+
# - While the page's own scripts boot (readyState "loading" and a script
|
|
1981
|
+
# running) the parser counts as active with a script nesting level above
|
|
1982
|
+
# 0: `open()` is a no-op that returns the document, and `write()` inserts
|
|
1983
|
+
# the parsed markup right after the running script (where the insertion
|
|
1984
|
+
# point would be), or at the end of the body when that script is not in
|
|
1985
|
+
# the body.
|
|
1986
|
+
# - Otherwise `write()` with no script-created parser runs the document open
|
|
1987
|
+
# steps first (the page is gone), and every write re-parses the whole
|
|
1988
|
+
# input written since `open()` as a fresh document, whose children replace
|
|
1989
|
+
# the document's. Earlier writes' nodes are therefore not the same objects
|
|
1990
|
+
# after a later write, and scripts in written markup do not run.
|
|
1991
|
+
#
|
|
1992
|
+
# Spec: https://html.spec.whatwg.org/#dynamic-markup-insertion
|
|
1993
|
+
|
|
1994
|
+
# `document.write(...text)` — the document write steps with lineFeed false.
|
|
1668
1995
|
def write(*args)
|
|
1669
|
-
|
|
1670
|
-
fragment = Parser.fragment(html, owner_doc: @backend_doc)
|
|
1671
|
-
removed = []
|
|
1672
|
-
added = fragment.children.to_a
|
|
1673
|
-
body_node = body.__dommy_backend_node__
|
|
1674
|
-
added.each { |node| body_node.add_child(node) }
|
|
1675
|
-
notify_child_list_mutation(target_node: body_node, added_nodes: added, removed_nodes: removed)
|
|
1676
|
-
nil
|
|
1996
|
+
document_write(args, line_feed: false)
|
|
1677
1997
|
end
|
|
1678
1998
|
|
|
1679
|
-
#
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
nil
|
|
1999
|
+
# `document.writeln(...text)` — the document write steps with lineFeed true.
|
|
2000
|
+
def writeln(*args)
|
|
2001
|
+
document_write(args, line_feed: true)
|
|
1683
2002
|
end
|
|
1684
2003
|
|
|
2004
|
+
# `document.open()` — the document open steps; returns the document. With
|
|
2005
|
+
# three arguments it is `window.open(url, name, features)` instead.
|
|
2006
|
+
#
|
|
2007
|
+
# Spec: https://html.spec.whatwg.org/#dom-document-open
|
|
2008
|
+
def open(*args)
|
|
2009
|
+
return open_window(args) if args.length >= 3
|
|
2010
|
+
|
|
2011
|
+
document_open_steps
|
|
2012
|
+
end
|
|
2013
|
+
|
|
2014
|
+
# `document.close()` — closes the input stream a script-created parser
|
|
2015
|
+
# reads: the parser reaches EOF and "stops parsing", so the document goes
|
|
2016
|
+
# "interactive" (DOMContentLoaded) then "complete" (load at the window,
|
|
2017
|
+
# when there is one). Dispatched synchronously rather than as queued tasks.
|
|
2018
|
+
#
|
|
2019
|
+
# Spec: https://html.spec.whatwg.org/#dom-document-close
|
|
1685
2020
|
def close
|
|
2021
|
+
raise DOMException::InvalidStateError, "close() is not supported on an XML document" unless html_document?
|
|
2022
|
+
raise_if_markup_insertion_forbidden!("close")
|
|
2023
|
+
return nil if @script_created_input.nil?
|
|
2024
|
+
|
|
2025
|
+
# The parser reaches EOF. With nothing written it has seen no doctype
|
|
2026
|
+
# (quirks mode) and, into an empty document, builds html/head/body.
|
|
2027
|
+
if @script_created_input.empty?
|
|
2028
|
+
if @backend_doc.children.to_a.empty?
|
|
2029
|
+
reparse_script_created_input
|
|
2030
|
+
else
|
|
2031
|
+
@quirks_mode = true
|
|
2032
|
+
end
|
|
2033
|
+
end
|
|
2034
|
+
@script_created_input = nil
|
|
2035
|
+
__internal_set_ready_state__("interactive")
|
|
2036
|
+
__internal_set_ready_state__("complete")
|
|
1686
2037
|
nil
|
|
1687
2038
|
end
|
|
1688
2039
|
|
|
@@ -1722,12 +2073,11 @@ module Dommy
|
|
|
1722
2073
|
end
|
|
1723
2074
|
|
|
1724
2075
|
def __js_get__(key)
|
|
1725
|
-
if
|
|
1726
|
-
# An event handler IDL attribute (GlobalEventHandlers
|
|
1727
|
-
#
|
|
1728
|
-
#
|
|
1729
|
-
|
|
1730
|
-
return @on_handlers&.[](event_name_from_on(key))
|
|
2076
|
+
if Internal::EventHandlers.idl_attribute?(self, key)
|
|
2077
|
+
# An event handler IDL attribute Document declares (GlobalEventHandlers,
|
|
2078
|
+
# plus onreadystatechange, onvisibilitychange, onfullscreenchange, …):
|
|
2079
|
+
# the registered handler, or null when unset — matching Element's.
|
|
2080
|
+
return on_handler(event_name_from_on(key))
|
|
1731
2081
|
end
|
|
1732
2082
|
|
|
1733
2083
|
case key
|
|
@@ -1753,10 +2103,17 @@ module Dommy
|
|
|
1753
2103
|
read_title
|
|
1754
2104
|
when "dir"
|
|
1755
2105
|
dir
|
|
2106
|
+
when "fgColor" then fg_color
|
|
2107
|
+
when "linkColor" then link_color
|
|
2108
|
+
when "vlinkColor" then vlink_color
|
|
2109
|
+
when "alinkColor" then alink_color
|
|
2110
|
+
when "bgColor" then bg_color
|
|
1756
2111
|
when "cookie"
|
|
1757
2112
|
cookie
|
|
1758
2113
|
when "nodeType"
|
|
1759
2114
|
9
|
|
2115
|
+
when "customElementRegistry"
|
|
2116
|
+
__internal_custom_element_registry__
|
|
1760
2117
|
when "isConnected"
|
|
1761
2118
|
# A document is its own shadow-including root, so it is always connected.
|
|
1762
2119
|
true
|
|
@@ -1784,7 +2141,7 @@ module Dommy
|
|
|
1784
2141
|
when "designMode"
|
|
1785
2142
|
@design_mode || "off"
|
|
1786
2143
|
when "lastModified"
|
|
1787
|
-
|
|
2144
|
+
last_modified
|
|
1788
2145
|
when "readyState"
|
|
1789
2146
|
# "complete" by default (the document is fully parsed before scripts
|
|
1790
2147
|
# run); an embedder can replay "loading" → "interactive" → "complete"
|
|
@@ -1854,20 +2211,36 @@ module Dommy
|
|
|
1854
2211
|
end
|
|
1855
2212
|
end
|
|
1856
2213
|
|
|
2214
|
+
# The elements of this document tree that can give its Window a named
|
|
2215
|
+
# property (HTML §7.2.2.3 "Named access on the Window object"), in tree
|
|
2216
|
+
# order: navigable containers, elements with an id, and embed / form /
|
|
2217
|
+
# img / object elements with a name. Window#__js_named_props__ applies the
|
|
2218
|
+
# spec's rules to them.
|
|
2219
|
+
def __internal_window_named_candidates__
|
|
2220
|
+
@backend_doc.css("[id], [name], iframe, frame").filter_map { |node| wrap_node(node) }
|
|
2221
|
+
end
|
|
2222
|
+
|
|
1857
2223
|
# The document's supported property names (for `"name" in document`): for
|
|
1858
2224
|
# each exposed element in tree order, its id when it is a named element with
|
|
1859
2225
|
# that name, then its name.
|
|
1860
2226
|
def __js_named_props__
|
|
1861
2227
|
names = []
|
|
1862
2228
|
named_getter_nodes.each do |node|
|
|
1863
|
-
id = node
|
|
2229
|
+
id = Backend.no_namespace_attribute_value(node, "id").to_s
|
|
1864
2230
|
names << id if named_element?(node, id)
|
|
1865
|
-
name = node
|
|
2231
|
+
name = Backend.no_namespace_attribute_value(node, "name").to_s
|
|
1866
2232
|
names << name if named_element?(node, name)
|
|
1867
2233
|
end
|
|
1868
2234
|
names.uniq
|
|
1869
2235
|
end
|
|
1870
2236
|
|
|
2237
|
+
# The named getter on its own, for the bridge to read a supported name
|
|
2238
|
+
# BEFORE the builtins ([LegacyOverrideBuiltIns]: `<form name=body>` makes
|
|
2239
|
+
# `document.body` that form).
|
|
2240
|
+
def __js_named_get__(name)
|
|
2241
|
+
document_named_property(name)
|
|
2242
|
+
end
|
|
2243
|
+
|
|
1871
2244
|
# Resolve a document named-getter property: nil when unsupported, a single
|
|
1872
2245
|
# element (a named iframe yields its content window), or an HTMLCollection
|
|
1873
2246
|
# when several elements share the name.
|
|
@@ -1884,39 +2257,8 @@ module Dommy
|
|
|
1884
2257
|
el.local_name.to_s.casecmp?("iframe") && el.respond_to?(:content_window) ? (el.content_window || el) : el
|
|
1885
2258
|
end
|
|
1886
2259
|
|
|
1887
|
-
private
|
|
1888
|
-
|
|
1889
|
-
# "The html element": the document element when it is an HTML <html>.
|
|
1890
|
-
def html_element
|
|
1891
|
-
root = document_element
|
|
1892
|
-
root if root.is_a?(HTMLElement) && root.local_name == "html"
|
|
1893
|
-
end
|
|
1894
|
-
|
|
1895
|
-
# An element is a named element with the name `name` when it is one of the
|
|
1896
|
-
# exposed kinds and either carries that `name`, or is an object with that
|
|
1897
|
-
# `id`, or is an img whose id it is and which also has a non-empty name.
|
|
1898
|
-
def named_element?(node, name)
|
|
1899
|
-
return false if name.empty?
|
|
1900
|
-
return false unless %w[embed form iframe img object].include?(node.name.to_s.downcase)
|
|
1901
|
-
return true if node["name"] == name
|
|
1902
|
-
return true if node.name.to_s.casecmp?("object") && node["id"] == name
|
|
1903
|
-
|
|
1904
|
-
node.name.to_s.casecmp?("img") && node["id"] == name && !node["name"].to_s.empty?
|
|
1905
|
-
end
|
|
1906
|
-
|
|
1907
|
-
# Elements the document's named getter exposes, in tree order.
|
|
1908
|
-
def named_getter_nodes
|
|
1909
|
-
@backend_doc.css("embed, form, iframe, img, object")
|
|
1910
|
-
end
|
|
1911
|
-
|
|
1912
|
-
def document_named_property_nodes(name)
|
|
1913
|
-
named_getter_nodes.select { |node| named_element?(node, name) }.map { |node| wrap_node(node) }.compact
|
|
1914
|
-
end
|
|
1915
|
-
|
|
1916
|
-
public
|
|
1917
|
-
|
|
1918
2260
|
def __js_set__(key, value)
|
|
1919
|
-
if
|
|
2261
|
+
if Internal::EventHandlers.idl_attribute?(self, key)
|
|
1920
2262
|
# `document.onXxx = fn` registers fn as a single named handler; nil
|
|
1921
2263
|
# removes it. Without this the assignment became a plain JS expando and
|
|
1922
2264
|
# the handler never fired (e.g. `document.onreadystatechange`).
|
|
@@ -1931,14 +2273,27 @@ module Dommy
|
|
|
1931
2273
|
self.cookie = value.to_s
|
|
1932
2274
|
when "dir"
|
|
1933
2275
|
self.dir = value
|
|
2276
|
+
when "fgColor" then self.fg_color = value
|
|
2277
|
+
when "linkColor" then self.link_color = value
|
|
2278
|
+
when "vlinkColor" then self.vlink_color = value
|
|
2279
|
+
when "alinkColor" then self.alink_color = value
|
|
2280
|
+
when "bgColor" then self.bg_color = value
|
|
1934
2281
|
when "designMode"
|
|
1935
2282
|
# Enumerated: only "on"/"off" (case-insensitive), else ignored.
|
|
1936
2283
|
v = value.to_s.downcase
|
|
1937
2284
|
@design_mode = v if %w[on off].include?(v)
|
|
2285
|
+
when "domain"
|
|
2286
|
+
self.domain = value.to_s
|
|
1938
2287
|
when "location"
|
|
1939
2288
|
# `document.location = url` navigates, same as `location.href = url`.
|
|
1940
2289
|
loc = @default_view&.__js_get__("location")
|
|
1941
2290
|
loc&.__js_set__("href", value)
|
|
2291
|
+
when "body"
|
|
2292
|
+
# WebIDL `attribute HTMLElement? body`: anything but an HTMLElement or
|
|
2293
|
+
# null fails the conversion before the setter's own checks run.
|
|
2294
|
+
raise Bridge::TypeError, "document.body must be an HTMLElement or null" unless value.nil? || value.is_a?(HTMLElement)
|
|
2295
|
+
|
|
2296
|
+
self.body = value
|
|
1942
2297
|
when "head", "documentElement"
|
|
1943
2298
|
# Readonly attributes: assignment is a silent no-op. Handle it here so the
|
|
1944
2299
|
# bridge doesn't store a JS-side expando that would shadow the getter.
|
|
@@ -1998,9 +2353,8 @@ module Dommy
|
|
|
1998
2353
|
document_remove_child(args[0])
|
|
1999
2354
|
when "insertBefore"
|
|
2000
2355
|
raise Bridge::TypeError, "insertBefore requires 2 arguments." if args.length < 2
|
|
2001
|
-
Internal::WebIDL.nullable_node!(args[1])
|
|
2002
2356
|
|
|
2003
|
-
|
|
2357
|
+
insert_before(args[0], args[1])
|
|
2004
2358
|
when "replaceChild"
|
|
2005
2359
|
document_replace_child(args[0], args[1])
|
|
2006
2360
|
when "moveBefore"
|
|
@@ -2013,7 +2367,8 @@ module Dommy
|
|
|
2013
2367
|
when "normalize"
|
|
2014
2368
|
normalize
|
|
2015
2369
|
when "writeln"
|
|
2016
|
-
|
|
2370
|
+
writeln(*args)
|
|
2371
|
+
Bridge::UNDEFINED
|
|
2017
2372
|
when "exitFullscreen"
|
|
2018
2373
|
exit_fullscreen
|
|
2019
2374
|
when "startViewTransition"
|
|
@@ -2021,17 +2376,13 @@ module Dommy
|
|
|
2021
2376
|
# synchronously; return a ViewTransition with already-resolved
|
|
2022
2377
|
# `finished` / `ready` / `updateCallbackDone` promises.
|
|
2023
2378
|
callback = args[0]
|
|
2024
|
-
|
|
2025
|
-
callback.__js_call__("call", [])
|
|
2026
|
-
elsif callback.respond_to?(:call)
|
|
2027
|
-
callback.call
|
|
2028
|
-
end
|
|
2379
|
+
CallableInvoker.invoke(callback)
|
|
2029
2380
|
|
|
2030
2381
|
ViewTransition.new(@default_view)
|
|
2031
2382
|
when "createElement"
|
|
2032
|
-
create_element(args[0])
|
|
2383
|
+
create_element(args[0], args[1])
|
|
2033
2384
|
when "createElementNS"
|
|
2034
|
-
create_element_ns(args[0], args[1])
|
|
2385
|
+
create_element_ns(args[0], args[1], args[2])
|
|
2035
2386
|
when "createTextNode"
|
|
2036
2387
|
create_text_node(args[0])
|
|
2037
2388
|
when "createComment"
|
|
@@ -2069,7 +2420,8 @@ module Dommy
|
|
|
2069
2420
|
when "createEvent"
|
|
2070
2421
|
create_event(args[0])
|
|
2071
2422
|
when "importNode"
|
|
2072
|
-
|
|
2423
|
+
# A missing options argument is undefined (the default), not null.
|
|
2424
|
+
import_node(args[0], args.fetch(1, Bridge::UNDEFINED))
|
|
2073
2425
|
when "adoptNode"
|
|
2074
2426
|
adopt_node(args[0])
|
|
2075
2427
|
when "hasFocus"
|
|
@@ -2088,10 +2440,12 @@ module Dommy
|
|
|
2088
2440
|
dispatch_event(args[0])
|
|
2089
2441
|
when "write"
|
|
2090
2442
|
write(*args)
|
|
2443
|
+
Bridge::UNDEFINED
|
|
2091
2444
|
when "open"
|
|
2092
|
-
open
|
|
2445
|
+
open(*args)
|
|
2093
2446
|
when "close"
|
|
2094
2447
|
close
|
|
2448
|
+
Bridge::UNDEFINED
|
|
2095
2449
|
else
|
|
2096
2450
|
nil
|
|
2097
2451
|
end
|
|
@@ -2112,16 +2466,113 @@ module Dommy
|
|
|
2112
2466
|
return if @ready_state == state
|
|
2113
2467
|
|
|
2114
2468
|
@ready_state = state
|
|
2115
|
-
|
|
2469
|
+
@loading_lifecycle = true if state == "loading"
|
|
2470
|
+
__internal_fire_event__("readystatechange")
|
|
2116
2471
|
case state
|
|
2117
2472
|
when "interactive"
|
|
2118
|
-
|
|
2473
|
+
__internal_fire_event__("DOMContentLoaded", {"bubbles" => true})
|
|
2119
2474
|
when "complete"
|
|
2120
|
-
|
|
2475
|
+
fire_load_and_pageshow
|
|
2121
2476
|
end
|
|
2122
2477
|
nil
|
|
2123
2478
|
end
|
|
2124
2479
|
|
|
2480
|
+
# The document's current readiness ("loading" / "interactive" /
|
|
2481
|
+
# "complete").
|
|
2482
|
+
def __internal_ready_state__ = @ready_state
|
|
2483
|
+
|
|
2484
|
+
# HTML "update the current document readiness" alone: readyState becomes
|
|
2485
|
+
# `state` and `readystatechange` fires — without the milestone event
|
|
2486
|
+
# __internal_set_ready_state__ adds. The parser's "the end" fires
|
|
2487
|
+
# DOMContentLoaded and load from tasks of their own (see ScriptBoot).
|
|
2488
|
+
def __internal_update_readiness__(state)
|
|
2489
|
+
state = state.to_s
|
|
2490
|
+
return nil if @ready_state == state
|
|
2491
|
+
|
|
2492
|
+
@ready_state = state
|
|
2493
|
+
@loading_lifecycle = true if state == "loading"
|
|
2494
|
+
__internal_fire_event__("readystatechange")
|
|
2495
|
+
nil
|
|
2496
|
+
end
|
|
2497
|
+
|
|
2498
|
+
# "The end", its DOMContentLoaded task: fire a trusted, bubbling
|
|
2499
|
+
# `DOMContentLoaded` at the document.
|
|
2500
|
+
def __internal_fire_dom_content_loaded__
|
|
2501
|
+
__internal_fire_event__("DOMContentLoaded", {"bubbles" => true})
|
|
2502
|
+
nil
|
|
2503
|
+
end
|
|
2504
|
+
|
|
2505
|
+
# "The end", its load task: readiness becomes "complete", then `load` at
|
|
2506
|
+
# the window and `pageshow`, and the document is completely loaded.
|
|
2507
|
+
def __internal_finish_loading__
|
|
2508
|
+
return nil unless @loading_lifecycle
|
|
2509
|
+
|
|
2510
|
+
__internal_update_readiness__("complete")
|
|
2511
|
+
fire_load_and_pageshow
|
|
2512
|
+
nil
|
|
2513
|
+
end
|
|
2514
|
+
|
|
2515
|
+
# Whether something delays this document's load event: a child navigable
|
|
2516
|
+
# still navigating (HTML "potentially delays the load event", iframe).
|
|
2517
|
+
def __internal_load_delayed__?
|
|
2518
|
+
@backend_doc.css("iframe").any? do |node|
|
|
2519
|
+
frame = wrap_node(node)
|
|
2520
|
+
frame.is_a?(HTMLIFrameElement) && frame.__internal_navigation_pending__?
|
|
2521
|
+
end
|
|
2522
|
+
end
|
|
2523
|
+
|
|
2524
|
+
# The end of loading, from "update the current document readiness to
|
|
2525
|
+
# complete": fire a trusted `load` at the window with the legacy target
|
|
2526
|
+
# override (so `event.target` is this document), then `pageshow` (persisted
|
|
2527
|
+
# false), and mark the document completely loaded — ready for post-load
|
|
2528
|
+
# tasks such as a print() requested while it loaded.
|
|
2529
|
+
def fire_load_and_pageshow
|
|
2530
|
+
view = @default_view
|
|
2531
|
+
if view
|
|
2532
|
+
load = Event.new("load")
|
|
2533
|
+
load.__internal_set_target__(self)
|
|
2534
|
+
view.dispatch_event(load.__internal_mark_trusted__)
|
|
2535
|
+
view.dispatch_event(__internal_page_transition_event__("pageshow"))
|
|
2536
|
+
end
|
|
2537
|
+
@loading_lifecycle = false
|
|
2538
|
+
view.__internal_ready_for_post_load_tasks__ if view.respond_to?(:__internal_ready_for_post_load_tasks__)
|
|
2539
|
+
run_after_load_tasks
|
|
2540
|
+
end
|
|
2541
|
+
|
|
2542
|
+
# Queue `block` as a task once this document has completely loaded (at
|
|
2543
|
+
# once when it already has).
|
|
2544
|
+
def __internal_after_load__(&block)
|
|
2545
|
+
if __internal_completely_loaded__?
|
|
2546
|
+
queue_after_load_task(block)
|
|
2547
|
+
else
|
|
2548
|
+
(@after_load_tasks ||= []) << block
|
|
2549
|
+
end
|
|
2550
|
+
nil
|
|
2551
|
+
end
|
|
2552
|
+
|
|
2553
|
+
def run_after_load_tasks
|
|
2554
|
+
tasks = @after_load_tasks || []
|
|
2555
|
+
@after_load_tasks = nil
|
|
2556
|
+
tasks.each { |task| queue_after_load_task(task) }
|
|
2557
|
+
end
|
|
2558
|
+
private :run_after_load_tasks
|
|
2559
|
+
|
|
2560
|
+
def queue_after_load_task(block)
|
|
2561
|
+
scheduler = __internal_scheduler__
|
|
2562
|
+
scheduler ? scheduler.set_timeout(block, 0) : block.call
|
|
2563
|
+
end
|
|
2564
|
+
private :queue_after_load_task
|
|
2565
|
+
private :fire_load_and_pageshow
|
|
2566
|
+
|
|
2567
|
+
# A trusted page transition event (`pageshow` / `pagehide`) for this
|
|
2568
|
+
# document's window: bubbles, cancelable, `persisted` false (there is no
|
|
2569
|
+
# back/forward cache), targeted at the document (legacy target override).
|
|
2570
|
+
def __internal_page_transition_event__(type, persisted: false)
|
|
2571
|
+
event = PageTransitionEvent.new(type, "persisted" => persisted, "bubbles" => true, "cancelable" => true)
|
|
2572
|
+
event.__internal_set_target__(self)
|
|
2573
|
+
event.__internal_mark_trusted__
|
|
2574
|
+
end
|
|
2575
|
+
|
|
2125
2576
|
# Set `document.currentScript` to the <script> element being executed (and
|
|
2126
2577
|
# back to nil afterward). The host (script boot) brackets each classic
|
|
2127
2578
|
# script run with this so code reading `document.currentScript` sees its own
|
|
@@ -2131,15 +2582,26 @@ module Dommy
|
|
|
2131
2582
|
nil
|
|
2132
2583
|
end
|
|
2133
2584
|
|
|
2585
|
+
# HTML "execute the script element", for a classic script: currentScript
|
|
2586
|
+
# is the element while its script runs (null when the element's root is a
|
|
2587
|
+
# shadow root — not "in a document tree", since a script removed before it
|
|
2588
|
+
# runs still points at itself), then goes back to whatever it was before,
|
|
2589
|
+
# so a script inserted and run from inside another script leaves the outer
|
|
2590
|
+
# one current again. A caller reports the script's exception from inside
|
|
2591
|
+
# the block: the report is part of the run, while currentScript is set.
|
|
2592
|
+
def __internal_with_current_script__(element)
|
|
2593
|
+
old = @__current_script__
|
|
2594
|
+
@__current_script__ = element.root_node.is_a?(ShadowRoot) ? nil : element
|
|
2595
|
+
yield
|
|
2596
|
+
ensure
|
|
2597
|
+
@__current_script__ = old
|
|
2598
|
+
end
|
|
2599
|
+
|
|
2134
2600
|
# Delegate node wrapping to NodeWrapperCache
|
|
2135
2601
|
def wrap_node(node)
|
|
2136
2602
|
@node_wrapper_cache.wrap(node)
|
|
2137
2603
|
end
|
|
2138
2604
|
|
|
2139
|
-
def wrap_cloned_element_ns(node, namespace, prefix, local, qualified_name)
|
|
2140
|
-
@node_factory.wrap_cloned_element_ns(node, namespace, prefix, local, qualified_name)
|
|
2141
|
-
end
|
|
2142
|
-
|
|
2143
2605
|
# The task scheduler this document's own tasks run on: its browsing context's
|
|
2144
2606
|
# when it has one, otherwise the one handed to it by whatever built it (a
|
|
2145
2607
|
# DOMParser document has no defaultView but still queues tasks on the window
|
|
@@ -2159,25 +2621,59 @@ module Dommy
|
|
|
2159
2621
|
# cleared (HTML §4.12.1.1: the HTML/XML parser clears it on every element it
|
|
2160
2622
|
# inserts, so a plain parsed `<script>` reports `.async === false`).
|
|
2161
2623
|
def __internal_run_parsed_insertion_steps__
|
|
2162
|
-
return nil unless @backend_doc.respond_to?(:css)
|
|
2163
|
-
|
|
2164
2624
|
# HTML-namespace only: a `css` query matches on local name, so a
|
|
2165
2625
|
# `<details>` the parser put inside `<svg>` answers it too, as an
|
|
2166
2626
|
# SVGElement that has none of these steps.
|
|
2167
|
-
elements =
|
|
2627
|
+
elements = parsed_css("details").filter_map { |node| __internal_html_element_wrapper__(node) }
|
|
2168
2628
|
HTMLDetailsElement.run_insertion_steps(elements) unless elements.empty?
|
|
2169
|
-
|
|
2170
|
-
|
|
2629
|
+
parsed_css("select").each { |node| __internal_html_element_wrapper__(node)&.__internal_settle_selectedness_once__ }
|
|
2630
|
+
parsed_css("script").each do |node|
|
|
2631
|
+
script = __internal_html_element_wrapper__(node)
|
|
2632
|
+
next unless script
|
|
2633
|
+
|
|
2634
|
+
script.__internal_mark_parser_inserted__
|
|
2635
|
+
script.__internal_mark_parser_document__
|
|
2636
|
+
end
|
|
2637
|
+
parsed_css("meta[http-equiv]").each { |node| __internal_html_element_wrapper__(node)&.__internal_run_pragma__ }
|
|
2638
|
+
# An open dialog the parser inserted runs its dialog setup steps.
|
|
2639
|
+
parsed_css("dialog[open]").each do |node|
|
|
2640
|
+
dialog = __internal_html_element_wrapper__(node)
|
|
2641
|
+
dialog.__internal_dialog_inserted__ if dialog.respond_to?(:__internal_dialog_inserted__)
|
|
2642
|
+
end
|
|
2643
|
+
# Each element the parser inserted with an autofocus attribute is an
|
|
2644
|
+
# autofocus candidate (only in a document with a browsing context).
|
|
2645
|
+
parsed_css("[autofocus]").each do |node|
|
|
2646
|
+
element = wrap_node(node)
|
|
2647
|
+
__internal_autofocus_inserted__(element) if element.respond_to?(:autofocus) && element.is_connected?
|
|
2648
|
+
end
|
|
2649
|
+
nil
|
|
2650
|
+
end
|
|
2651
|
+
|
|
2652
|
+
# Script boot replays the parser's insertion of each `<iframe>` (in tree
|
|
2653
|
+
# order): its post-connection steps create its child navigable and process
|
|
2654
|
+
# its attributes, so a srcless one fires `load` and one with a `src` starts
|
|
2655
|
+
# navigating. Only a document with a browsing context has child navigables.
|
|
2656
|
+
def __internal_process_parsed_iframes__
|
|
2657
|
+
return nil unless @default_view
|
|
2658
|
+
|
|
2659
|
+
parsed_css("iframe").each do |node|
|
|
2660
|
+
frame = __internal_html_element_wrapper__(node)
|
|
2661
|
+
frame.__internal_parser_inserted__ if frame.is_a?(HTMLIFrameElement)
|
|
2662
|
+
end
|
|
2171
2663
|
nil
|
|
2172
2664
|
end
|
|
2173
2665
|
|
|
2666
|
+
# HTML's pragma-set default language (`<meta http-equiv=content-language
|
|
2667
|
+
# content=…>`): nil until such a pragma has been processed.
|
|
2668
|
+
attr_accessor :__internal_pragma_default_language__
|
|
2669
|
+
|
|
2174
2670
|
# DOMParser parses with scripting disabled (HTML) or XML scripting support
|
|
2175
2671
|
# disabled, so every script it makes is "already started": moved or cloned
|
|
2176
2672
|
# into a document that runs scripts, it still does not run. Found by local
|
|
2177
2673
|
# name, so an XML document's prefixed `h:script` counts too.
|
|
2178
|
-
def __internal_mark_scripts_already_started__
|
|
2179
|
-
Internal::NodeTraversal.subtree_nodes(
|
|
2180
|
-
next unless node.
|
|
2674
|
+
def __internal_mark_scripts_already_started__(root = @backend_doc)
|
|
2675
|
+
Internal::NodeTraversal.subtree_nodes(root).each do |node|
|
|
2676
|
+
next unless node.element?
|
|
2181
2677
|
next unless node.local_name == "script"
|
|
2182
2678
|
|
|
2183
2679
|
wrapper = wrap_node(node)
|
|
@@ -2217,26 +2713,6 @@ module Dommy
|
|
|
2217
2713
|
wrapper
|
|
2218
2714
|
end
|
|
2219
2715
|
|
|
2220
|
-
# The wrapper already cached for a backend node, or nil — never builds one.
|
|
2221
|
-
def __internal_cached_wrapper__(node)
|
|
2222
|
-
@node_wrapper_cache.cached_wrapper(node)
|
|
2223
|
-
end
|
|
2224
|
-
|
|
2225
|
-
# Recorded when an element is created outside the HTML namespace or with a
|
|
2226
|
-
# prefix. Deep cloning only has to carry that metadata across for a document
|
|
2227
|
-
# that has some — which the overwhelming majority never do, so the ordinary
|
|
2228
|
-
# `body.cloneNode(true)` keeps walking nothing.
|
|
2229
|
-
def __internal_note_namespaced_element__(namespace, prefix)
|
|
2230
|
-
return if namespace == Element::HTML_NAMESPACE && prefix.nil?
|
|
2231
|
-
|
|
2232
|
-
@namespaced_elements = true
|
|
2233
|
-
nil
|
|
2234
|
-
end
|
|
2235
|
-
|
|
2236
|
-
def __internal_namespaced_elements__?
|
|
2237
|
-
@namespaced_elements == true
|
|
2238
|
-
end
|
|
2239
|
-
|
|
2240
2716
|
# Clear the cached wrapper so the next `wrap_node` creates a new
|
|
2241
2717
|
# one. Used by `customElements.define` to upgrade nodes that were
|
|
2242
2718
|
# constructed before the registration landed.
|
|
@@ -2266,6 +2742,69 @@ module Dommy
|
|
|
2266
2742
|
@shadow_registry.find_for_host(host_node)
|
|
2267
2743
|
end
|
|
2268
2744
|
|
|
2745
|
+
# DOM "allow declarative shadow roots": whether the document's own parser
|
|
2746
|
+
# (the page parse, document.write) attaches declarative shadow roots. On
|
|
2747
|
+
# for a browsing context's documents and parseHTMLUnsafe()'s; off for
|
|
2748
|
+
# createHTMLDocument(), DOMParser and `new Document()`; a clone keeps it.
|
|
2749
|
+
attr_writer :__internal_allow_declarative_shadow_roots__
|
|
2750
|
+
|
|
2751
|
+
def __internal_allow_declarative_shadow_roots__? = @__internal_allow_declarative_shadow_roots__ ? true : false
|
|
2752
|
+
|
|
2753
|
+
# The document's parser has built `root_bn`'s subtree: attach its
|
|
2754
|
+
# declarative shadow roots, when the document allows them. The shadow
|
|
2755
|
+
# roots are the document's parsed roots from then on (their scripts run
|
|
2756
|
+
# at boot, their elements get the parser's insertion steps). Returns them.
|
|
2757
|
+
def __internal_attach_declarative_shadow_roots__(root_bn, top_host: nil)
|
|
2758
|
+
return [] unless __internal_allow_declarative_shadow_roots__? && Backend.html_backed?(@backend_doc)
|
|
2759
|
+
|
|
2760
|
+
roots = Internal::DeclarativeShadowRoots.attach(self, root_bn, top_host: top_host)
|
|
2761
|
+
(@parser_shadow_roots ||= []).concat(roots)
|
|
2762
|
+
roots
|
|
2763
|
+
end
|
|
2764
|
+
|
|
2765
|
+
# The roots of what the document's parser built: the document, and the
|
|
2766
|
+
# declarative shadow roots it attached.
|
|
2767
|
+
def __internal_parsed_roots__
|
|
2768
|
+
return [@backend_doc] unless @parser_shadow_roots
|
|
2769
|
+
|
|
2770
|
+
[@backend_doc, *@parser_shadow_roots.map(&:__dommy_backend_node__)]
|
|
2771
|
+
end
|
|
2772
|
+
|
|
2773
|
+
# The document's parser-inserted scripts, in the order the parser met
|
|
2774
|
+
# them: tree order, where a declarative shadow tree's contents come at
|
|
2775
|
+
# the place its template was. With no declarative shadow roots, that is
|
|
2776
|
+
# `document.scripts`.
|
|
2777
|
+
def __internal_parser_scripts__
|
|
2778
|
+
return scripts.to_a unless @parser_shadow_roots
|
|
2779
|
+
|
|
2780
|
+
parsed_shadows = @parser_shadow_roots.to_h { |root| [Backend.identity_key(root.host.__dommy_backend_node__), root] }
|
|
2781
|
+
list = []
|
|
2782
|
+
collect_parser_scripts(@backend_doc, parsed_shadows, list)
|
|
2783
|
+
list
|
|
2784
|
+
end
|
|
2785
|
+
|
|
2786
|
+
# Whether any shadow root was ever attached in this document — the HTML
|
|
2787
|
+
# serializer's fast path asks before looking for shadow hosts.
|
|
2788
|
+
def __internal_any_shadow_roots__? = @shadow_registry.any?
|
|
2789
|
+
|
|
2790
|
+
# Whether an element of this document has an is value it does not carry
|
|
2791
|
+
# as an `is` attribute (createElement's `{is}`, a customized built-in's
|
|
2792
|
+
# constructor, a clone of either) — the serializer's other reason to
|
|
2793
|
+
# leave its fast path. The elements given an is value are held weakly.
|
|
2794
|
+
def __internal_any_is_values__?
|
|
2795
|
+
return false unless @is_value_elements
|
|
2796
|
+
|
|
2797
|
+
@is_value_elements.keys.any? do |element|
|
|
2798
|
+
node = element.__dommy_backend_node__
|
|
2799
|
+
current = @node_wrapper_cache.peek(node) || element
|
|
2800
|
+
current.__internal_is_value__ && !Backend.has_attribute_ns?(node, nil, "is")
|
|
2801
|
+
end
|
|
2802
|
+
end
|
|
2803
|
+
|
|
2804
|
+
def __internal_note_is_value__(element)
|
|
2805
|
+
(@is_value_elements ||= ObjectSpace::WeakMap.new)[element] = true
|
|
2806
|
+
end
|
|
2807
|
+
|
|
2269
2808
|
# Every element among `root`'s shadow-including inclusive descendants, as
|
|
2270
2809
|
# backend nodes, in shadow-including tree order: an element, then the shadow
|
|
2271
2810
|
# tree it hosts, then its children. The whole list, shadow trees included, is
|
|
@@ -2277,18 +2816,6 @@ module Dommy
|
|
|
2277
2816
|
nil
|
|
2278
2817
|
end
|
|
2279
2818
|
|
|
2280
|
-
def shadow_including_elements(root, list = [])
|
|
2281
|
-
elements = root.respond_to?(:element?) && root.element? ? [root] : []
|
|
2282
|
-
elements.concat(root.css("*").to_a) if root.respond_to?(:css)
|
|
2283
|
-
elements.each do |element|
|
|
2284
|
-
list << element
|
|
2285
|
-
shadow = @shadow_registry.find_for_host(element)
|
|
2286
|
-
shadow_including_elements(shadow.__dommy_backend_node__, list) if shadow
|
|
2287
|
-
end
|
|
2288
|
-
list
|
|
2289
|
-
end
|
|
2290
|
-
private :shadow_including_elements
|
|
2291
|
-
|
|
2292
2819
|
def __internal_shadow_root_containing__(node)
|
|
2293
2820
|
@shadow_registry.find_enclosing(node)
|
|
2294
2821
|
end
|
|
@@ -2326,8 +2853,67 @@ module Dommy
|
|
|
2326
2853
|
@mutation_coordinator.notify_disconnected_subtree(nk)
|
|
2327
2854
|
end
|
|
2328
2855
|
|
|
2329
|
-
|
|
2330
|
-
|
|
2856
|
+
# DOM "custom element registry" of the document: the one initialize()
|
|
2857
|
+
# set, else its window's (none without a browsing context).
|
|
2858
|
+
attr_writer :__internal_custom_element_registry__
|
|
2859
|
+
|
|
2860
|
+
def __internal_custom_element_registry__
|
|
2861
|
+
@__internal_custom_element_registry__ || (@default_view.custom_elements if @default_view.respond_to?(:custom_elements))
|
|
2862
|
+
end
|
|
2863
|
+
|
|
2864
|
+
# The element the HTML element constructor makes for `new MyElement()`.
|
|
2865
|
+
def __internal_create_custom_element__(definition)
|
|
2866
|
+
@node_factory.create_custom_element(definition)
|
|
2867
|
+
end
|
|
2868
|
+
|
|
2869
|
+
# HTML "try to upgrade an element": enqueue an upgrade reaction when the
|
|
2870
|
+
# registry `element` looks definitions up in has one for it.
|
|
2871
|
+
def __internal_try_to_upgrade__(element)
|
|
2872
|
+
registry = element.__internal_ce_registry__
|
|
2873
|
+
return unless registry&.any_definitions?
|
|
2874
|
+
|
|
2875
|
+
definition = CustomElementRegistry.lookup(registry, element.namespace_uri, element.local_name,
|
|
2876
|
+
element.__internal_is_value__)
|
|
2877
|
+
Internal::CEReactions.enqueue_upgrade(element, definition) if definition
|
|
2878
|
+
end
|
|
2879
|
+
|
|
2880
|
+
# The elements a parser or a clone just created in this document without
|
|
2881
|
+
# the synchronous custom elements flag (DOM "create an element" step 6.3),
|
|
2882
|
+
# with `registry` (the parser's intended parent's, the original's): each
|
|
2883
|
+
# one gets the registry when it is a scoped one, and each one a definition
|
|
2884
|
+
# applies to an upgrade reaction. `nodes` are backend nodes, each walked
|
|
2885
|
+
# in tree order (a template's contents are not its children, and belong
|
|
2886
|
+
# to a document with no registry).
|
|
2887
|
+
def __internal_enqueue_created_upgrades__(nodes, registry = CustomElementRegistry.for_document(self))
|
|
2888
|
+
explicit = !registry.equal?(CustomElementRegistry.effective_global_for(self))
|
|
2889
|
+
return unless explicit || registry&.any_definitions?
|
|
2890
|
+
|
|
2891
|
+
(nodes.is_a?(Array) ? nodes : [nodes]).each do |root|
|
|
2892
|
+
Internal::NodeTraversal.subtree_nodes(root).each do |node|
|
|
2893
|
+
next unless node.element?
|
|
2894
|
+
|
|
2895
|
+
known = __internal_peek_wrapper__(node)
|
|
2896
|
+
if explicit
|
|
2897
|
+
known ||= wrap_node(node)
|
|
2898
|
+
known.__internal_ce_data__.registry = registry
|
|
2899
|
+
end
|
|
2900
|
+
next if registry.nil?
|
|
2901
|
+
next unless Backend.namespace_uri(node) == Element::HTML_NAMESPACE
|
|
2902
|
+
|
|
2903
|
+
# The is value a wrapper was made with (a clone's), else the one
|
|
2904
|
+
# the parser gives it: its `is` attribute.
|
|
2905
|
+
is_value = known ? known.__internal_is_value__ : Backend.get_attribute_ns(node, nil, "is")
|
|
2906
|
+
definition = registry.lookup_definition(node.name, is_value)
|
|
2907
|
+
next unless definition
|
|
2908
|
+
|
|
2909
|
+
element = known || wrap_node(node)
|
|
2910
|
+
Internal::CEReactions.enqueue_upgrade(element, definition) if element
|
|
2911
|
+
end
|
|
2912
|
+
end
|
|
2913
|
+
end
|
|
2914
|
+
|
|
2915
|
+
def __internal_notify_attribute_changed__(element, name, old_value, new_value, namespace = nil)
|
|
2916
|
+
@mutation_coordinator.notify_attribute_changed(element, name, old_value, new_value, namespace)
|
|
2331
2917
|
end
|
|
2332
2918
|
|
|
2333
2919
|
def register_observer(observer)
|
|
@@ -2446,6 +3032,184 @@ module Dommy
|
|
|
2446
3032
|
end
|
|
2447
3033
|
end
|
|
2448
3034
|
|
|
3035
|
+
# The document write steps (without Trusted Types).
|
|
3036
|
+
#
|
|
3037
|
+
# Spec: https://html.spec.whatwg.org/#document-write-steps
|
|
3038
|
+
def document_write(args, line_feed:)
|
|
3039
|
+
string = args.map { |a| a.nil? ? "null" : a.to_s }.join
|
|
3040
|
+
string += "\n" if line_feed
|
|
3041
|
+
raise DOMException::InvalidStateError, "write() is not supported on an XML document" unless html_document?
|
|
3042
|
+
raise_if_markup_insertion_forbidden!("write")
|
|
3043
|
+
|
|
3044
|
+
return boot_script_write(string) if @script_created_input.nil? && boot_script_running?
|
|
3045
|
+
|
|
3046
|
+
document_open_steps if @script_created_input.nil?
|
|
3047
|
+
# The open steps return early (no script-created parser) only when the
|
|
3048
|
+
# parser is active, which the branch above already took.
|
|
3049
|
+
return nil if @script_created_input.nil?
|
|
3050
|
+
|
|
3051
|
+
@script_created_input << string
|
|
3052
|
+
reparse_script_created_input
|
|
3053
|
+
nil
|
|
3054
|
+
end
|
|
3055
|
+
|
|
3056
|
+
# Whether a page script is running while the page boots: the closest Dommy
|
|
3057
|
+
# has to "an active parser whose script nesting level is greater than 0".
|
|
3058
|
+
def boot_script_running?
|
|
3059
|
+
@ready_state == "loading" && !@__current_script__.nil?
|
|
3060
|
+
end
|
|
3061
|
+
|
|
3062
|
+
# The document open steps, minus what Dommy does not model (the origin
|
|
3063
|
+
# check against the entry document, unload counters, stopping a
|
|
3064
|
+
# navigation, and the URL and history update steps).
|
|
3065
|
+
#
|
|
3066
|
+
# Spec: https://html.spec.whatwg.org/#document-open-steps
|
|
3067
|
+
def document_open_steps
|
|
3068
|
+
raise DOMException::InvalidStateError, "open() is not supported on an XML document" unless html_document?
|
|
3069
|
+
raise_if_markup_insertion_forbidden!("open")
|
|
3070
|
+
return self if boot_script_running?
|
|
3071
|
+
# A second open() while a script-created parser is open keeps it (the
|
|
3072
|
+
# spec's steps re-run, but the written input so far is already gone with
|
|
3073
|
+
# the children they replace).
|
|
3074
|
+
erase_all_event_listeners_and_handlers
|
|
3075
|
+
document_replace_children([])
|
|
3076
|
+
@quirks_mode = false
|
|
3077
|
+
@script_created_input = +""
|
|
3078
|
+
unless @ready_state == "loading"
|
|
3079
|
+
@ready_state = "loading"
|
|
3080
|
+
__internal_fire_event__("readystatechange")
|
|
3081
|
+
end
|
|
3082
|
+
self
|
|
3083
|
+
end
|
|
3084
|
+
|
|
3085
|
+
# Insert what a boot-time script wrote at the insertion point: right after
|
|
3086
|
+
# the running script when it sits in the body, else at the end of the body
|
|
3087
|
+
# (or of the document element when there is no body).
|
|
3088
|
+
def boot_script_write(string)
|
|
3089
|
+
script_bn = @__current_script__&.__dommy_backend_node__
|
|
3090
|
+
parent_bn = script_bn&.parent
|
|
3091
|
+
in_body = parent_bn && body && parent_bn != @backend_doc &&
|
|
3092
|
+
Internal::NodeTraversal.subtree_nodes(body.__dommy_backend_node__).include?(parent_bn)
|
|
3093
|
+
target_bn = in_body ? parent_bn : (body || document_element)&.__dommy_backend_node__
|
|
3094
|
+
return nil unless target_bn
|
|
3095
|
+
|
|
3096
|
+
context = target_bn.element? ? target_bn : nil
|
|
3097
|
+
fragment = Parser.fragment(string, owner_doc: @backend_doc, context: context)
|
|
3098
|
+
# The document's parser reads the markup: a top-level declarative
|
|
3099
|
+
# template's adjusted current node is the element it is written into.
|
|
3100
|
+
__internal_attach_declarative_shadow_roots__(fragment, top_host: target_bn)
|
|
3101
|
+
added = fragment.children.to_a
|
|
3102
|
+
return nil if added.empty?
|
|
3103
|
+
|
|
3104
|
+
# The document's parser creates a defined element by running its
|
|
3105
|
+
# constructor; Dommy's parsed the markup already, so each one is
|
|
3106
|
+
# upgraded instead, before it is inserted.
|
|
3107
|
+
__internal_enqueue_created_upgrades__(added)
|
|
3108
|
+
|
|
3109
|
+
if in_body
|
|
3110
|
+
reference = script_bn
|
|
3111
|
+
added.each do |node|
|
|
3112
|
+
reference.add_next_sibling(node)
|
|
3113
|
+
reference = node
|
|
3114
|
+
end
|
|
3115
|
+
else
|
|
3116
|
+
added.each { |node| target_bn.add_child(node) }
|
|
3117
|
+
end
|
|
3118
|
+
@__document_writing = true
|
|
3119
|
+
begin
|
|
3120
|
+
notify_child_list_mutation(target_node: target_bn, added_nodes: added, removed_nodes: [])
|
|
3121
|
+
ensure
|
|
3122
|
+
@__document_writing = false
|
|
3123
|
+
end
|
|
3124
|
+
nil
|
|
3125
|
+
end
|
|
3126
|
+
|
|
3127
|
+
# Whether the insertion under way comes from document.write: an external
|
|
3128
|
+
# script it writes is the parser's pending parsing-blocking script, which
|
|
3129
|
+
# runs as soon as the writing script returns — before the parser goes on.
|
|
3130
|
+
def __internal_document_writing__ = @__document_writing == true
|
|
3131
|
+
|
|
3132
|
+
# Re-parse everything written since open() as a whole document and make
|
|
3133
|
+
# its children the document's (see "Dynamic markup insertion" above).
|
|
3134
|
+
# HTML's throw-on-dynamic-markup-insertion counter: while the parser runs
|
|
3135
|
+
# custom element constructors and reactions (see #parser_runs_scripts),
|
|
3136
|
+
# open(), write() and close() are an InvalidStateError.
|
|
3137
|
+
def raise_if_markup_insertion_forbidden!(operation)
|
|
3138
|
+
return unless @throw_on_dynamic_markup_insertion_counter.to_i.positive?
|
|
3139
|
+
|
|
3140
|
+
raise DOMException::InvalidStateError, "#{operation}() is not allowed while the parser constructs a custom element"
|
|
3141
|
+
end
|
|
3142
|
+
|
|
3143
|
+
# The parser's custom element work for markup it inserts: the
|
|
3144
|
+
# constructors ("create an element for a token" with willExecuteScript)
|
|
3145
|
+
# and the reactions they enqueue run before it continues, with the
|
|
3146
|
+
# throw-on-dynamic-markup-insertion counter raised.
|
|
3147
|
+
def parser_runs_scripts
|
|
3148
|
+
@throw_on_dynamic_markup_insertion_counter = @throw_on_dynamic_markup_insertion_counter.to_i + 1
|
|
3149
|
+
Internal::CEReactions.scope { yield }
|
|
3150
|
+
ensure
|
|
3151
|
+
@throw_on_dynamic_markup_insertion_counter -= 1
|
|
3152
|
+
end
|
|
3153
|
+
|
|
3154
|
+
def reparse_script_created_input
|
|
3155
|
+
parser_runs_scripts { reparse_script_created_input_now }
|
|
3156
|
+
end
|
|
3157
|
+
|
|
3158
|
+
def reparse_script_created_input_now
|
|
3159
|
+
parsed = Document.new(nil, backend_doc: Backend.parse(@script_created_input))
|
|
3160
|
+
@quirks_mode = parsed.quirks_mode?
|
|
3161
|
+
parsed.__internal_mark_scripts_already_started__
|
|
3162
|
+
# What the parser builds, not what a script inserts: a doctype and an
|
|
3163
|
+
# element go in together, which replaceChildren's checks would refuse.
|
|
3164
|
+
removed = @backend_doc.children.to_a
|
|
3165
|
+
removed.each { |child| detach_node(child) }
|
|
3166
|
+
added = document_insertion_nodes(parsed.child_nodes.to_a)
|
|
3167
|
+
added.each { |n| @backend_doc.add_child(n) }
|
|
3168
|
+
# The written markup is this document's parser's: its declarative
|
|
3169
|
+
# shadow roots, when the document allows them, replace the last parse's.
|
|
3170
|
+
@parser_shadow_roots = nil
|
|
3171
|
+
__internal_attach_declarative_shadow_roots__(@backend_doc).each do |root|
|
|
3172
|
+
__internal_mark_scripts_already_started__(root.__dommy_backend_node__)
|
|
3173
|
+
end
|
|
3174
|
+
notify_document_child_list(added: added, removed: removed)
|
|
3175
|
+
end
|
|
3176
|
+
|
|
3177
|
+
# "Erase all event listeners and handlers" for the document's
|
|
3178
|
+
# shadow-including inclusive descendants and, for a window's document, the
|
|
3179
|
+
# window. Only nodes that have a wrapper can hold listeners.
|
|
3180
|
+
def erase_all_event_listeners_and_handlers
|
|
3181
|
+
targets = [self]
|
|
3182
|
+
collect_listener_targets(@backend_doc, targets)
|
|
3183
|
+
targets << @default_view if @default_view
|
|
3184
|
+
targets.each { |t| t.__internal_erase_event_listeners_and_handlers__ if t.respond_to?(:__internal_erase_event_listeners_and_handlers__) }
|
|
3185
|
+
end
|
|
3186
|
+
|
|
3187
|
+
def collect_listener_targets(root_bn, targets)
|
|
3188
|
+
Internal::NodeTraversal.subtree_nodes(root_bn).each do |bn|
|
|
3189
|
+
wrapper = @node_wrapper_cache.peek(bn)
|
|
3190
|
+
next unless wrapper
|
|
3191
|
+
|
|
3192
|
+
targets << wrapper unless wrapper.equal?(self)
|
|
3193
|
+
shadow = wrapper.respond_to?(:__internal_shadow_root__) ? wrapper.__internal_shadow_root__ : nil
|
|
3194
|
+
next unless shadow
|
|
3195
|
+
|
|
3196
|
+
targets << shadow
|
|
3197
|
+
collect_listener_targets(shadow.__dommy_backend_node__, targets) if shadow.__dommy_backend_node__
|
|
3198
|
+
end
|
|
3199
|
+
end
|
|
3200
|
+
|
|
3201
|
+
# document.open(url, name, features) is window.open; it needs a fully
|
|
3202
|
+
# active document (one with a window, here).
|
|
3203
|
+
def open_window(args)
|
|
3204
|
+
raise DOMException::InvalidAccessError, "the document is not fully active" unless @default_view
|
|
3205
|
+
|
|
3206
|
+
@default_view.__js_call__("open", args)
|
|
3207
|
+
end
|
|
3208
|
+
|
|
3209
|
+
private :document_write, :boot_script_running?, :document_open_steps, :boot_script_write,
|
|
3210
|
+
:reparse_script_created_input, :erase_all_event_listeners_and_handlers,
|
|
3211
|
+
:collect_listener_targets, :open_window
|
|
3212
|
+
|
|
2449
3213
|
# Unlink a backend node from its parent and queue a childList removal record
|
|
2450
3214
|
# capturing the node's position (previous/next sibling) BEFORE the unlink, so
|
|
2451
3215
|
# the record's previousSibling/nextSibling are correct (the coordinator can't
|
|
@@ -2524,7 +3288,7 @@ module Dommy
|
|
|
2524
3288
|
# https://github.com/takahashim/dommy/issues/24
|
|
2525
3289
|
def __internal_normalize__(root)
|
|
2526
3290
|
text_nodes = []
|
|
2527
|
-
root.traverse { |node| text_nodes << node if node.
|
|
3291
|
+
root.traverse { |node| text_nodes << node if node.text? }
|
|
2528
3292
|
|
|
2529
3293
|
text_nodes.each do |node|
|
|
2530
3294
|
next unless node.parent # already removed as part of an earlier run
|
|
@@ -2535,7 +3299,7 @@ module Dommy
|
|
|
2535
3299
|
end
|
|
2536
3300
|
|
|
2537
3301
|
sib = node.next
|
|
2538
|
-
while sib
|
|
3302
|
+
while sib&.text?
|
|
2539
3303
|
following = sib.next
|
|
2540
3304
|
data = sib.content.to_s
|
|
2541
3305
|
# The offset the sibling's data lands at inside the survivor — the
|
|
@@ -2600,8 +3364,8 @@ module Dommy
|
|
|
2600
3364
|
|
|
2601
3365
|
# Delegate factory methods to NodeWrapperCache
|
|
2602
3366
|
|
|
2603
|
-
def create_element(name)
|
|
2604
|
-
@node_factory.create_element(name)
|
|
3367
|
+
def create_element(name, options = nil)
|
|
3368
|
+
@node_factory.create_element(name, options)
|
|
2605
3369
|
end
|
|
2606
3370
|
|
|
2607
3371
|
def create_text_node(text)
|
|
@@ -2659,6 +3423,123 @@ module Dommy
|
|
|
2659
3423
|
|
|
2660
3424
|
private
|
|
2661
3425
|
|
|
3426
|
+
# HTML "is a registrable domain suffix of or is equal to" (a single label
|
|
3427
|
+
# stands in for the public suffix; see #domain=).
|
|
3428
|
+
def registrable_domain_suffix_or_equal?(suffix_string, original_host)
|
|
3429
|
+
return false if suffix_string.empty?
|
|
3430
|
+
|
|
3431
|
+
suffix = Internal::UrlParser.parse("http://#{suffix_string}/").host.to_s
|
|
3432
|
+
return true if suffix == original_host
|
|
3433
|
+
return false if ip_host?(suffix) || ip_host?(original_host)
|
|
3434
|
+
return false unless original_host.end_with?(".#{suffix}")
|
|
3435
|
+
# The suffix may not itself be a public suffix.
|
|
3436
|
+
return false unless suffix.include?(".")
|
|
3437
|
+
|
|
3438
|
+
true
|
|
3439
|
+
rescue Internal::UrlParser::Failure
|
|
3440
|
+
false
|
|
3441
|
+
end
|
|
3442
|
+
|
|
3443
|
+
def ip_host?(host)
|
|
3444
|
+
host.start_with?("[") || host.match?(/\A\d+\.\d+\.\d+\.\d+\z/)
|
|
3445
|
+
end
|
|
3446
|
+
|
|
3447
|
+
|
|
3448
|
+
def creator_base_url
|
|
3449
|
+
@creator_base_url if @creator_base_url && FALLBACK_BASE_URLS.include?(url)
|
|
3450
|
+
end
|
|
3451
|
+
|
|
3452
|
+
# importNode is a clone, so the HTML cloning steps run for it as they do
|
|
3453
|
+
# for cloneNode (#__internal_apply_cloning_steps__): the live state Dommy
|
|
3454
|
+
# keeps on a wrapper — an input's dirty value, a script's "already
|
|
3455
|
+
# started" — is copied onto the new node. The originals' wrappers belong
|
|
3456
|
+
# to the source document, so they are looked up there.
|
|
3457
|
+
def apply_imported_cloning_steps(src_root, copy_root, deep, source_document)
|
|
3458
|
+
return unless source_document.respond_to?(:__internal_peek_wrapper__)
|
|
3459
|
+
|
|
3460
|
+
src_nodes = deep ? Internal::NodeTraversal.subtree_nodes(src_root) : [src_root]
|
|
3461
|
+
copy_nodes = deep ? Internal::NodeTraversal.subtree_nodes(copy_root) : [copy_root]
|
|
3462
|
+
return unless src_nodes.length == copy_nodes.length
|
|
3463
|
+
|
|
3464
|
+
src_nodes.zip(copy_nodes).each do |orig, copy|
|
|
3465
|
+
state = source_document.__internal_peek_wrapper__(orig)&.then { |w| w.respond_to?(:__internal_cloning_state__) && w.__internal_cloning_state__ }
|
|
3466
|
+
next unless state
|
|
3467
|
+
|
|
3468
|
+
wrapper = wrap_node(copy)
|
|
3469
|
+
wrapper.__internal_apply_cloning_state__(state) if wrapper.respond_to?(:__internal_apply_cloning_state__)
|
|
3470
|
+
end
|
|
3471
|
+
end
|
|
3472
|
+
|
|
3473
|
+
def node_adopter
|
|
3474
|
+
@node_adopter ||= Internal::NodeAdopter.new(self)
|
|
3475
|
+
end
|
|
3476
|
+
|
|
3477
|
+
def collect_parser_scripts(node, parsed_shadows, list)
|
|
3478
|
+
shadow = node.element? ? parsed_shadows[Backend.identity_key(node)] : nil
|
|
3479
|
+
shadow = nil unless shadow&.host&.__dommy_backend_node__ == node
|
|
3480
|
+
pending = shadow
|
|
3481
|
+
node.children.each do |child|
|
|
3482
|
+
if pending && (before = pending.__internal_parsed_before__) && Backend.identity_key(child) == Backend.identity_key(before)
|
|
3483
|
+
collect_parser_scripts(pending.__dommy_backend_node__, parsed_shadows, list)
|
|
3484
|
+
pending = nil
|
|
3485
|
+
end
|
|
3486
|
+
next unless child.element?
|
|
3487
|
+
|
|
3488
|
+
if child.local_name == "script" && (script = __internal_html_element_wrapper__(child))
|
|
3489
|
+
list << script
|
|
3490
|
+
end
|
|
3491
|
+
collect_parser_scripts(child, parsed_shadows, list)
|
|
3492
|
+
end
|
|
3493
|
+
collect_parser_scripts(pending.__dommy_backend_node__, parsed_shadows, list) if pending
|
|
3494
|
+
end
|
|
3495
|
+
|
|
3496
|
+
# A `css` query over every root the parser built.
|
|
3497
|
+
def parsed_css(selector)
|
|
3498
|
+
roots = __internal_parsed_roots__
|
|
3499
|
+
return @backend_doc.css(selector) if roots.size == 1
|
|
3500
|
+
|
|
3501
|
+
roots.flat_map { |root| root.css(selector).to_a }
|
|
3502
|
+
end
|
|
3503
|
+
|
|
3504
|
+
def shadow_including_elements(root, list = [])
|
|
3505
|
+
elements = root.element? ? [root] : []
|
|
3506
|
+
elements.concat(root.css("*").to_a)
|
|
3507
|
+
elements.each do |element|
|
|
3508
|
+
list << element
|
|
3509
|
+
shadow = @shadow_registry.find_for_host(element)
|
|
3510
|
+
shadow_including_elements(shadow.__dommy_backend_node__, list) if shadow
|
|
3511
|
+
end
|
|
3512
|
+
list
|
|
3513
|
+
end
|
|
3514
|
+
|
|
3515
|
+
# "The html element": the document element when it is an HTML <html>.
|
|
3516
|
+
def html_element
|
|
3517
|
+
root = document_element
|
|
3518
|
+
root if root.is_a?(HTMLElement) && root.local_name == "html"
|
|
3519
|
+
end
|
|
3520
|
+
|
|
3521
|
+
# An element is a named element with the name `name` when it is one of the
|
|
3522
|
+
# exposed kinds and either carries that `name`, or is an object with that
|
|
3523
|
+
# `id`, or is an img whose id it is and which also has a non-empty name.
|
|
3524
|
+
def named_element?(node, name)
|
|
3525
|
+
return false if name.empty?
|
|
3526
|
+
return false unless %w[embed form iframe img object].include?(node.name.to_s.downcase)
|
|
3527
|
+
own_name = Backend.no_namespace_attribute_value(node, "name")
|
|
3528
|
+
return true if own_name == name
|
|
3529
|
+
return true if node.name.to_s.casecmp?("object") && Backend.no_namespace_attribute_value(node, "id") == name
|
|
3530
|
+
|
|
3531
|
+
node.name.to_s.casecmp?("img") && Backend.no_namespace_attribute_value(node, "id") == name && !own_name.to_s.empty?
|
|
3532
|
+
end
|
|
3533
|
+
|
|
3534
|
+
# Elements the document's named getter exposes, in tree order.
|
|
3535
|
+
def named_getter_nodes
|
|
3536
|
+
@backend_doc.css("embed, form, iframe, img, object")
|
|
3537
|
+
end
|
|
3538
|
+
|
|
3539
|
+
def document_named_property_nodes(name)
|
|
3540
|
+
named_getter_nodes.select { |node| named_element?(node, name) }.map { |node| wrap_node(node) }.compact
|
|
3541
|
+
end
|
|
3542
|
+
|
|
2662
3543
|
# Build a Nokogiri copy of the given node inside our @backend_doc.
|
|
2663
3544
|
# `deep: true` recurses into children. Used by importNode and
|
|
2664
3545
|
# adoptNode for cross-document transfer.
|
|
@@ -2670,11 +3551,9 @@ module Dommy
|
|
|
2670
3551
|
# A <template>'s contents live in a separate content fragment, not its
|
|
2671
3552
|
# child list, so the pass over `children` misses them. It still runs: an
|
|
2672
3553
|
# XML document's <template> keeps its children in the child list.
|
|
2673
|
-
clone_template_content(source, copy, source_document) if
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
copy.add_child(clone_into_doc(child, true, source_document))
|
|
2677
|
-
end
|
|
3554
|
+
clone_template_content(source, copy, source_document) if @template_content_registry.template_node?(source)
|
|
3555
|
+
source.children.each do |child|
|
|
3556
|
+
copy.add_child(clone_into_doc(child, true, source_document))
|
|
2678
3557
|
end
|
|
2679
3558
|
|
|
2680
3559
|
copy
|
|
@@ -2682,12 +3561,13 @@ module Dommy
|
|
|
2682
3561
|
|
|
2683
3562
|
# "Clone a single node" (§4.4): the copy implements the SAME interface as
|
|
2684
3563
|
# the original — a ProcessingInstruction clones to one, not to the comment
|
|
2685
|
-
# its serialization looks like — and carries the same data.
|
|
2686
|
-
#
|
|
3564
|
+
# its serialization looks like — and carries the same data. An element's
|
|
3565
|
+
# copy keeps its name, namespace, prefix and attributes as the backend
|
|
3566
|
+
# holds them; children are the caller's business.
|
|
2687
3567
|
def clone_single_node_into_doc(source, source_document)
|
|
2688
3568
|
if source.element?
|
|
2689
|
-
|
|
2690
|
-
elsif
|
|
3569
|
+
Backend.import_element(source, @backend_doc)
|
|
3570
|
+
elsif source.is_a?(Backend.cdata_class)
|
|
2691
3571
|
# CDATA is a Text subtype in the backend, so ask about it first.
|
|
2692
3572
|
Backend.create_cdata(source.content, @backend_doc)
|
|
2693
3573
|
elsif source.text?
|
|
@@ -2705,7 +3585,7 @@ module Dommy
|
|
|
2705
3585
|
# `document_fragment_class.new`, so it works on backends whose fragment
|
|
2706
3586
|
# class isn't directly instantiable (Makiri).
|
|
2707
3587
|
Parser.fragment("", owner_doc: @backend_doc)
|
|
2708
|
-
elsif
|
|
3588
|
+
elsif source.is_a?(Backend.document_type_class)
|
|
2709
3589
|
wrapper = source_document.wrap_node(source)
|
|
2710
3590
|
Backend.create_document_type(wrapper.name, wrapper.public_id, wrapper.system_id, @backend_doc)
|
|
2711
3591
|
else
|
|
@@ -2718,36 +3598,6 @@ module Dommy
|
|
|
2718
3598
|
# element stays `A:B`, where the DOM's setAttribute would lower-case it).
|
|
2719
3599
|
# Only a createElementNS name the backend node does not carry lives on the
|
|
2720
3600
|
# original's wrapper, so the copy's wrapper is given the same metadata.
|
|
2721
|
-
def clone_element_into_doc(source, source_document)
|
|
2722
|
-
wrapper = source_document.wrap_node(source)
|
|
2723
|
-
namespace, prefix, local, qualified = clone_name_parts(wrapper, source)
|
|
2724
|
-
copy = Backend.import_element(source, @backend_doc)
|
|
2725
|
-
note_cloned_element_namespace(copy, namespace, prefix, local, qualified)
|
|
2726
|
-
copy
|
|
2727
|
-
end
|
|
2728
|
-
|
|
2729
|
-
# The four parts of the original's name — namespace, prefix, local name and
|
|
2730
|
-
# qualified name. The wrapper's own metadata has them whenever there is
|
|
2731
|
-
# anything to carry (createElementNS's case and prefix, an XML document's,
|
|
2732
|
-
# the HTML parser's SVG names); otherwise the element is an unprefixed one
|
|
2733
|
-
# whose namespace the backend knows.
|
|
2734
|
-
def clone_name_parts(wrapper, source)
|
|
2735
|
-
meta = wrapper.__internal_namespace_metadata__ if wrapper.respond_to?(:__internal_namespace_metadata__)
|
|
2736
|
-
return meta if meta
|
|
2737
|
-
|
|
2738
|
-
local = wrapper ? wrapper.local_name.to_s : source.name
|
|
2739
|
-
[wrapper&.namespace_uri, nil, local, local]
|
|
2740
|
-
end
|
|
2741
|
-
|
|
2742
|
-
# Give the copy's wrapper the namespace metadata whenever the backend node
|
|
2743
|
-
# alone would report something else (see #clone_element_into_doc).
|
|
2744
|
-
def note_cloned_element_namespace(copy, namespace, prefix, local, qualified)
|
|
2745
|
-
derived = Backend.namespace_uri(copy)
|
|
2746
|
-
return if derived == namespace && prefix.nil? && local == copy.name
|
|
2747
|
-
|
|
2748
|
-
wrap_cloned_element_ns(copy, namespace, prefix, local, qualified)
|
|
2749
|
-
end
|
|
2750
|
-
|
|
2751
3601
|
# HTML's cloning steps for a <template>: a deep copy of each of the
|
|
2752
3602
|
# source's contents, appended to the copy's contents.
|
|
2753
3603
|
def clone_template_content(source, copy, source_document = self)
|
|
@@ -2758,29 +3608,91 @@ module Dommy
|
|
|
2758
3608
|
content_nodes.each { |n| frag.add_child(clone_into_doc(n, true, source_document)) }
|
|
2759
3609
|
end
|
|
2760
3610
|
|
|
3611
|
+
# The document's element child, as a backend node, or nil. The backend's
|
|
3612
|
+
# `root` is that element in the ordinary case, but once the element it
|
|
3613
|
+
# held is gone it answers the doctype, even when another element has been
|
|
3614
|
+
# appended since — so look past it.
|
|
3615
|
+
def backend_document_element
|
|
3616
|
+
root = @backend_doc.root
|
|
3617
|
+
root&.element? ? root : @backend_doc.children.find(&:element?)
|
|
3618
|
+
end
|
|
3619
|
+
|
|
3620
|
+
# The first child of the html element — the document element, when it is
|
|
3621
|
+
# `html` in the HTML namespace — that is an HTML element named one of
|
|
3622
|
+
# `names`, as a backend node; HTML defines both the head and the body
|
|
3623
|
+
# element this way. Only the match is wrapped.
|
|
3624
|
+
def html_element_child(names)
|
|
3625
|
+
root = backend_document_element
|
|
3626
|
+
return nil unless root && root.local_name == "html" && Backend.namespace_uri(root) == Internal::Namespaces::HTML
|
|
3627
|
+
|
|
3628
|
+
root.element_children.find { |c| names.include?(c.local_name) && Backend.namespace_uri(c) == Internal::Namespaces::HTML }
|
|
3629
|
+
end
|
|
3630
|
+
|
|
3631
|
+
def body_or_frameset?(node)
|
|
3632
|
+
node.is_a?(Element) && %w[body frameset].include?(node.local_name) &&
|
|
3633
|
+
node.namespace_uri == Internal::Namespaces::HTML
|
|
3634
|
+
end
|
|
3635
|
+
|
|
3636
|
+
# document.title's getter (HTML §3.1.3): the child text content of the
|
|
3637
|
+
# SVG `title` child of an SVG `svg` document element, else of the title
|
|
3638
|
+
# element, stripped and collapsed of ASCII whitespace. ASCII whitespace is
|
|
3639
|
+
# exactly tab/LF/FF/CR/space — NOT Ruby's String#strip set, which also
|
|
3640
|
+
# removes U+000B (vertical tab) and must be left intact.
|
|
2761
3641
|
def read_title
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
# ASCII whitespace is exactly tab/LF/FF/CR/space — NOT Ruby's String#strip
|
|
2765
|
-
# set, which also removes U+000B (vertical tab) and must be left intact.
|
|
2766
|
-
title = @backend_doc.at_css("title")
|
|
3642
|
+
root = backend_document_element
|
|
3643
|
+
title = svg_root?(root) ? svg_title_child(root) : html_title_element
|
|
2767
3644
|
return "" unless title
|
|
2768
3645
|
|
|
2769
|
-
title.
|
|
3646
|
+
Backend.child_text_content(title).gsub(/[\t\n\f\r ]+/, " ").gsub(/\A[\t\n\f\r ]+|[\t\n\f\r ]+\z/, "")
|
|
2770
3647
|
end
|
|
2771
3648
|
|
|
3649
|
+
# document.title's setter: string-replace-all in the SVG title (made the
|
|
3650
|
+
# document element's first child when missing), or in the title element
|
|
3651
|
+
# (appended to the head element when missing, unless there is no head
|
|
3652
|
+
# either). A document element in any other namespace takes no title.
|
|
2772
3653
|
def write_title(value)
|
|
2773
|
-
|
|
2774
|
-
|
|
3654
|
+
root = backend_document_element
|
|
3655
|
+
if svg_root?(root)
|
|
3656
|
+
title = svg_title_child(root)
|
|
3657
|
+
title = insert_title(root, Internal::Namespaces::SVG, before: root.first_child) unless title
|
|
3658
|
+
elsif root && Backend.namespace_uri(root) == Internal::Namespaces::HTML
|
|
3659
|
+
title = html_title_element
|
|
3660
|
+
unless title
|
|
3661
|
+
head = html_element_child(%w[head])
|
|
3662
|
+
return unless head
|
|
3663
|
+
|
|
3664
|
+
title = insert_title(head, Internal::Namespaces::HTML)
|
|
3665
|
+
end
|
|
3666
|
+
else
|
|
3667
|
+
return
|
|
3668
|
+
end
|
|
3669
|
+
wrap_node(title).text_content = value
|
|
3670
|
+
end
|
|
2775
3671
|
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
3672
|
+
# The title element: the first HTML `title` in the document, in tree order.
|
|
3673
|
+
# Matched by namespace and local name: an XML document's `css("title")`
|
|
3674
|
+
# does not find an HTML-namespace title, so there the tree is walked.
|
|
3675
|
+
def html_title_element
|
|
3676
|
+
candidates = html_document? ? @backend_doc.css("title") : Internal::NodeTraversal.subtree_nodes(@backend_doc)
|
|
3677
|
+
candidates.find do |node|
|
|
3678
|
+
node.element? && node.local_name == "title" && Backend.namespace_uri(node) == Internal::Namespaces::HTML
|
|
2780
3679
|
end
|
|
3680
|
+
end
|
|
3681
|
+
|
|
3682
|
+
def svg_root?(root)
|
|
3683
|
+
root && root.local_name == "svg" && Backend.namespace_uri(root) == Internal::Namespaces::SVG
|
|
3684
|
+
end
|
|
3685
|
+
|
|
3686
|
+
def svg_title_child(root)
|
|
3687
|
+
root.element_children.find { |c| c.local_name == "title" && Backend.namespace_uri(c) == Internal::Namespaces::SVG }
|
|
3688
|
+
end
|
|
2781
3689
|
|
|
2782
|
-
|
|
2783
|
-
|
|
3690
|
+
# A new `title` in `namespace`, inserted into `parent` before `before`
|
|
3691
|
+
# (appended when nil), as a backend node.
|
|
3692
|
+
def insert_title(parent, namespace, before: nil)
|
|
3693
|
+
title = create_element_ns(namespace, "title")
|
|
3694
|
+
wrap_node(parent).insert_before(title, before && wrap_node(before))
|
|
3695
|
+
title.__dommy_backend_node__
|
|
2784
3696
|
end
|
|
2785
3697
|
|
|
2786
3698
|
end
|