/
githubmirror
/
servo
Обзор
Документация
Войти
/
githubmirror
/
servo
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
components/script/dom/document/document.rs
6 984 строки
279 KB
Martin Robinson
script: Properly clear selection flags on siblings (#47070)
08 авг 2026, 11:44
Не верифицирован
08 авг 2026, 11:44
2dabc41
Код
Авторство
О чём код?
/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use std::cell::{Cell, RefCell}; use std::cmp::Ordering; use std::collections::hash_map::Entry::{Occupied, Vacant}; use std::collections::{HashMap, HashSet, VecDeque}; use std::default::Default; use std::ops::Deref; use std::rc::Rc; use std::str::FromStr; use std::sync::{Arc as StdArc, LazyLock, Mutex}; use std::time::Duration; use bitflags::bitflags; use chrono::Local; use content_security_policy::sandboxing_directive::SandboxingFlagSet; use content_security_policy::{CspList, Policy as CspPolicy, PolicyDisposition}; use cookie::Cookie; use data_url::mime::Mime; use devtools_traits::ScriptToDevtoolsControlMsg; use dom_struct::dom_struct; use embedder_traits::{ AllowOrDeny, AnimationState, CustomHandlersAutomationMode, EmbedderMsg, Image, LoadStatus, }; use encoding_rs::{Encoding, UTF_8}; use html5ever::{LocalName, QualName, local_name, ns}; use hyper_serde::Serde; use indexmap::IndexSet; use js::context::{JSContext, NoGC}; use js::realm::CurrentRealm; use js::rust::{HandleObject, HandleValue, MutableHandleValue}; use layout_api::{ PendingRestyle, ReflowGoal, ReflowPhasesRun, ReflowStatistics, RestyleReason, ScrollContainerQueryFlags, TrustedNodeAddress, }; use metrics::{InteractiveFlag, InteractiveWindow, ProgressiveWebMetrics}; use net_traits::CookieSource::NonHTTP; use net_traits::CoreResourceMsg::{GetCookieStringForUrl, SetCookiesForUrl}; use net_traits::image_cache::ImageCache; use net_traits::policy_container::PolicyContainer; use net_traits::pub_domains::is_pub_domain; use net_traits::request::{ InsecureRequestsPolicy, PreloadId, PreloadKey, PreloadedResources, RequestBuilder, }; use net_traits::{ReferrerPolicy, ResourceFetchTiming}; use paint_api::largest_contentful_paint_candidate::LCPCandidateID; use percent_encoding::percent_decode; use profile_traits::generic_channel as profile_generic_channel; use profile_traits::time::TimerMetadataFrameType; use regex::bytes::Regex; use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet}; use script_bindings::cell::{DomRefCell, Ref, RefMut}; use script_bindings::interfaces::DocumentHelpers; use script_bindings::reflector::reflect_dom_object_with_proto; use script_bindings::trace::CustomTraceable; use script_traits::{DocumentActivity, ProgressiveWebMetricType}; use servo_arc::Arc; use servo_base::cross_process_instant::CrossProcessInstant; use servo_base::generic_channel::GenericSend; use servo_base::id::{PipelineId, WebViewId}; use servo_base::{Epoch, generic_channel}; use servo_config::pref; use servo_constellation_traits::{NavigationHistoryBehavior, ScriptToConstellationMessage}; use servo_media::{ClientContextId, ServoMedia}; use servo_url::{ImmutableOrigin, MutableOrigin, ServoUrl}; use style::attr::AttrValue; use style::context::QuirksMode; use style::dom::OpaqueNode; use style::invalidation::element::restyle_hints::RestyleHint; use style::selector_parser::Snapshot; use style::shared_lock::{SharedRwLock, SharedRwLockReadGuard}; use style::str::{split_html_space_chars, str_join}; use style::stylesheet_set::DocumentStylesheetSet; use style::stylesheets::{Origin, OriginSet, Stylesheet}; use style::stylist::Stylist; use stylo_atoms::Atom; use time::Duration as TimeDuration; use url::{Host, Position}; use crate::animations::Animations; use crate::document_loader::{DocumentLoader, LoadType}; use crate::dom::FlatTreeParent; use crate::dom::animationtimeline::AnimationTimeline; use crate::dom::attr::Attr; use crate::dom::beforeunloadevent::BeforeUnloadEvent; use crate::dom::bindings::callback::ExceptionHandling; use crate::dom::bindings::codegen::Bindings::AnimationFrameProviderBinding::FrameRequestCallback; use crate::dom::bindings::codegen::Bindings::BeforeUnloadEventBinding::BeforeUnloadEvent_Binding::BeforeUnloadEventMethods; use crate::dom::bindings::codegen::Bindings::DocumentBinding::{ DocumentMethods, DocumentReadyState, DocumentVisibilityState, NamedPropertyValue, }; use crate::dom::bindings::codegen::Bindings::ElementBinding::ScrollLogicalPosition; use crate::dom::bindings::codegen::Bindings::EventBinding::Event_Binding::EventMethods; use crate::dom::bindings::codegen::Bindings::HTMLIFrameElementBinding::HTMLIFrameElement_Binding::HTMLIFrameElementMethods; #[cfg(any(feature = "webxr", feature = "gamepad"))] use crate::dom::bindings::codegen::Bindings::NavigatorBinding::Navigator_Binding::NavigatorMethods; use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods; use crate::dom::bindings::codegen::Bindings::NodeFilterBinding::NodeFilter; use crate::dom::bindings::codegen::Bindings::PerformanceBinding::PerformanceMethods; use crate::dom::bindings::codegen::Bindings::PermissionStatusBinding::PermissionName; use crate::dom::bindings::codegen::Bindings::SanitizerBinding::{ SetHTMLOptions, SetHTMLUnsafeOptions, }; use crate::dom::bindings::codegen::Bindings::WindowBinding::{ScrollBehavior, WindowMethods}; use crate::dom::bindings::codegen::Bindings::XPathEvaluatorBinding::XPathEvaluatorMethods; use crate::dom::bindings::codegen::Bindings::XPathNSResolverBinding::XPathNSResolver; use crate::dom::bindings::codegen::UnionTypes::{ BooleanOrImportNodeOptions, NodeOrString, StringOrElementCreationOptions, TrustedHTMLOrString, }; use crate::dom::bindings::domname::{ self, is_valid_attribute_local_name, is_valid_element_local_name, namespace_from_domstring, }; use crate::dom::bindings::error::{Error, ErrorInfo, ErrorResult, Fallible}; use crate::dom::bindings::frozenarray::CachedFrozenArray; use crate::dom::bindings::inheritance::{Castable, ElementTypeId, HTMLElementTypeId, NodeTypeId}; use crate::dom::bindings::num::Finite; use crate::dom::bindings::refcounted::Trusted; use crate::dom::bindings::reflector::DomGlobal; use crate::dom::bindings::root::{ Dom, DomRoot, LayoutDom, MutNullableDom, ToLayout, ToLayoutOptional, UnrootedDom, }; use crate::dom::bindings::str::{DOMString, USVString}; use crate::dom::bindings::trace::{HashMapTracedValues, NoTrace}; use crate::dom::bindings::weakref::DOMTracker; use crate::dom::bindings::xmlname::matches_name_production; use crate::dom::cdatasection::CDATASection; use crate::dom::comment::Comment; use crate::dom::compositionevent::CompositionEvent; use crate::dom::css::cssstylesheet::CSSStyleSheet; use crate::dom::css::fontfaceset::FontFaceSet; use crate::dom::css::stylesheetlist::{StyleSheetList, StyleSheetListOwner}; use crate::dom::customelementregistry::{CustomElementReactionStack, CustomElementRegistry}; use crate::dom::customevent::CustomEvent; use crate::dom::document::accessibility_data::AccessibilityData; use crate::dom::document::focus::{DocumentFocusHandler, FocusableArea}; use crate::dom::document::tree_ordered_index_map::TreeOrderedIndexMap; use crate::dom::document::websocket::WebSocket; use crate::dom::document_embedder_controls::DocumentEmbedderControls; use crate::dom::document_event_handler::DocumentEventHandler; use crate::dom::documentfragment::DocumentFragment; use crate::dom::documentorshadowroot::{ DocumentOrShadowRoot, ServoStylesheetInDocument, StylesheetSource, }; use crate::dom::documenttimeline::DocumentTimeline; use crate::dom::documenttype::DocumentType; use crate::dom::domimplementation::DOMImplementation; use crate::dom::element::attributes::storage::AttrRef; use crate::dom::element::{CustomElementCreationMode, Element, ElementCreator}; use crate::dom::event::{Event, EventBubbles, EventCancelable}; use crate::dom::eventtarget::EventTarget; use crate::dom::execcommand::basecommand::{CommandName, DefaultSingleLineContainerName}; use crate::dom::execcommand::execcommands::DocumentExecCommandSupport; use crate::dom::focusevent::FocusEvent; use crate::dom::globalscope::GlobalScope; use crate::dom::hashchangeevent::HashChangeEvent; use crate::dom::history::History; use crate::dom::html::htmlanchorelement::HTMLAnchorElement; use crate::dom::html::htmlareaelement::HTMLAreaElement; use crate::dom::html::htmlbaseelement::HTMLBaseElement; use crate::dom::html::htmlcollection::{CollectionFilter, HTMLCollection}; use crate::dom::html::htmlelement::HTMLElement; use crate::dom::html::htmlembedelement::HTMLEmbedElement; use crate::dom::html::htmlformelement::{FormControl, FormControlElementHelpers, HTMLFormElement}; use crate::dom::html::htmlheadelement::HTMLHeadElement; use crate::dom::html::htmlhtmlelement::HTMLHtmlElement; use crate::dom::html::htmliframeelement::HTMLIFrameElement; use crate::dom::html::htmlimageelement::HTMLImageElement; use crate::dom::html::htmlscriptelement::{HTMLScriptElement, ScriptResult}; use crate::dom::html::htmltitleelement::HTMLTitleElement; use crate::dom::htmldetailselement::DetailsNameGroups; use crate::dom::intersectionobserver::IntersectionObserver; use crate::dom::iterators::ShadowIncluding; use crate::dom::keyboardevent::KeyboardEvent; use crate::dom::largestcontentfulpaint::LargestContentfulPaint; use crate::dom::location::Location; use crate::dom::messageevent::MessageEvent; use crate::dom::mouseevent::MouseEvent; use crate::dom::node::treewalker::TreeWalker; use crate::dom::node::virtualmethods::vtable_for; use crate::dom::node::{Node, NodeDamage, NodeFlags, NodeTraits}; use crate::dom::nodeiterator::NodeIterator; use crate::dom::nodelist::NodeList; use crate::dom::pagetransitionevent::PageTransitionEvent; use crate::dom::performance::performanceentry::PerformanceEntry; use crate::dom::performance::performancepainttiming::PerformancePaintTiming; use crate::dom::processinginstruction::ProcessingInstruction; use crate::dom::promise::Promise; use crate::dom::range::Range; use crate::dom::resizeobserver::{ResizeObservationDepth, ResizeObserver}; use crate::dom::sanitizer::Sanitizer; use crate::dom::selection::Selection; use crate::dom::servoparser::ServoParser; use crate::dom::shadowroot::ShadowRoot; use crate::dom::storageevent::StorageEvent; use crate::dom::text::Text; use crate::dom::touchevent::TouchEvent as DomTouchEvent; use crate::dom::touchlist::TouchList; use crate::dom::trustedtypes::trustedhtml::TrustedHTML; use crate::dom::types::{HTMLCanvasElement, VisibilityStateEntry}; use crate::dom::uievent::UIEvent; use crate::dom::window::Window; use crate::dom::window::scrolling_box::{ScrollAxisState, ScrollingBox}; use crate::dom::windowproxy::WindowProxy; use crate::dom::xpathevaluator::XPathEvaluator; use crate::dom::xpathexpression::XPathExpression; use crate::fetch::{DeferredFetchRecordInvokeState, FetchCanceller}; use crate::iframe_collection::IFrameCollection; use crate::image_animation::ImageAnimationManager; use crate::mime::{APPLICATION, CHARSET}; use crate::navigation::navigate; use crate::network_listener::{FetchResponseListener, NetworkListener}; use crate::script_thread::{ScriptThread, SharedRwLocks}; use crate::stylesheet_loader::StylesheetContextId; use crate::stylesheet_set::StylesheetSetRef; use crate::tasks::task::NonSendTaskBox; use crate::tasks::task_manager::TaskManager; use crate::tasks::task_source::TaskSourceName; use crate::timers::{OneshotTimerCallback, OneshotTimers}; use crate::xpath::parse_expression; #[derive(Clone, Copy, PartialEq)] pub(crate) enum FireMouseEventType { Move, Over, Out, Enter, Leave, } impl FireMouseEventType { pub(crate) fn as_str(&self) -> &str { match *self { FireMouseEventType::Move => "mousemove", FireMouseEventType::Over => "mouseover", FireMouseEventType::Out => "mouseout", FireMouseEventType::Enter => "mouseenter", FireMouseEventType::Leave => "mouseleave", } } } #[derive(JSTraceable, MallocSizeOf)] pub(crate) struct RefreshRedirectDue { #[no_trace] pub(crate) url: ServoUrl, /// Whether the refresh originated from a `<meta>` element. pub(crate) from_meta_element: bool, } impl RefreshRedirectDue { /// Step 13 of <https://html.spec.whatwg.org/multipage/#shared-declarative-refresh-steps> pub(crate) fn invoke(self, cx: &mut JSContext, global: &GlobalScope) { let window = global .downcast::<Window>() .expect("Queued a RefreshRedirectDue on a non-Window globalscope"); // After the refresh has come due (as defined below), // if the user has not canceled the redirect and, if meta is given, // document's active sandboxing flag set does not have the sandboxed // automatic features browsing context flag set, // then navigate document's node navigable to urlRecord using document, // with historyHandling set to "replace". if self.from_meta_element && window.Document().has_active_sandboxing_flag( SandboxingFlagSet::SANDBOXED_AUTOMATIC_FEATURES_BROWSING_CONTEXT_FLAG, ) { return; } let load_data = window.load_data_for_document(self.url, window.pipeline_id()); navigate( cx, window, NavigationHistoryBehavior::Replace, false, load_data, ); } } #[derive(Clone, Copy, Debug, JSTraceable, MallocSizeOf, PartialEq)] pub(crate) enum IsHTMLDocument { HTMLDocument, NonHTMLDocument, } #[derive(Clone, Copy, Default, MallocSizeOf, PartialEq)] pub(crate) enum TheEndLoadingPhase { #[default] Initial, ProcessingDeferredScripts, ProcessingAsSoonAsPossibleScripts, WaitingForLoadEventBlockers, Done, } /// Information about a declarative refresh #[derive(JSTraceable, MallocSizeOf)] pub(crate) enum DeclarativeRefresh { PendingLoad { #[no_trace] url: ServoUrl, time: u64, /// Whether the refresh originated from a `<meta>` element. from_meta_element: bool, }, CreatedAfterLoad, } #[derive(JSTraceable, MallocSizeOf, PartialEq)] #[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)] struct PendingScrollEvent { /// The target of this pending scroll event. target: Dom<EventTarget>, /// The kind of event. #[no_trace] event: Atom, } impl PendingScrollEvent { fn equivalent(&self, target: &EventTarget, event: &Atom) -> bool { &*self.target == target && self.event == *event } } /// Reasons why a [`Document`] might need a rendering update that is otherwise /// untracked via other [`Document`] properties. #[derive(Clone, Copy, Debug, Default, JSTraceable, MallocSizeOf)] pub(crate) struct RenderingUpdateReason(u8); bitflags! { impl RenderingUpdateReason: u8 { /// When a `ResizeObserver` starts observing a target, this becomes true, which in turn is a /// signal to the [`ScriptThread`] that a rendering update should happen. const ResizeObserverStartedObservingTarget = 1 << 0; /// When an `IntersectionObserver` starts observing a target, this becomes true, which in turn is a /// signal to the [`ScriptThread`] that a rendering update should happen. const IntersectionObserverStartedObservingTarget = 1 << 1; /// All web fonts have loaded and `fonts.ready` promise has been fulfilled. We want to trigger /// one more rendering update possibility after this happens, so that any potential screenshot /// reflects the up-to-date contents. const FontReadyPromiseFulfilled = 1 << 2; } } /// <https://html.spec.whatwg.org/multipage/#document-load-timing-info> #[derive(Clone, Debug, Default, MallocSizeOf)] pub(crate) struct NavigationTiming { pub(crate) dom_loading: Cell<Option<CrossProcessInstant>>, /// <https://html.spec.whatwg.org/multipage/#navigation-start-time> pub(crate) navigation_start: Cell<Option<CrossProcessInstant>>, /// <https://html.spec.whatwg.org/multipage/#unload-event-start-time> pub(crate) unload_event_start: Cell<Option<CrossProcessInstant>>, /// <https://html.spec.whatwg.org/multipage/#unload-event-end-time> pub(crate) unload_event_end: Cell<Option<CrossProcessInstant>>, /// <https://html.spec.whatwg.org/multipage/#dom-interactive-time> pub(crate) dom_interactive: Cell<Option<CrossProcessInstant>>, /// <https://html.spec.whatwg.org/multipage/#dom-content-loaded-event-start-time> pub(crate) dom_content_loaded_event_start: Cell<Option<CrossProcessInstant>>, /// <https://html.spec.whatwg.org/multipage/#dom-content-loaded-event-end-time> pub(crate) dom_content_loaded_event_end: Cell<Option<CrossProcessInstant>>, /// <https://html.spec.whatwg.org/multipage/#dom-complete-time> pub(crate) dom_complete: Cell<Option<CrossProcessInstant>>, /// <https://html.spec.whatwg.org/multipage/#load-event-start-time> pub(crate) load_event_start: Cell<Option<CrossProcessInstant>>, /// <https://html.spec.whatwg.org/multipage/#load-event-end-time> pub(crate) load_event_end: Cell<Option<CrossProcessInstant>>, /// Servo-only timing for when top-level content (not iframes) is complete pub(crate) top_level_dom_complete: Cell<Option<CrossProcessInstant>>, } /// <https://dom.spec.whatwg.org/#document> #[dom_struct] pub(crate) struct Document { node: Node, document_or_shadow_root: DocumentOrShadowRoot, window: Dom<Window>, implementation: MutNullableDom<DOMImplementation>, #[ignore_malloc_size_of = "type from external crate"] #[no_trace] content_type: Mime, last_modified: Option<String>, #[no_trace] encoding: Cell<&'static Encoding>, has_browsing_context: bool, is_html_document: bool, #[no_trace] activity: Cell<DocumentActivity>, /// <https://html.spec.whatwg.org/multipage/#the-document%27s-address> #[no_trace] url: DomRefCell<ServoUrl>, /// <https://html.spec.whatwg.org/multipage/#concept-document-about-base-url> #[no_trace] about_base_url: DomRefCell<Option<ServoUrl>>, #[ignore_malloc_size_of = "defined in selectors"] #[no_trace] quirks_mode: Cell<QuirksMode>, /// A helper used to process and store data related to input event handling. event_handler: DocumentEventHandler, /// A helper used to process and store data related to focus handling. focus_handler: DocumentFocusHandler, /// A helper to handle showing and hiding user interface controls in the embedding layer. embedder_controls: DocumentEmbedderControls, id_map: TreeOrderedIndexMap, name_map: TreeOrderedIndexMap, tag_map: DomRefCell<HashMapTracedValues<LocalName, Dom<HTMLCollection>, FxBuildHasher>>, tagns_map: DomRefCell<HashMapTracedValues<QualName, Dom<HTMLCollection>, FxBuildHasher>>, classes_map: DomRefCell<HashMapTracedValues<Vec<Atom>, Dom<HTMLCollection>>>, images: MutNullableDom<HTMLCollection>, embeds: MutNullableDom<HTMLCollection>, links: MutNullableDom<HTMLCollection>, forms: MutNullableDom<HTMLCollection>, scripts: MutNullableDom<HTMLCollection>, anchors: MutNullableDom<HTMLCollection>, applets: MutNullableDom<HTMLCollection>, /// Information about the `<iframes>` in this [`Document`]. iframes: RefCell<IFrameCollection>, /// Shared locks used for style attributes, author-origin stylesheets, and user and /// user agent stylesheets in this document. Can be acquired once for accessing many /// objects. This is shared with the owning [`ScriptThread`]. #[no_trace] shared_style_locks: SharedRwLocks, /// List of stylesheets associated with nodes in this document. |None| if the list needs to be refreshed. #[custom_trace] stylesheets: DomRefCell<DocumentStylesheetSet<ServoStylesheetInDocument>>, stylesheet_list: MutNullableDom<StyleSheetList>, ready_state: Cell<DocumentReadyState>, /// Whether the DOMContentLoaded event has already been dispatched. /// TODO(43149): Remove when document replacement is implemented domcontentloaded_dispatched: Cell<bool>, /// The script element that is currently executing. current_script: MutNullableDom<HTMLScriptElement>, #[no_trace] current_the_end_loading_phase: Cell<TheEndLoadingPhase>, /// <https://html.spec.whatwg.org/multipage/#pending-parsing-blocking-script> pending_parsing_blocking_script: DomRefCell<Option<PendingScript>>, /// <https://html.spec.whatwg.org/multipage/#script-blocking-style-sheet-set> /// > A Document has a script-blocking style sheet set, which is an ordered set, initially empty. script_blocking_stylesheet_set: DomRefCell<IndexSet<StylesheetContextId>>, /// Number of elements that block the rendering of the page. /// <https://html.spec.whatwg.org/multipage/#implicitly-potentially-render-blocking> render_blocking_element_count: Cell<u32>, /// <https://html.spec.whatwg.org/multipage/#list-of-scripts-that-will-execute-when-the-document-has-finished-parsing> deferred_scripts: PendingInOrderScriptVec, /// <https://html.spec.whatwg.org/multipage/#list-of-scripts-that-will-execute-in-order-as-soon-as-possible> asap_in_order_scripts_list: PendingInOrderScriptVec, /// <https://html.spec.whatwg.org/multipage/#set-of-scripts-that-will-execute-as-soon-as-possible> asap_scripts_set: DomRefCell<Vec<Dom<HTMLScriptElement>>>, /// <https://html.spec.whatwg.org/multipage/#animation-frame-callback-identifier> /// Current identifier of animation frame callback animation_frame_ident: Cell<u32>, /// <https://html.spec.whatwg.org/multipage/#list-of-animation-frame-callbacks> /// List of animation frame callbacks animation_frame_list: DomRefCell<VecDeque<(u32, Option<AnimationFrameCallback>)>>, /// Whether we're in the process of running animation callbacks. /// /// Tracking this is not necessary for correctness. Instead, it is an optimization to avoid /// sending needless `ChangeRunningAnimationsState` messages to `Paint`. running_animation_callbacks: Cell<bool>, /// Tracks all outstanding loads related to this document. loader: DomRefCell<DocumentLoader>, /// The current active HTML parser, to allow resuming after interruptions. current_parser: MutNullableDom<ServoParser>, /// The cached first `base` element with an `href` attribute. base_element: MutNullableDom<HTMLBaseElement>, /// The cached first `base` element, used for its target (doesn't need a href) target_base_element: MutNullableDom<HTMLBaseElement>, /// This field is set to the document itself for inert documents. /// <https://html.spec.whatwg.org/multipage/#appropriate-template-contents-owner-document> appropriate_template_contents_owner_document: MutNullableDom<Document>, /// Information on elements needing restyle to ship over to layout when the /// time comes. pending_restyles: DomRefCell<FxHashMap<Dom<Element>, NoTrace<PendingRestyle>>>, /// A collection of reasons that the [`Document`] needs to be restyled at the next /// opportunity for a reflow. If this is empty, then the [`Document`] does not need to /// be restyled. #[no_trace] needs_restyle: Cell<RestyleReason>, /// The document's origin. #[no_trace] origin: DomRefCell<MutableOrigin>, /// <https://html.spec.whatwg.org/multipage/#dom-document-referrer> referrer: Option<String>, /// <https://html.spec.whatwg.org/multipage/#target-element> target_element: MutNullableDom<Element>, /// <https://html.spec.whatwg.org/multipage/#concept-document-policy-container> #[no_trace] policy_container: DomRefCell<PolicyContainer>, /// <https://html.spec.whatwg.org/multipage/#map-of-preloaded-resources> #[no_trace] preloaded_resources: DomRefCell<PreloadedResources>, /// <https://html.spec.whatwg.org/multipage/#ignore-destructive-writes-counter> ignore_destructive_writes_counter: Cell<u32>, /// <https://html.spec.whatwg.org/multipage/#ignore-opens-during-unload-counter> ignore_opens_during_unload_counter: Cell<u32>, /// The number of spurious `requestAnimationFrame()` requests we've received. /// /// A rAF request is considered spurious if nothing was actually reflowed. spurious_animation_frames: Cell<u8>, /// Entry node for fullscreen. fullscreen_element: MutNullableDom<Element>, /// Map from ID to set of form control elements that have that ID as /// their 'form' content attribute. Used to reset form controls /// whenever any element with the same ID as the form attribute /// is inserted or removed from the document. /// See <https://html.spec.whatwg.org/multipage/#form-owner> /// It is safe to use FxBuildHasher here as Atoms are in the string_cache form_id_listener_map: DomRefCell<HashMapTracedValues<Atom, HashSet<Dom<Element>>, FxBuildHasher>>, #[no_trace] interactive_time: DomRefCell<ProgressiveWebMetrics>, #[no_trace] tti_window: DomRefCell<InteractiveWindow>, /// RAII canceller for Fetch canceller: FetchCanceller, /// <https://html.spec.whatwg.org/multipage/#throw-on-dynamic-markup-insertion-counter> throw_on_dynamic_markup_insertion_counter: Cell<u64>, /// <https://html.spec.whatwg.org/multipage/#page-showing> page_showing: Cell<bool>, /// Whether the document is salvageable. salvageable: Cell<bool>, /// Whether the document was aborted with an active parser active_parser_was_aborted: Cell<bool>, /// Whether the unload event has already been fired. fired_unload: Cell<bool>, /// List of responsive images responsive_images: DomRefCell<Vec<Dom<HTMLImageElement>>>, /// [`NavigationTiming`] information for this [`Document`]. /// <https://html.spec.whatwg.org/multipage/#load-timing-info> #[no_trace] #[conditional_malloc_size_of] navigation_timing: Rc<NavigationTiming>, /// A [`ResourceFetchTiming`] that holds timing information for this [`Document`]. #[no_trace] resource_fetch_timing: RefCell<Option<ResourceFetchTiming>>, /// Number of outstanding requests to prevent JS or layout from running. script_and_layout_blockers: Cell<u32>, /// List of tasks to execute as soon as last script/layout blocker is removed. #[ignore_malloc_size_of = "Measuring trait objects is hard"] delayed_tasks: DomRefCell<Vec<Box<dyn NonSendTaskBox>>>, /// <https://html.spec.whatwg.org/multipage/#completely-loaded> completely_loaded: Cell<bool>, /// Set of shadow roots connected to the document tree. shadow_roots: DomRefCell<HashSet<Dom<ShadowRoot>>>, /// Whether any of the shadow roots need the stylesheets flushed. shadow_roots_styles_changed: Cell<bool>, /// List of registered media controls. /// We need to keep this list to allow the media controls to /// access the "privileged" document.servoGetMediaControls(id) API, /// where `id` needs to match any of the registered ShadowRoots /// hosting the media controls UI. media_controls: DomRefCell<HashMap<String, Dom<ShadowRoot>>>, /// A set of dirty HTML canvas elements that need their WebRender images updated the /// next time the rendering is updated. dirty_canvases: DomRefCell<Vec<Dom<HTMLCanvasElement>>>, /// Whether or not animated images need to have their contents updated. has_pending_animated_image_update: Cell<bool>, /// <https://w3c.github.io/slection-api/#dfn-selection> selection: MutNullableDom<Selection>, /// A timeline for animations which is used for synchronizing animations. /// <https://drafts.csswg.org/web-animations/#timeline> timeline: Dom<DocumentTimeline>, /// Animations for this Document animations: Animations, /// Image Animation Manager for this Document image_animation_manager: DomRefCell<ImageAnimationManager>, /// The nearest inclusive ancestors to all the nodes that require a restyle. dirty_root: MutNullableDom<Element>, /// <https://html.spec.whatwg.org/multipage/#will-declaratively-refresh> declarative_refresh: DomRefCell<Option<DeclarativeRefresh>>, /// <https://drafts.csswg.org/resize-observer/#dom-document-resizeobservers-slot> /// /// Note: we are storing, but never removing, resize observers. /// The lifetime of resize observers is specified at /// <https://drafts.csswg.org/resize-observer/#lifetime>. /// But implementing it comes with known problems: /// - <https://bugzilla.mozilla.org/show_bug.cgi?id=1596992> /// - <https://github.com/w3c/csswg-drafts/issues/4518> resize_observers: DomRefCell<Vec<Dom<ResizeObserver>>>, /// The set of all fonts loaded by this document. /// <https://drafts.csswg.org/css-font-loading/#font-face-source> fonts: MutNullableDom<FontFaceSet>, /// <https://html.spec.whatwg.org/multipage/#visibility-state> visibility_state: Cell<DocumentVisibilityState>, /// <https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml> status_code: Option<u16>, /// <https://html.spec.whatwg.org/multipage/#is-initial-about:blank> is_initial_about_blank: Cell<bool>, /// <https://dom.spec.whatwg.org/#document-allow-declarative-shadow-roots> allow_declarative_shadow_roots: Cell<bool>, /// <https://w3c.github.io/webappsec-upgrade-insecure-requests/#insecure-requests-policy> #[no_trace] inherited_insecure_requests_policy: Cell<Option<InsecureRequestsPolicy>>, //// <https://w3c.github.io/webappsec-mixed-content/#categorize-settings-object> has_trustworthy_ancestor_origin: Cell<bool>, /// <https://w3c.github.io/IntersectionObserver/#document-intersectionobservertaskqueued> intersection_observer_task_queued: Cell<bool>, /// Active intersection observers that should be processed by this document in /// the update intersection observation steps. /// <https://w3c.github.io/IntersectionObserver/#run-the-update-intersection-observations-steps> /// > Let observer list be a list of all IntersectionObservers whose root is in the DOM tree of document. /// > For the top-level browsing context, this includes implicit root observers. /// /// Details of which document that should process an observers is discussed further at /// <https://github.com/w3c/IntersectionObserver/issues/525>. /// /// The lifetime of an intersection observer is specified at /// <https://github.com/w3c/IntersectionObserver/issues/525>. intersection_observers: DomRefCell<Vec<Dom<IntersectionObserver>>>, /// The node that is currently highlighted by the devtools highlighted_dom_node: MutNullableDom<Node>, /// Resolved LCP candidate elements, keyed by their [LCPCandidateID]. lcp_candidates: DomRefCell<HashMapTracedValues<LCPCandidateID, Dom<Element>>>, /// The constructed stylesheet that is adopted by this [Document]. /// <https://drafts.csswg.org/cssom/#dom-documentorshadowroot-adoptedstylesheets> adopted_stylesheets: DomRefCell<Vec<Dom<CSSStyleSheet>>>, /// Cached frozen array of [`Self::adopted_stylesheets`] #[ignore_malloc_size_of = "mozjs"] adopted_stylesheets_frozen_types: CachedFrozenArray, /// <https://drafts.csswg.org/cssom-view/#document-pending-scroll-events> /// > Each Document has an associated list of pending scroll events, which stores /// > pairs of (EventTarget, DOMString), initially empty. pending_scroll_events: DomRefCell<Vec<PendingScrollEvent>>, /// Other reasons that a rendering update might be required for this [`Document`]. rendering_update_reasons: Cell<RenderingUpdateReason>, /// Whether or not this [`Document`] is waiting on canvas image updates. If it is /// waiting it will not do any new layout until the canvas images are up-to-date in /// the renderer. waiting_on_canvas_image_updates: Cell<bool>, /// Whether we have already noted that the document element was removed. root_removal_noted: Cell<bool>, /// The current rendering epoch, which is used to track updates in the renderer. /// /// - Every display list update also advances the Epoch, so that the renderer knows /// when a particular display list is ready in order to take a screenshot. /// - Canvas image updates happen asynchronously and are tagged with this Epoch. Until /// those asynchronous updates are complete, the `Document` will not perform any /// more rendering updates. #[no_trace] current_rendering_epoch: Cell<Epoch>, /// The global custom element reaction stack for this script thread. #[conditional_malloc_size_of] custom_element_reaction_stack: Rc<CustomElementReactionStack>, #[no_trace] /// <https://html.spec.whatwg.org/multipage/#active-sandboxing-flag-set>, active_sandboxing_flag_set: Cell<SandboxingFlagSet>, #[no_trace] /// The [`SandboxingFlagSet`] use to create the browsing context for this [`Document`]. /// These are cached here as they cannot always be retrieved readily if the owner of /// browsing context (either `<iframe>` or popup) might be in a different `ScriptThread`. /// /// See /// <https://html.spec.whatwg.org/multipage/#determining-the-creation-sandboxing-flags>. creation_sandboxing_flag_set: Cell<SandboxingFlagSet>, /// The cached favicon for that document. #[no_trace] favicon: RefCell<Option<Image>>, /// All websockets created that are associated with this document. websockets: DOMTracker<WebSocket>, /// <https://html.spec.whatwg.org/multipage/#details-name-group> details_name_groups: DomRefCell<Option<DetailsNameGroups>>, /// <https://html.spec.whatwg.org/multipage/#registerprotocolhandler()-automation-mode> #[no_trace] protocol_handler_automation_mode: RefCell<CustomHandlersAutomationMode>, /// Reflect the value of that preferences to prevent paying the cost of a RwLock access. layout_animations_test_enabled: bool, /// <https://w3c.github.io/editing/docs/execCommand/#state-override> #[no_trace] state_override: DomRefCell<FxHashMap<CommandName, bool>>, /// <https://w3c.github.io/editing/docs/execCommand/#value-override> #[no_trace] value_override: DomRefCell<FxHashMap<CommandName, DOMString>>, /// <https://w3c.github.io/editing/docs/execCommand/#default-single-line-container-name> #[no_trace] default_single_line_container_name: Cell<DefaultSingleLineContainerName>, /// <https://w3c.github.io/editing/docs/execCommand/#css-styling-flag> css_styling_flag: Cell<bool>, /// Data necessary for maintaining the accessibility tree. accessibility_data: DomRefCell<AccessibilityData>, /// <https://html.spec.whatwg.org/multipage/#iframe-load-in-progress> iframe_load_in_progress: Cell<bool>, /// <https://html.spec.whatwg.org/multipage/#mute-iframe-load> mute_iframe_load: Cell<bool>, /// The mechanism by which time-outs and intervals are scheduled. /// <https://html.spec.whatwg.org/multipage/#timers> timers: OneshotTimers, #[no_trace] pipeline_id: PipelineId, /// A [`TaskManager`] for this [`Window`]. #[conditional_malloc_size_of] task_manager: Rc<TaskManager>, #[ignore_malloc_size_of = "ImageCache"] #[no_trace] image_cache: StdArc<dyn ImageCache>, /// <https://html.spec.whatwg.org/multipage/#doc-history> history: MutNullableDom<History>, } impl Document { pub(crate) fn history(&self, cx: &mut JSContext) -> DomRoot<History> { self.history.or_init(|| History::new(cx, &self.window)) } pub(crate) fn image_cache(&self) -> StdArc<dyn ImageCache> { self.image_cache.clone() } pub(crate) fn task_manager(&self) -> Rc<TaskManager> { self.task_manager.clone() } pub(crate) fn timers(&self) -> &OneshotTimers { &self.timers } pub(crate) fn pipeline_id(&self) -> PipelineId { self.pipeline_id } /// <https://html.spec.whatwg.org/multipage/#unloading-document-cleanup-steps> fn unloading_cleanup_steps(&self) { // Step 1. Let window be document's relevant global object. // Step 2. For each WebSocket object webSocket whose relevant global object is window, make disappear webSocket. if self.close_outstanding_websockets() { // If this affected any WebSocket objects, then make document unsalvageable given document and "websocket". self.salvageable.set(false); } // Step 3. For each WebTransport object transport whose relevant global object is window, run the context cleanup steps given transport. // TODO // Step 4. If document's salvageable state is false, then: if !self.salvageable.get() { let global_scope = self.window.as_global_scope(); // Step 4.1. For each EventSource object eventSource whose relevant global object is equal to window, forcibly close eventSource. global_scope.close_event_sources(); // Step 4.2. Clear window's map of active timers. // TODO // Ensure the constellation discards all bfcache information for this document. let msg = ScriptToConstellationMessage::DiscardDocument; let _ = global_scope.script_to_constellation_chan().send(msg); } } pub(crate) fn track_websocket(&self, websocket: &WebSocket) { self.websockets.track(websocket); } fn close_outstanding_websockets(&self) -> bool { let mut closed_any_websocket = false; self.websockets.for_each(|websocket: DomRoot<WebSocket>| { if websocket.make_disappear() { closed_any_websocket = true; } }); closed_any_websocket } fn document_element_changed(&self) { if self.GetDocumentElement().is_some() { // This ensures that if the document element is removed in the future, it // will trigger a new empty display list. self.root_removal_noted.set(false); } else if !self.root_removal_noted.get() { // If there is no document element, attempt to trigger a new root removal update, // but do not do any updating of the dirty root or HAS_DIRTY_DESCENDANTS flags. self.add_restyle_reason(RestyleReason::DOMChanged); self.root_removal_noted.set(true); } } /// This is a port of Gecko's restyle root architecture. The idea is that we track a /// node which is the root of restyle damage. Below that root, certain nodes can be /// marked with a HAS_DIRTY_DESCENDANTS flag which means they should be traversed /// during styling and damage propagation. Above the dirty root nothing should be /// marked with the HAS_DIRTY_DESCENDANTS flag. /// /// The overall algorithm is as follows: /// * When the first dirty element is noted, we just set it as the restyle root. /// * When additional dirty elements are noted, we propagate the given bit up /// the tree, until we either reach the dirty root or the document root. /// * If we reach the document root, we then propagate the HAS_DIRTY_DESCENDANTS /// flags up the tree until we cross the path of the new root. Once /// we find this common ancestor, we record it as the restyle root, and then /// clear the bits between the new restyle root and the document root. pub(crate) fn note_dirty_element(&self, element: &Element) { let node = element.upcast::<Node>(); debug_assert!(*node.owner_doc() == *self); if !node.is_connected() { return; } let parent_element = match node.parent_in_flat_tree() { FlatTreeParent::Parent(parent) => DomRoot::downcast::<Element>(parent), FlatTreeParent::NotInFlatTree | FlatTreeParent::RootNode => return, }; // The node may not have a parent element if it is a direct descendant of the // `Document` node (i.e. it is the document element aka the `<html>` element in HTML // documents). if let Some(parent_element) = parent_element { // If the parent isn't styled, then it either isn't part of the flat tree or will // be styled later, ensuring the layout of the dirtied node as well. if !parent_element.is_styled() { return; } // If the parent has `display: none`, the change that caused the node to be dirty // will not affect style or layout. if parent_element.is_display_none() { return; } } let Some(old_dirty_root) = self.dirty_root.get() else { node.set_flag(NodeFlags::HAS_DIRTY_DESCENDANTS, true); self.set_dirty_root(Some(element)); return; }; let old_dirty_root_node = old_dirty_root.upcast::<Node>(); for ancestor in element.upcast::<Node>().inclusive_ancestors_in_flat_tree() { // Never mark the Document node as having dirty descendants. It's never the dirty root. if !ancestor.is::<Element>() { break; } if ancestor.get_flag(NodeFlags::HAS_DIRTY_DESCENDANTS) { return; } ancestor.set_flag(NodeFlags::HAS_DIRTY_DESCENDANTS, true); // If this node is already under the existing dirty root, there is nothing else to // do apart from marking this node as having dirty descendants. We need to ensure // we mark the root as having dirty descendants now because that has become true. if old_dirty_root_node == &*ancestor { return; } } let common_element_ancestor = old_dirty_root_node .inclusive_ancestors_in_flat_tree() .skip(1) // Skip the old root itself. .find_map(|ancestor| { // Never mark the Document node as having dirty descendants. It's never the dirty root. let element = ancestor.downcast::<Element>().map(DomRoot::from_ref)?; if ancestor.get_flag(NodeFlags::HAS_DIRTY_DESCENDANTS) { return Some(element); } ancestor.set_flag(NodeFlags::HAS_DIRTY_DESCENDANTS, true); None }); // In the case that there was no common ancestor dirty root, one or both of the nodes // is not in the flat tree any longer. When this happens just move the dirty root to // the document element. let Some(new_dirty_root) = common_element_ancestor else { let new_dirty_root = self.GetDocumentElement(); if let Some(new_dirty_root) = new_dirty_root.as_ref() { new_dirty_root .upcast::<Node>() .set_flag(NodeFlags::HAS_DIRTY_DESCENDANTS, true); } self.set_dirty_root(new_dirty_root.as_deref()); return; }; // Now mark all nodes *above* the new dirty root as not having dirty descendants // to ensure our invariant that nothing above the dirty root is marked with this // flag. for ancestor in new_dirty_root .upcast::<Node>() .inclusive_ancestors_in_flat_tree() .skip(1) { ancestor.set_flag(NodeFlags::HAS_DIRTY_DESCENDANTS, false) } self.set_dirty_root(Some(&*new_dirty_root)); } fn set_dirty_root(&self, new_dirty_root: Option<&Element>) { // Assertion: No nodes above the dirty root should be marked with the HAS_DIRTY_DESCENDANTS flag. debug_assert!(new_dirty_root.as_ref().is_none_or(|new_dirty_root| { new_dirty_root .upcast::<Node>() .inclusive_ancestors_in_flat_tree() .skip(1) .all(|node| !node.get_flag(NodeFlags::HAS_DIRTY_DESCENDANTS)) })); self.dirty_root.set(new_dirty_root); } pub(crate) fn take_dirty_root(&self) -> Option<DomRoot<Element>> { self.dirty_root.take() } #[inline] pub(crate) fn loader(&self) -> Ref<'_, DocumentLoader> { self.loader.borrow() } #[inline] pub(crate) fn loader_mut(&self) -> RefMut<'_, DocumentLoader> { self.loader.borrow_mut() } #[inline] pub(crate) fn has_browsing_context(&self) -> bool { self.has_browsing_context } /// <https://html.spec.whatwg.org/multipage/#concept-document-bc> #[inline] pub(crate) fn browsing_context(&self) -> Option<DomRoot<WindowProxy>> { if self.has_browsing_context { self.window.undiscarded_window_proxy() } else { None } } pub(crate) fn webview_id(&self) -> WebViewId { self.window.webview_id() } #[inline] pub(crate) fn window(&self) -> &Window { &self.window } #[inline] pub(crate) fn is_html_document(&self) -> bool { self.is_html_document } pub(crate) fn is_xhtml_document(&self) -> bool { self.content_type.matches(APPLICATION, "xhtml+xml") } pub(crate) fn is_fully_active(&self) -> bool { self.activity.get() == DocumentActivity::FullyActive } pub(crate) fn is_active(&self) -> bool { self.activity.get() != DocumentActivity::Inactive } #[inline] pub(crate) fn current_rendering_epoch(&self) -> Epoch { self.current_rendering_epoch.get() } /// Get the [`Selection`] instance for this [`Document`] if there is one or `None`. #[inline] pub(crate) fn selection(&self) -> Option<DomRoot<Selection>> { self.selection.get() } pub(crate) fn set_activity(&self, cx: &mut JSContext, activity: DocumentActivity) { // This function should only be called on documents with a browsing context assert!(self.has_browsing_context); if activity == self.activity.get() { return; } // Set the document's activity level, reflow if necessary, and suspend or resume timers. self.activity.set(activity); let media = ServoMedia::get(); let pipeline_id = self.window().pipeline_id(); let client_context_id = ClientContextId::build(pipeline_id.namespace_id.0, pipeline_id.index.0.get()); if activity != DocumentActivity::FullyActive { self.window().suspend(cx); media.suspend(&client_context_id); return; } self.title_changed(); self.notify_embedder_favicon(); self.dirty_all_nodes(cx.no_gc()); self.window().resume(cx); media.resume(&client_context_id); if self.ready_state.get() != DocumentReadyState::Complete { return; } // This step used to be Step 4.6 in html.spec.whatwg.org/multipage/#history-traversal // But it's now Step 4 in https://html.spec.whatwg.org/multipage/#reactivate-a-document // TODO: See #32687 for more information. let document = Trusted::new(self); self.owner_global() .task_manager() .dom_manipulation_task_source() .queue(task!(fire_pageshow_event: move |cx| { let document = document.root(); let window = document.window(); // Step 4.6.1 if document.page_showing.get() { return; } // Step 4.6.2 Set document's page showing flag to true. document.page_showing.set(true); // Step 4.6.3 Update the visibility state of document to "visible". document.update_visibility_state(cx, DocumentVisibilityState::Visible); // Step 4.6.4 Fire a page transition event named pageshow at document's relevant // global object with true. let event = PageTransitionEvent::new( cx, window, atom!("pageshow"), false, // bubbles false, // cancelable true, // persisted ); let event = event.upcast::<Event>(); event.set_trusted(true); window.dispatch_event_with_target_override(cx, event); })) } pub(crate) fn origin(&self) -> Ref<'_, MutableOrigin> { self.origin.borrow() } /// Part of <https://html.spec.whatwg.org/multipage/#navigate-ua-inline> /// TODO: Remove this when we create documents after processing headers pub(crate) fn mark_as_internal(&self) { *self.origin.borrow_mut() = MutableOrigin::new(ImmutableOrigin::new_opaque()); } pub(crate) fn set_protocol_handler_automation_mode(&self, mode: CustomHandlersAutomationMode) { *self.protocol_handler_automation_mode.borrow_mut() = mode; } /// <https://dom.spec.whatwg.org/#concept-document-url> pub(crate) fn url(&self) -> ServoUrl { self.url.borrow().clone() } pub(crate) fn set_url(&self, url: ServoUrl) { *self.url.borrow_mut() = url; } pub(crate) fn about_base_url(&self) -> Option<ServoUrl> { self.about_base_url.borrow().clone() } pub(crate) fn set_about_base_url(&self, about_base_url: Option<ServoUrl>) { *self.about_base_url.borrow_mut() = about_base_url; } /// <https://html.spec.whatwg.org/multipage/#fallback-base-url> pub(crate) fn fallback_base_url(&self) -> ServoUrl { let document_url = self.url(); // Step 1: If document is an iframe srcdoc document: if document_url.as_str() == "about:srcdoc" { // Step 1.1: Assert: document's about base URL is non-null. // Step 1.2: Return document's about base URL. return self .about_base_url() .expect("about:srcdoc page should always have an about base URL"); } // Step 2: If document's URL matches about:blank and document's about base URL is // non-null, then return document's about base URL. if document_url.matches_about_blank() && let Some(about_base_url) = self.about_base_url() { return about_base_url; } // Step 3: Return document's URL. document_url } /// <https://html.spec.whatwg.org/multipage/#document-base-url> pub(crate) fn base_url(&self) -> ServoUrl { match self.base_element() { // Step 1. None => self.fallback_base_url(), // Step 2. Some(base) => base.frozen_base_url(), } } pub(crate) fn add_restyle_reason(&self, reason: RestyleReason) { self.needs_restyle.set(self.needs_restyle.get() | reason) } pub(crate) fn clear_restyle_reasons(&self) { self.needs_restyle.set(RestyleReason::empty()); } pub(crate) fn stylesheets_changed_since_last_reflow(&self) -> bool { self.stylesheets.borrow().has_changed() } pub(crate) fn restyle_reason(&self, no_gc: &NoGC) -> RestyleReason { let mut condition = self.needs_restyle.get(); if self.stylesheets_changed_since_last_reflow() { condition.insert(RestyleReason::StylesheetsChanged); } // FIXME: This should check the dirty bit on the document, // not the document element. Needs some layout changes to make // that workable. if let Some(root) = self.get_document_element_unrooted(no_gc) && root.has_dirty_descendants() { condition.insert(RestyleReason::DOMChanged); } if !self.pending_restyles.borrow().is_empty() { condition.insert(RestyleReason::PendingRestyles); } condition } /// Returns the first `base` element in the DOM that has an `href` attribute. pub(crate) fn base_element(&self) -> Option<DomRoot<HTMLBaseElement>> { self.base_element.get() } /// Returns the first `base` element in the DOM (doesn't need to have an `href` attribute). pub(crate) fn target_base_element(&self) -> Option<DomRoot<HTMLBaseElement>> { self.target_base_element.get() } /// Refresh the cached first base element in the DOM. pub(crate) fn refresh_base_element(&self, cx: &mut JSContext) { if let Some(base_element) = self.base_element.get() { base_element.clear_frozen_base_url(); } let new_base_element = self .upcast::<Node>() .traverse_preorder(ShadowIncluding::No) .filter_map(DomRoot::downcast::<HTMLBaseElement>) .find(|element| { element .upcast::<Element>() .has_attribute(&local_name!("href")) }); if let Some(ref new_base_element) = new_base_element { new_base_element.set_frozen_base_url(cx); } self.base_element.set(new_base_element.as_deref()); let new_target_base_element = self .upcast::<Node>() .traverse_preorder(ShadowIncluding::No) .filter_map(DomRoot::downcast::<HTMLBaseElement>) .next(); self.target_base_element .set(new_target_base_element.as_deref()); } pub(crate) fn quirks_mode(&self) -> QuirksMode { self.quirks_mode.get() } pub(crate) fn set_quirks_mode(&self, new_mode: QuirksMode) { let old_mode = self.quirks_mode.replace(new_mode); if old_mode != new_mode { self.window.layout_mut().set_quirks_mode(new_mode); } } pub(crate) fn encoding(&self) -> &'static Encoding { self.encoding.get() } pub(crate) fn set_encoding(&self, encoding: &'static Encoding) { self.encoding.set(encoding); } pub(crate) fn content_and_heritage_changed(&self, no_gc: &NoGC, node: &Node) { if node.is::<Document>() { self.document_element_changed(); } // TODO: A change to the children of a node only affects style when dealing with // selectors like `:has()`, so the application of this restyle should be more // targeted like in Gecko. // See https://searchfox.org/firefox-main/rev/7d438b99e58d16388e4327f2460d14ad4c8be075/layout/style/RestyleManager.cpp#245. node.dirty(no_gc, NodeDamage::ContentOrHeritage); } /// Remove any existing association between the provided id and any elements in this document. pub(crate) fn unregister_element_id(&self, cx: &mut JSContext, id: &Atom) { self.id_map.remove(id); self.reset_form_owner_for_listeners(cx, id); } /// Associate an element present in this document with the provided id. pub(crate) fn register_element_id(&self, cx: &mut JSContext, element: &Element, id: &Atom) { self.id_map.add(id, element); self.reset_form_owner_for_listeners(cx, id); } /// Remove any existing association between the provided name and any elements in this document. pub(crate) fn unregister_element_name(&self, name: &Atom) { self.name_map.remove(name); } /// Associate an element present in this document with the provided name. pub(crate) fn register_element_name(&self, element: &Element, name: &Atom) { self.name_map.add(name, element); } pub(crate) fn register_form_id_listener<T: ?Sized + FormControl>( &self, id: DOMString, listener: &T, ) { let mut map = self.form_id_listener_map.borrow_mut(); let listener = listener.to_element(); let set = map.entry(Atom::from(id)).or_default(); set.insert(Dom::from_ref(listener)); } pub(crate) fn unregister_form_id_listener<T: ?Sized + FormControl>( &self, id: DOMString, listener: &T, ) { let mut map = self.form_id_listener_map.borrow_mut(); if let Occupied(mut entry) = map.entry(Atom::from(id)) { entry .get_mut() .remove(&Dom::from_ref(listener.to_element())); if entry.get().is_empty() { entry.remove(); } } } /// <https://html.spec.whatwg.org/multipage/#find-a-potential-indicated-element> fn find_a_potential_indicated_element( &self, cx: &mut JSContext, fragment: &str, ) -> Option<DomRoot<Element>> { // Step 1. If there is an element in the document tree whose root is // document and that has an ID equal to fragment, then return the first such element in tree order. // Step 3. Return null. self.get_element_by_id(cx.no_gc(), &Atom::from(fragment)) // Step 2. If there is an a element in the document tree whose root is // document that has a name attribute whose value is equal to fragment, // then return the first such element in tree order. .or_else(|| self.get_anchor_by_name(cx, fragment)) } /// Attempt to find a named element in this page's document. /// <https://html.spec.whatwg.org/multipage/#the-indicated-part-of-the-document> fn select_indicated_part(&self, cx: &mut JSContext, fragment: &str) -> Option<DomRoot<Node>> { // Step 1. If document's URL does not equal url with exclude fragments set to true, then return null. // // Already handled by calling function // Step 2. Let fragment be url's fragment. // // Already handled by calling function // Step 3. If fragment is the empty string, then return the special value top of the document. if fragment.is_empty() { return Some(DomRoot::from_ref(self.upcast())); } // Step 4. Let potentialIndicatedElement be the result of finding a potential indicated element given document and fragment. if let Some(potential_indicated_element) = self.find_a_potential_indicated_element(cx, fragment) { // Step 5. If potentialIndicatedElement is not null, then return potentialIndicatedElement. return Some(DomRoot::upcast(potential_indicated_element)); } // Step 6. Let fragmentBytes be the result of percent-decoding fragment. let fragment_bytes = percent_decode(fragment.as_bytes()); // Step 7. Let decodedFragment be the result of running UTF-8 decode without BOM on fragmentBytes. let Ok(decoded_fragment) = fragment_bytes.decode_utf8() else { return None; }; // Step 8. Set potentialIndicatedElement to the result of finding a potential indicated element given document and decodedFragment. if let Some(potential_indicated_element) = self.find_a_potential_indicated_element(cx, &decoded_fragment) { // Step 9. If potentialIndicatedElement is not null, then return potentialIndicatedElement. return Some(DomRoot::upcast(potential_indicated_element)); } // Step 10. If decodedFragment is an ASCII case-insensitive match for the string top, then return the top of the document. if decoded_fragment.eq_ignore_ascii_case("top") { return Some(DomRoot::from_ref(self.upcast())); } // Step 11. Return null. None } /// <https://html.spec.whatwg.org/multipage/#scroll-to-the-fragment-identifier> pub(crate) fn scroll_to_the_fragment(&self, cx: &mut JSContext, fragment: &str) { // Step 1. If document's indicated part is null, then set document's target element to null. // // > For an HTML document document, its indicated part is the result of // > selecting the indicated part given document and document's URL. let Some(indicated_part) = self.select_indicated_part(cx, fragment) else { self.set_target_element(None); return; }; // Step 2. Otherwise, if document's indicated part is top of the document, then: if *indicated_part == *self.upcast() { // Step 2.1. Set document's target element to null. self.set_target_element(None); // Step 2.2. Scroll to the beginning of the document for document. [CSSOMVIEW] // // FIXME(stshine): this should be the origin of the stacking context space, // which may differ under the influence of writing mode. self.window.scroll(cx, 0.0, 0.0, ScrollBehavior::Instant); // Step 2.3. Return. return; } // Step 3. Otherwise: // Step 3.2. Let target be document's indicated part. let Some(target) = indicated_part.downcast::<Element>() else { // Step 3.1. Assert: document's indicated part is an element. unreachable!("Indicated part should always be an element"); }; // Step 3.3. Set document's target element to target. self.set_target_element(Some(target)); // Step 3.4. Run the ancestor revealing algorithm on target. // TODO // Step 3.5. Scroll target into view, with behavior set to "auto", block set to "start", and inline set to "nearest". [CSSOMVIEW] target.scroll_into_view_with_options( cx, ScrollBehavior::Auto, ScrollAxisState::new_always_scroll_position(ScrollLogicalPosition::Start), ScrollAxisState::new_always_scroll_position(ScrollLogicalPosition::Nearest), None, None, ); // Step 3.6. Run the focusing steps for target, with the Document's viewport as the fallback // target. indicated_part.run_the_focusing_steps(cx, Some(FocusableArea::Viewport)); // Step 3.7. Move the sequential focus navigation starting point to target. self.focus_handler() .set_sequential_focus_navigation_starting_point(target.upcast()); } fn get_anchor_by_name(&self, cx: &mut JSContext, name: &str) -> Option<DomRoot<Element>> { let document_element = self.GetDocumentElement()?; self.name_map .get_all(cx.no_gc(), document_element.upcast(), &Atom::from(name)) .iter() .find(|element| element.is::<HTMLAnchorElement>()) .map(|element| DomRoot::from_ref(&**element)) } // https://html.spec.whatwg.org/multipage/#current-document-readiness pub(crate) fn set_ready_state(&self, cx: &mut JSContext, state: DocumentReadyState) { match state { DocumentReadyState::Loading => { if self.window().is_top_level() { self.send_to_embedder(EmbedderMsg::NotifyLoadStatusChanged( self.webview_id(), LoadStatus::Started, )); self.send_to_embedder(EmbedderMsg::Status(self.webview_id(), None)); update_with_current_instant(&self.navigation_timing.dom_loading); } }, DocumentReadyState::Complete => { if self.window().is_top_level() { self.send_to_embedder(EmbedderMsg::NotifyLoadStatusChanged( self.webview_id(), LoadStatus::Complete, )); } update_with_current_instant(&self.navigation_timing.dom_complete); }, DocumentReadyState::Interactive => { update_with_current_instant(&self.navigation_timing.dom_interactive) }, }; self.ready_state.set(state); self.upcast::<EventTarget>() .fire_event(cx, atom!("readystatechange")); } /// Return whether scripting is enabled or not /// <https://html.spec.whatwg.org/multipage/#concept-n-script> pub(crate) fn scripting_enabled(&self) -> bool { // Scripting is enabled for a node node if node's node document's browsing context is non-null, // and scripting is enabled for node's relevant settings object. self.has_browsing_context() && // Either settings's global object is not a Window object, // or settings's global object's associated Document's active sandboxing flag // set does not have its sandboxed scripts browsing context flag set. !self.has_active_sandboxing_flag( SandboxingFlagSet::SANDBOXED_SCRIPTS_BROWSING_CONTEXT_FLAG, ) } /// Handles any updates when the document's title has changed. pub(crate) fn title_changed(&self) { if self.browsing_context().is_some() { self.send_title_to_embedder(); let title = String::from(self.Title()); self.window .send_to_constellation(ScriptToConstellationMessage::TitleChanged( self.window.pipeline_id(), title.clone(), )); if let Some(chan) = self.window.as_global_scope().devtools_chan() { let _ = chan.send(ScriptToDevtoolsControlMsg::TitleChanged( self.window.pipeline_id(), title, )); } } } /// Determine the title of the [`Document`] according to the specification at: /// <https://html.spec.whatwg.org/multipage/#document.title>. The difference /// here is that when the title isn't specified `None` is returned. fn title(&self) -> Option<DOMString> { let title = self.GetDocumentElement().and_then(|root| { if root.namespace() == &ns!(svg) && root.local_name() == &local_name!("svg") { // Step 1. root.upcast::<Node>() .child_elements() .find(|node| { node.namespace() == &ns!(svg) && node.local_name() == &local_name!("title") }) .map(DomRoot::upcast::<Node>) } else { // Step 2. root.upcast::<Node>() .traverse_preorder(ShadowIncluding::No) .find(|node| node.is::<HTMLTitleElement>()) } }); title.map(|title| { // Steps 3-4. let value = title.child_text_content(); DOMString::from(str_join(value.str().split_html_space_characters(), " ")) }) } /// Sends this document's title to the constellation. pub(crate) fn send_title_to_embedder(&self) { let window = self.window(); if window.is_top_level() { let title = self.title().map(String::from); self.send_to_embedder(EmbedderMsg::ChangePageTitle(self.webview_id(), title)); } } pub(crate) fn send_to_embedder(&self, msg: EmbedderMsg) { let window = self.window(); window.send_to_embedder(msg); } pub(crate) fn dirty_all_nodes(&self, no_gc: &NoGC) { let root = match self.GetDocumentElement() { Some(root) => root, None => return, }; for node in root .upcast::<Node>() .traverse_preorder_non_rooting(no_gc, ShadowIncluding::Yes) { node.dirty(no_gc, NodeDamage::Other) } } /// <https://drafts.csswg.org/cssom-view/#document-run-the-scroll-steps> pub(crate) fn run_the_scroll_steps(&self, cx: &mut JSContext) { // Step 1: For each scrolling box `box` that was scrolled: // // Note: Since scrolling is currently synchronous (no scroll animations / // smooth scrolling), we consider any box event target that had a scroll // event to be a box that scrolled. Once scrolling is asynchronous this // should reflect scrolling targets which have finished their scroll // animation. let boxes_that_were_scrolled: Vec<_> = self .pending_scroll_events .borrow() .iter() .filter_map(|pending_event| { if &*pending_event.event == "scroll" { Some(pending_event.target.as_rooted()) } else { None } }) .collect(); for target in boxes_that_were_scrolled.into_iter() { // Step 1.1: If box belongs to a viewport, let doc be the viewport’s associated // Document and target be the viewport. If box belongs to a VisualViewport, // let doc be the VisualViewport’s associated document and target be the // VisualViewport. Otherwise, box belongs to an element and let doc be the // element’s node document and target be the element. let Some(element) = target.downcast::<Element>() else { continue; }; let document = element.owner_document(); // Step 1.2: If box belongs to a snap container, snapcontainer, run the // update scrollsnapchange targets steps for snapcontainer. // TODO: Implement this. // Step 1.3: If (target, "scrollend") is already in doc’s pending scroll // events, abort these steps. let mut pending_scroll_events = document.pending_scroll_events.borrow_mut(); let event = "scrollend".into(); if pending_scroll_events .iter() .any(|existing| existing.equivalent(&target, &event)) { continue; } // > Step 1.4: Append (target, "scrollend") to doc’s pending scroll events. pending_scroll_events.push(PendingScrollEvent { target: target.as_traced(), event: "scrollend".into(), }); } // Step 2: For each item (target, type) in doc’s pending scroll events, in // the order they were added to the list, run these substeps: rooted_vec!(let pending_scroll_events <- self.pending_scroll_events.take().into_iter()); for pending_event in pending_scroll_events.iter() { // Step 2.1: If target is a Document, and type is "scroll" or "scrollend", // fire an event named type that bubbles at target. let event = pending_event.event.clone(); if pending_event.target.is::<Document>() { pending_event.target.fire_bubbling_event(cx, event); } // Step 2.2: Otherwise, if type is "scrollsnapchange", then: // .... // TODO: Implement this. // Step 2.3: Otherwise, if type is "scrollsnapchanging", then: // ... // TODO: Implement this. // // Step 2.4: Otherwise, fire an event named type at target. else { pending_event.target.fire_event(cx, event); } } // Step 3. Empty doc’s pending scroll events. // Note: This is done above. } /// <https://drafts.csswg.org/cssom-view/#scrolling-events> /// /// > Whenever a viewport gets scrolled (whether in response to user interaction or /// > by an API), the user agent must run these steps: pub(crate) fn handle_viewport_scroll_event(&self) { // Step 1: Let doc be the viewport’s associated Document. // // Note: This is self. // >> Step 2: If doc is a snap container, run the steps to update scrollsnapchanging targets // > for doc with doc’s eventual snap target in the block axis as newBlockTarget and // > doc’s eventual snap target in the inline axis as newInlineTarget. // // TODO(#7673): Implement scroll snapping // Steps 3 and 4 are shared with other scroll targets. self.finish_handle_scroll_event(self.upcast()); } /// <https://drafts.csswg.org/cssom-view/#scrolling-events> /// /// These are the shared steps 3 and 4 from all scroll targets listed in the /// first section of the specification. pub(crate) fn finish_handle_scroll_event(&self, event_target: &EventTarget) { // Step 3. // > If the element is already in doc’s pending scroll event targets, abort these steps. let event = "scroll".into(); if self .pending_scroll_events .borrow() .iter() .any(|existing| existing.equivalent(event_target, &event)) { return; } // Step 4. // > Append the element to doc’s pending scroll event targets. self.pending_scroll_events .borrow_mut() .push(PendingScrollEvent { target: Dom::from_ref(event_target), event: "scroll".into(), }); } // https://dom.spec.whatwg.org/#converting-nodes-into-a-node pub(crate) fn node_from_nodes_and_strings( &self, cx: &mut JSContext, mut nodes: Vec<NodeOrString>, ) -> Fallible<DomRoot<Node>> { if nodes.len() == 1 { Ok(match nodes.pop().unwrap() { NodeOrString::Node(node) => node, NodeOrString::String(string) => DomRoot::upcast(self.CreateTextNode(cx, string)), }) } else { let fragment = DomRoot::upcast::<Node>(self.CreateDocumentFragment(cx)); for node in nodes { match node { NodeOrString::Node(node) => { fragment.AppendChild(cx, &node)?; }, NodeOrString::String(string) => { let node = DomRoot::upcast::<Node>(self.CreateTextNode(cx, string)); // No try!() here because appending a text node // should not fail. fragment.AppendChild(cx, &node).unwrap(); }, } } Ok(fragment) } } pub(crate) fn get_body_attribute(&self, local_name: &LocalName) -> DOMString { match self.GetBody() { Some(ref body) if body.is_body_element() => { body.upcast::<Element>().get_string_attribute(local_name) }, _ => DOMString::new(), } } pub(crate) fn set_body_attribute( &self, cx: &mut JSContext, local_name: &LocalName, value: DOMString, ) { if let Some(ref body) = self.GetBody().filter(|elem| elem.is_body_element()) { let body = body.upcast::<Element>(); let value = body.parse_attribute(&ns!(), local_name, value); body.set_attribute(cx, local_name, value); } } pub(crate) fn set_current_script(&self, script: Option<&HTMLScriptElement>) { self.current_script.set(script); } /// <https://html.spec.whatwg.org/multipage/#has-a-style-sheet-that-is-blocking-scripts> pub(crate) fn has_a_stylesheet_that_is_blocking_scripts(&self) -> bool { !self.script_blocking_stylesheet_set.borrow().is_empty() } pub(crate) fn add_script_blocking_stylesheet(&self, id: StylesheetContextId) { self.script_blocking_stylesheet_set.borrow_mut().insert(id); } pub(crate) fn remove_script_blocking_stylesheet(&self, id: StylesheetContextId) { self.script_blocking_stylesheet_set .borrow_mut() .shift_remove(&id); } pub(crate) fn render_blocking_element_count(&self) -> u32 { self.render_blocking_element_count.get() } /// <https://html.spec.whatwg.org/multipage/#block-rendering> pub(crate) fn increment_render_blocking_element_count(&self) { // Step 1. Let document be el's node document. // // That's self // Step 2. If document allows adding render-blocking elements, // then append el to document's render-blocking element set. assert!(self.allows_adding_render_blocking_elements()); let count_cell = &self.render_blocking_element_count; count_cell.set(count_cell.get() + 1); } /// <https://html.spec.whatwg.org/multipage/#unblock-rendering> pub(crate) fn decrement_render_blocking_element_count(&self) { // Step 1. Let document be el's node document. // // That's self // Step 2. Remove el from document's render-blocking element set. let count_cell = &self.render_blocking_element_count; assert!(count_cell.get() > 0); count_cell.set(count_cell.get() - 1); } /// <https://html.spec.whatwg.org/multipage/#allows-adding-render-blocking-elements> pub(crate) fn allows_adding_render_blocking_elements(&self) -> bool { // > A Document document allows adding render-blocking elements // > if document's content type is "text/html" and the body element of document is null. self.is_html_document && self.GetBody().is_none() } /// <https://html.spec.whatwg.org/multipage/#render-blocked> pub(crate) fn is_render_blocked(&self) -> bool { // > A Document document is render-blocked if both of the following are true: // > document's render-blocking element set is non-empty, // > or document allows adding render-blocking elements. self.render_blocking_element_count() > 0 // TODO: add `allows_adding_render_blocking_elements` which currently breaks for empty iframes // > The current high resolution time given document's relevant global object // has not exceeded an implementation-defined timeout value. // TODO } pub(crate) fn invalidate_stylesheets(&self, no_gc: &NoGC) { self.stylesheets.borrow_mut().force_dirty(OriginSet::all()); // Mark the document element dirty so a reflow will be performed. // // FIXME(emilio): Use the DocumentStylesheetSet invalidation stuff. if let Some(element) = self.GetDocumentElement() { element.upcast::<Node>().dirty(no_gc, NodeDamage::Style); } } /// Whether or not this `Document` has any active requestAnimationFrame callbacks /// registered. pub(crate) fn has_active_request_animation_frame_callbacks(&self) -> bool { !self.animation_frame_list.borrow().is_empty() } /// <https://html.spec.whatwg.org/multipage/#dom-window-requestanimationframe> pub(crate) fn request_animation_frame(&self, callback: AnimationFrameCallback) -> u32 { let ident = self.animation_frame_ident.get() + 1; self.animation_frame_ident.set(ident); let had_animation_frame_callbacks; { let mut animation_frame_list = self.animation_frame_list.borrow_mut(); had_animation_frame_callbacks = !animation_frame_list.is_empty(); animation_frame_list.push_back((ident, Some(callback))); } // No need to send a `ChangeRunningAnimationsState` if we're running animation callbacks: // we're guaranteed to already be in the "animation callbacks present" state. // // This reduces CPU usage by avoiding needless thread wakeups in the common case of // repeated rAF. if !self.running_animation_callbacks.get() && !had_animation_frame_callbacks { self.window().send_to_constellation( ScriptToConstellationMessage::ChangeRunningAnimationsState( AnimationState::AnimationCallbacksPresent, ), ); } ident } /// <https://html.spec.whatwg.org/multipage/#dom-window-cancelanimationframe> pub(crate) fn cancel_animation_frame(&self, ident: u32) { let mut list = self.animation_frame_list.borrow_mut(); if let Some(pair) = list.iter_mut().find(|pair| pair.0 == ident) { pair.1 = None; } } /// <https://html.spec.whatwg.org/multipage/#run-the-animation-frame-callbacks> pub(crate) fn run_the_animation_frame_callbacks(&self, cx: &mut CurrentRealm) { self.running_animation_callbacks.set(true); let timing = self.global().performance(cx).Now(); let num_callbacks = self.animation_frame_list.borrow().len(); for _ in 0..num_callbacks { let (_, maybe_callback) = self.animation_frame_list.borrow_mut().pop_front().unwrap(); if let Some(callback) = maybe_callback { callback.call(cx, self, *timing); } } self.running_animation_callbacks.set(false); if self.animation_frame_list.borrow().is_empty() { self.window().send_to_constellation( ScriptToConstellationMessage::ChangeRunningAnimationsState( AnimationState::AnimationCallbacksAbsent, ), ); } } pub(crate) fn policy_container(&self) -> Ref<'_, PolicyContainer> { self.policy_container.borrow() } pub(crate) fn set_policy_container(&self, policy_container: PolicyContainer) { *self.policy_container.borrow_mut() = policy_container; } pub(crate) fn set_csp_list(&self, csp_list: Option<CspList>) { self.policy_container.borrow_mut().set_csp_list(csp_list); } /// <https://www.w3.org/TR/CSP/#enforced> pub(crate) fn enforce_csp_policy(&self, policy: CspPolicy) { // > A policy is enforced or monitored for a global object by inserting it into the global object’s CSP list. let mut csp_list = self.get_csp_list().clone().unwrap_or(CspList(vec![])); csp_list.push(policy); self.policy_container .borrow_mut() .set_csp_list(Some(csp_list)); } pub(crate) fn get_csp_list(&self) -> Ref<'_, Option<CspList>> { Ref::map(self.policy_container.borrow(), |policy_container| { &policy_container.csp_list }) } pub(crate) fn preloaded_resources(&self) -> std::cell::Ref<'_, PreloadedResources> { self.preloaded_resources.borrow() } pub(crate) fn insert_preloaded_resource(&self, key: PreloadKey, preload_id: PreloadId) { self.preloaded_resources .borrow_mut() .insert(key, preload_id); } pub(crate) fn fetch<Listener: FetchResponseListener>( &self, load: LoadType, request: RequestBuilder, listener: Listener, ) { let callback = NetworkListener { context: std::sync::Arc::new(Mutex::new(Some(listener))), task_source: self .owner_global() .task_manager() .networking_task_source() .into(), } .into_callback(); self.loader_mut() .fetch_async_with_callback(load, request, callback); } pub(crate) fn fetch_background<Listener: FetchResponseListener>( &self, request: RequestBuilder, listener: Listener, ) { let callback = NetworkListener { context: std::sync::Arc::new(Mutex::new(Some(listener))), task_source: self .owner_global() .task_manager() .networking_task_source() .into(), } .into_callback(); self.loader_mut().fetch_async_background(request, callback); } /// <https://fetch.spec.whatwg.org/#deferred-fetch-control-document> fn deferred_fetch_control_document(&self) -> DomRoot<Document> { match self.window().window_proxy().frame_element() { // Step 1. If document’ node navigable’s container document is null // or a document whose origin is not same origin with document, then return document; None => DomRoot::from_ref(self), // otherwise, return the deferred-fetch control document given document’s node navigable’s container document. Some(container) => container.owner_document().deferred_fetch_control_document(), } } /// <https://fetch.spec.whatwg.org/#available-deferred-fetch-quota> pub(crate) fn available_deferred_fetch_quota(&self, origin: ImmutableOrigin) -> isize { // Step 1. Let controlDocument be document’s deferred-fetch control document. let control_document = self.deferred_fetch_control_document(); // Step 2. Let navigable be controlDocument’s node navigable. let navigable = control_document.window(); // Step 3. Let isTopLevel be true if controlDocument’s node navigable // is a top-level traversable; otherwise false. let is_top_level = navigable.is_top_level(); // Step 4. Let deferredFetchAllowed be true if controlDocument is allowed // to use the policy-controlled feature "deferred-fetch"; otherwise false. // TODO let deferred_fetch_allowed = true; // Step 5. Let deferredFetchMinimalAllowed be true if controlDocument // is allowed to use the policy-controlled feature "deferred-fetch-minimal"; otherwise false. // TODO let deferred_fetch_minimal_allowed = true; // Step 6. Let quota be the result of the first matching statement: let mut quota = match is_top_level { // isTopLevel is true and deferredFetchAllowed is false true if !deferred_fetch_allowed => 0, // isTopLevel is true and deferredFetchMinimalAllowed is false true if !deferred_fetch_minimal_allowed => 640 * 1024, // isTopLevel is true true => 512 * 1024, // deferredFetchAllowed is true, and navigable’s navigable container’s // reserved deferred-fetch quota is normal quota // TODO _ if deferred_fetch_allowed => 0, // deferredFetchMinimalAllowed is true, and navigable’s navigable container’s // reserved deferred-fetch quota is minimal quota // TODO _ if deferred_fetch_minimal_allowed => 8 * 1024, // Otherwise _ => 0, } as isize; // Step 7. Let quotaForRequestOrigin be 64 kibibytes. let mut quota_for_request_origin = 64 * 1024_isize; // Step 8. For each navigable in controlDocument’s node navigable’s // inclusive descendant navigables whose active document’s deferred-fetch control document is controlDocument: // TODO // Step 8.1. For each container in navigable’s active document’s shadow-including inclusive descendants // which is a navigable container, decrement quota by container’s reserved deferred-fetch quota. // TODO // Step 8.2. For each deferred fetch record deferredRecord of navigable’s active document’s // relevant settings object’s fetch group’s deferred fetch records: for deferred_fetch in navigable.as_global_scope().deferred_fetches() { // Step 8.2.1. If deferredRecord’s invoke state is not "pending", then continue. if deferred_fetch.invoke_state.get() != DeferredFetchRecordInvokeState::Pending { continue; } // Step 8.2.2. Let requestLength be the total request length of deferredRecord’s request. let request_length = deferred_fetch.request.total_request_length(); // Step 8.2.3. Decrement quota by requestLength. quota -= request_length as isize; // Step 8.2.4. If deferredRecord’s request’s URL’s origin is same origin with origin, // then decrement quotaForRequestOrigin by requestLength. if deferred_fetch.request.url().origin() == origin { quota_for_request_origin -= request_length as isize; } } // Step 9. If quota is equal or less than 0, then return 0. if quota <= 0 { return 0; } // Step 10. If quota is less than quotaForRequestOrigin, then return quota. if quota < quota_for_request_origin { return quota; } // Step 11. Return quotaForRequestOrigin. quota_for_request_origin } /// <https://html.spec.whatwg.org/multipage/#update-document-for-history-step-application> pub(crate) fn update_document_for_history_step_application( &self, old_url: &ServoUrl, new_url: &ServoUrl, ) { // Step 6. If documentsEntryChanged is true, then: // // It is right now since we already have a document and a new_url // Step 6.1. Let oldURL be document's latest entry's URL. // Passed in as argument // Step 6.2. Set document's latest entry to entry. // TODO // Step 6.3. Restore the history object state given document and entry. // TODO // Step 6.4. If documentIsNew is false, then: // TODO // Step 6.4.1. Assert: navigationType is not null. // TODO // Step 6.4.2. Update the navigation API entries for a same-document navigation given navigation, entry, and navigationType. // TODO // Step 6.4.3. Fire an event named popstate at document's relevant global object, using PopStateEvent, // with the state attribute initialized to document's history object's state and hasUAVisualTransition // initialized to true if a visual transition, to display a cached rendered state of the latest entry, was done by the user agent. // TODO // Step 6.4.4. Restore persisted state given entry. // TODO // Step 6.4.5. If oldURL's fragment is not equal to entry's URL's fragment, // then queue a global task on the DOM manipulation task source given document's relevant global object // to fire an event named hashchange at document's relevant global object, using HashChangeEvent, // with the oldURL attribute initialized to the serialization of oldURL // and the newURL attribute initialized to the serialization of entry's URL. if old_url.as_url()[Position::BeforeFragment..] != new_url.as_url()[Position::BeforeFragment..] { let window = Trusted::new(self.owner_window().deref()); let old_url = old_url.to_string(); let new_url = new_url.to_string(); self.owner_global() .task_manager() .dom_manipulation_task_source() .queue(task!(hashchange_event: move |cx| { let window = window.root(); HashChangeEvent::new( cx, &window, atom!("hashchange"), false, false, old_url, new_url, ) .upcast::<Event>() .fire(cx, window.upcast()); })); } } pub(crate) fn finish_load_for_dropped_blocker(&self, load: LoadType) { let this = Trusted::new(self); self.owner_global() .task_manager() .dom_manipulation_task_source() .queue(task!(check_finished_load: move |cx| { this.root().finish_load(load, cx); })); } /// Step 8 of <https://html.spec.whatwg.org/multipage/#the-end> /// <https://html.spec.whatwg.org/multipage/#delay-the-load-event> pub(crate) fn finish_load(&self, load: LoadType, cx: &mut JSContext) { // This does not delay the load event anymore. debug!("Document got finish_load: {:?}", load); self.loader.borrow_mut().finish_load(&load); match load { LoadType::Stylesheet(_) => { // A stylesheet finishing to load may unblock any pending // parsing-blocking script or deferred script. self.process_pending_parsing_blocking_script(cx); // Step 3. self.process_deferred_scripts(cx); }, LoadType::PageSource(_) => { // We finished loading the page, so if the `Window` is still waiting for // the first layout, allow it. if self.has_browsing_context && self.is_fully_active() { self.window().allow_layout_if_necessary(cx); } // Deferred scripts have to wait for page to finish loading, // this is the first opportunity to process them. // Step 3. self.process_deferred_scripts(cx); }, _ => {}, } // START TODO(43149): Remove when document replacement is implemented // Step 4 is in another castle, namely at the end of // process_deferred_scripts. // Step 5 can be found in asap_script_loaded and // asap_in_order_script_loaded. let loader = self.loader.borrow(); // Servo measures when the top-level content (not iframes) is loaded. if self .navigation_timing .top_level_dom_complete .get() .is_none() && loader.is_only_blocked_by_iframes() { update_with_current_instant(&self.navigation_timing.top_level_dom_complete); } if loader.is_blocked() || loader.events_inhibited() { // Step 6. return; } ScriptThread::mark_document_with_no_blocked_loads(self); // END TODO(43149): Remove when document replacement is implemented // Step 8. Spin the event loop until there is nothing that delays the load event in the Document. let document = Trusted::new(self); self.owner_global() .task_manager() .dom_manipulation_task_source() .queue(task!(wait_for_load_blockers: move |cx| { document.root().wait_until_load_blockers_have_resolved(cx); })); } /// <https://html.spec.whatwg.org/multipage/#checking-if-unloading-is-canceled> pub(crate) fn check_if_unloading_is_cancelled( &self, cx: &mut JSContext, recursive_flag: bool, ) -> bool { // TODO: Step 1, increase the event loop's termination nesting level by 1. // Step 2 self.incr_ignore_opens_during_unload_counter(); // Step 3-5. let beforeunload_event = BeforeUnloadEvent::new( cx, &self.window, atom!("beforeunload"), EventBubbles::Bubbles, EventCancelable::Cancelable, ); let event = beforeunload_event.upcast::<Event>(); event.set_trusted(true); let event_target = self.window.upcast::<EventTarget>(); let has_listeners = event_target.has_listeners_for(&atom!("beforeunload")); self.window.dispatch_event_with_target_override(cx, event); // TODO: Step 6, decrease the event loop's termination nesting level by 1. // Step 7 if has_listeners { self.salvageable.set(false); } let mut can_unload = true; // TODO: Step 8, also check sandboxing modals flag. let default_prevented = event.DefaultPrevented(); let return_value_not_empty = !event .downcast::<BeforeUnloadEvent>() .unwrap() .ReturnValue() .is_empty(); if default_prevented || return_value_not_empty { let (chan, port) = generic_channel::channel().expect("Failed to create IPC channel!"); let msg = EmbedderMsg::AllowUnload(self.webview_id(), chan); self.send_to_embedder(msg); can_unload = port.recv().unwrap() == AllowOrDeny::Allow; } // Step 9 if !recursive_flag { // `check_if_unloading_is_cancelled` might cause futher modifications to the DOM so collecting here prevents // a double borrow if the `IFrameCollection` needs to be validated again. let iframes: Vec<_> = self.iframes().iter().collect(); for iframe in &iframes { // TODO: handle the case of cross origin iframes. let document = iframe.owner_document(); can_unload = document.check_if_unloading_is_cancelled(cx, true); if !document.salvageable() { self.salvageable.set(false); } if !can_unload { break; } } } // Step 10 self.decr_ignore_opens_during_unload_counter(); can_unload } // https://html.spec.whatwg.org/multipage/#unload-a-document pub(crate) fn unload(&self, cx: &mut JSContext, recursive_flag: bool) { // TODO: Step 1, increase the event loop's termination nesting level by 1. // Step 2 self.incr_ignore_opens_during_unload_counter(); // Step 3-6 If oldDocument's page showing is true: if self.page_showing.get() { // Set oldDocument's page showing to false. self.page_showing.set(false); // Fire a page transition event named pagehide at oldDocument's relevant global object with oldDocument's // salvageable state. let event = PageTransitionEvent::new( cx, &self.window, atom!("pagehide"), false, // bubbles false, // cancelable self.salvageable.get(), // persisted ); let event = event.upcast::<Event>(); event.set_trusted(true); self.window.dispatch_event_with_target_override(cx, event); // Step 6 Update the visibility state of oldDocument to "hidden". self.update_visibility_state(cx, DocumentVisibilityState::Hidden); } // Step 7 if !self.fired_unload.get() { let event = Event::new( cx, self.window.upcast(), atom!("unload"), EventBubbles::Bubbles, EventCancelable::Cancelable, ); event.set_trusted(true); let event_target = self.window.upcast::<EventTarget>(); let has_listeners = event_target.has_listeners_for(&atom!("unload")); self.window.dispatch_event_with_target_override(cx, &event); self.fired_unload.set(true); // Step 9 if has_listeners { self.salvageable.set(false); } } // TODO: Step 8, decrease the event loop's termination nesting level by 1. // Step 13 if !recursive_flag { // `unload` might cause futher modifications to the DOM so collecting here prevents // a double borrow if the `IFrameCollection` needs to be validated again. let iframes: Vec<_> = self.iframes().iter().collect(); for iframe in &iframes { // TODO: handle the case of cross origin iframes. let document = iframe.owner_document(); document.unload(cx, true); if !document.salvageable() { self.salvageable.set(false); } } } // Step 18. Run any unloading document cleanup steps for oldDocument that are defined by this specification and other applicable specifications. self.unloading_cleanup_steps(); // https://w3c.github.io/FileAPI/#lifeTime self.window.as_global_scope().clean_up_all_file_resources(); // Step 15, End self.decr_ignore_opens_during_unload_counter(); // Step 20. If oldDocument's salvageable state is false, then destroy oldDocument. // TODO } /// <https://html.spec.whatwg.org/multipage/#completely-finish-loading> fn completely_finish_loading(&self) { // Step 1. Assert: document's browsing context is non-null. // TODO: Adding this assert fails a lot of tests // Step 2. Set document's completely loaded time to the current time. self.completely_loaded.set(true); // Step 3. Let container be document's node navigable's container. // TODO // Step 4. If container is an iframe element, then queue an element task // on the DOM manipulation task source given container to run the iframe load event steps given container. // // Note: this will also result in the "iframe-load-event-steps" being run. // https://html.spec.whatwg.org/multipage/#iframe-load-event-steps self.notify_constellation_load(); // Step 5. Otherwise, if container is non-null, then queue an element task on the DOM manipulation task source // given container to fire an event named load at container. // TODO // Step 13 of https://html.spec.whatwg.org/multipage/#shared-declarative-refresh-steps // // At least time seconds have elapsed since document's completely loaded time, // adjusted to take into account user or user agent preferences. if let Some(DeclarativeRefresh::PendingLoad { url, time, from_meta_element, }) = &*self.declarative_refresh.borrow() { self.window.as_global_scope().schedule_callback( OneshotTimerCallback::RefreshRedirectDue(RefreshRedirectDue { url: url.clone(), from_meta_element: *from_meta_element, }), Duration::from_secs(*time), ); } } // https://html.spec.whatwg.org/multipage/#the-end // TODO(43149): Remove when document replacement is implemented pub(crate) fn maybe_queue_document_completion(&self, cx: &mut JSContext) { // https://html.spec.whatwg.org/multipage/#delaying-load-events-mode let is_in_delaying_load_events_mode = match self.window.undiscarded_window_proxy() { Some(window_proxy) => window_proxy.is_delaying_load_events_mode(), None => false, }; // Note: if the document is not fully active, layout will have exited already, // and this method will panic. // The underlying problem might actually be that layout exits while it should be kept alive. // See https://github.com/servo/servo/issues/22507 let not_ready_for_load = self.loader.borrow().is_blocked() || !self.is_fully_active() || is_in_delaying_load_events_mode || // In case we have already aborted this document and receive a // a subsequent message to load the document self.loader.borrow().events_inhibited(); if not_ready_for_load { // Step 6. return; } self.queue_document_completion(cx); } /// Step 9 of <https://html.spec.whatwg.org/multipage/#the-end> fn queue_document_completion(&self, cx: &mut JSContext) { self.loader.borrow_mut().inhibit_events(); // The rest will ever run only once per document. // Step 9. Queue a global task on the DOM manipulation task source given // the Document's relevant global object to run the following steps: debug!("Document loads are complete."); let document = Trusted::new(self); self.owner_global() .task_manager() .dom_manipulation_task_source() .queue(task!(fire_load_event: move |cx| { let document = document.root(); // Step 9.3. Let window be the Document's relevant global object. let window = document.window(); if !window.is_alive() { return; } // Step 9.1. Update the current document readiness to "complete". document.set_ready_state(cx,DocumentReadyState::Complete); // Step 9.2. If the Document object's browsing context is null, then abort these steps. if document.browsing_context().is_none() { return; } // Step 9.4. Set the Document's load timing info's load event start time to the current high resolution time given window. update_with_current_instant(&document.navigation_timing.load_event_start); // Step 9.5. Fire an event named load at window, with legacy target override flag set. let load_event = Event::new( cx, window.upcast(), atom!("load"), EventBubbles::DoesNotBubble, EventCancelable::NotCancelable, ); load_event.set_trusted(true); debug!("About to dispatch load for {:?}", document.url()); window.dispatch_event_with_target_override(cx, &load_event); // Step 9.6. Invoke WebDriver BiDi load complete with the Document's browsing context, // and a new WebDriver BiDi navigation status whose id is the Document object's during-loading navigation ID // for WebDriver BiDi, status is "complete", and url is the Document object's URL. // TODO // Step 9.7. Set the Document object's during-loading navigation ID for WebDriver BiDi to null. // TODO // Step 9.8. Set the Document's load timing info's load event end time to the current high resolution time given window. update_with_current_instant(&document.navigation_timing.load_event_end); // Step 9.9. Assert: Document's page showing is false. // TODO: Adding this assert fails a lot of tests // Step 9.10. Set the Document's page showing to true. document.page_showing.set(true); // Step 9.11. Fire a page transition event named pageshow at window with false. let page_show_event = PageTransitionEvent::new( cx, window, atom!("pageshow"), false, // bubbles false, // cancelable false, // persisted ); let page_show_event = page_show_event.upcast::<Event>(); page_show_event.set_trusted(true); page_show_event.fire(cx, window.upcast()); // Step 9.12. Completely finish loading the Document. document.completely_finish_loading(); // Step 9.13. Queue the navigation timing entry for the Document. // TODO if let Some(fragment) = document.url().fragment() { document.scroll_to_the_fragment(cx, fragment); } })); // Step 9. // TODO: pending application cache download process tasks. // Step 10. // TODO: printing steps. // Step 11. // TODO: ready for post-load tasks. // The dom.webxr.sessionavailable pref allows webxr // content to immediately begin a session without waiting for a user gesture. // TODO: should this only happen on the first document loaded? // https://immersive-web.github.io/webxr/#user-intention // https://github.com/immersive-web/navigation/issues/10 #[cfg(feature = "webxr")] if pref!(dom_webxr_sessionavailable) && self.window.is_top_level() { self.window.Navigator(cx).Xr(cx).dispatch_sessionavailable(); } } pub(crate) fn completely_loaded(&self) -> bool { self.completely_loaded.get() } pub(crate) fn start_the_end_loading_phase(&self) { self.current_the_end_loading_phase .set(TheEndLoadingPhase::ProcessingDeferredScripts); } // https://html.spec.whatwg.org/multipage/#pending-parsing-blocking-script pub(crate) fn set_pending_parsing_blocking_script( &self, script: &HTMLScriptElement, load: Option<ScriptResult>, ) { assert!(!self.has_pending_parsing_blocking_script()); *self.pending_parsing_blocking_script.borrow_mut() = Some(PendingScript::new_with_load(script, load)); } // https://html.spec.whatwg.org/multipage/#pending-parsing-blocking-script pub(crate) fn has_pending_parsing_blocking_script(&self) -> bool { self.pending_parsing_blocking_script.borrow().is_some() } /// <https://html.spec.whatwg.org/multipage/#prepare-a-script> step 22.d. pub(crate) fn pending_parsing_blocking_script_loaded( &self, element: &HTMLScriptElement, result: ScriptResult, cx: &mut JSContext, ) { { let mut blocking_script = self.pending_parsing_blocking_script.borrow_mut(); let entry = blocking_script.as_mut().unwrap(); assert!(&*entry.element == element); entry.loaded(result); } self.process_pending_parsing_blocking_script(cx); } fn process_pending_parsing_blocking_script(&self, cx: &mut JSContext) { if self.has_a_stylesheet_that_is_blocking_scripts() { return; } let pair = self .pending_parsing_blocking_script .borrow_mut() .as_mut() .and_then(PendingScript::take_result); if let Some((element, result)) = pair { *self.pending_parsing_blocking_script.borrow_mut() = None; self.get_current_parser() .unwrap() .resume_with_pending_parsing_blocking_script(cx, &element, result); } } // https://html.spec.whatwg.org/multipage/#set-of-scripts-that-will-execute-as-soon-as-possible pub(crate) fn add_asap_script(&self, script: &HTMLScriptElement) { self.asap_scripts_set .borrow_mut() .push(Dom::from_ref(script)); } /// <https://html.spec.whatwg.org/multipage/#the-end> step 5. /// <https://html.spec.whatwg.org/multipage/#prepare-a-script> step 22.d. pub(crate) fn asap_script_loaded( &self, cx: &mut JSContext, element: &HTMLScriptElement, result: ScriptResult, ) { { let mut scripts = self.asap_scripts_set.borrow_mut(); let idx = scripts .iter() .position(|entry| &**entry == element) .unwrap(); scripts.swap_remove(idx); } element.execute(cx, result); self.wait_until_asap_scripts_have_executed(); } // https://html.spec.whatwg.org/multipage/#list-of-scripts-that-will-execute-in-order-as-soon-as-possible pub(crate) fn push_asap_in_order_script(&self, script: &HTMLScriptElement) { self.asap_in_order_scripts_list.push(script); } /// <https://html.spec.whatwg.org/multipage/#the-end> step 5. /// <https://html.spec.whatwg.org/multipage/#prepare-a-script> step> 22.c. pub(crate) fn asap_in_order_script_loaded( &self, cx: &mut JSContext, element: &HTMLScriptElement, result: ScriptResult, ) { self.asap_in_order_scripts_list.loaded(element, result); while let Some((element, result)) = self .asap_in_order_scripts_list .take_next_ready_to_be_executed() { element.execute(cx, result); } self.wait_until_asap_scripts_have_executed(); } /// <https://html.spec.whatwg.org/multipage/#list-of-scripts-that-will-execute-when-the-document-has-finished-parsing> pub(crate) fn add_deferred_script(&self, script: &HTMLScriptElement) { self.deferred_scripts.push(script); } /// <https://html.spec.whatwg.org/multipage/#the-end> step 3. /// <https://html.spec.whatwg.org/multipage/#prepare-a-script> step 22.d. pub(crate) fn deferred_script_loaded( &self, cx: &mut JSContext, element: &HTMLScriptElement, result: ScriptResult, ) { self.deferred_scripts.loaded(element, result); self.process_deferred_scripts(cx); } /// Step 5 of <https://html.spec.whatwg.org/multipage/#the-end> fn process_deferred_scripts(&self, cx: &mut JSContext) { if self.current_the_end_loading_phase.get() != TheEndLoadingPhase::ProcessingDeferredScripts { return; } // Step 5.1. Spin the event loop until the first script in the list of scripts that will execute when the // document has finished parsing has its ready to be parser-executed set to true and the parser's Document // has no style sheet that is blocking scripts. loop { if self.has_a_stylesheet_that_is_blocking_scripts() { return; } // Step 5.3. Remove the first script element from the list of scripts that will execute when the // document has finished parsing (i.e. shift out the first entry in the list). if let Some((element, result)) = self.deferred_scripts.take_next_ready_to_be_executed() { // Step 5.2. Execute the script element given by the first script in the list of scripts that will execute when the document has finished parsing. element.execute(cx, result); } else { break; } } // Step 5. While the list of scripts that will execute when the document has finished parsing is not empty: if self.deferred_scripts.is_empty() { self.current_the_end_loading_phase .set(TheEndLoadingPhase::ProcessingAsSoonAsPossibleScripts); // TODO(43149): Use `dispatch_dom_content_loaded` when document replacement is implemented self.maybe_dispatch_dom_content_loaded(); } } /// Step 6. of <https://html.spec.whatwg.org/multipage/#the-end> pub(crate) fn maybe_dispatch_dom_content_loaded(&self) { // TODO(43149): Remove when document replacement is implemented if self.domcontentloaded_dispatched.get() { return; } self.domcontentloaded_dispatched.set(true); self.dispatch_dom_content_loaded(); } /// Step 6 of <https://html.spec.whatwg.org/multipage/#the-end> fn dispatch_dom_content_loaded(&self) { assert_ne!( self.ReadyState(), DocumentReadyState::Complete, "Complete before DOMContentLoaded?" ); // Step 6. Queue a global task on the DOM manipulation task source given the Document's // relevant global object to run the following substeps: let document = Trusted::new(self); self.owner_global() .task_manager() .dom_manipulation_task_source() .queue(task!(fire_dom_content_loaded_event: move |cx| { // Step 6.1. Set the Document's load timing info's DOM content loaded event start time to // the current high resolution time given the Document's relevant global object. let document = document.root(); update_with_current_instant(&document.navigation_timing.dom_content_loaded_event_start); // Step 6.2. Fire an event named DOMContentLoaded at the Document object, with its bubbles attribute initialized to true. document.upcast::<EventTarget>().fire_bubbling_event(cx, atom!("DOMContentLoaded")); // Step 6.3. Set the Document's load timing info's DOM content loaded event end time to // the current high resolution time given the Document's relevant global object. update_with_current_instant(&document.navigation_timing.dom_content_loaded_event_end); // Step 6.4. Enable the client message queue of the ServiceWorkerContainer object // whose associated service worker client is the Document object's relevant settings object. // TODO // Step 6.5. Invoke WebDriver BiDi DOM content loaded with the Document's browsing context, // and a new WebDriver BiDi navigation status whose id is the Document object's during-loading // navigation ID for WebDriver BiDi, status is "pending", and url is the Document object's URL. // TODO })); // html parsing has finished - set dom content loaded self.interactive_time .borrow() .maybe_set_tti(InteractiveFlag::DOMContentLoaded); self.wait_until_asap_scripts_have_executed(); } fn has_finished_all_asap_scripts(&self) -> bool { self.asap_scripts_set.borrow().is_empty() && self.asap_in_order_scripts_list.is_empty() } /// Step 7 of <https://html.spec.whatwg.org/multipage/#the-end> fn wait_until_asap_scripts_have_executed(&self) { if self.current_the_end_loading_phase.get() != TheEndLoadingPhase::ProcessingAsSoonAsPossibleScripts { return; } // Step 7. Spin the event loop until the set of scripts that will execute as soon as possible // and the list of scripts that will execute in order as soon as possible are empty. if self.has_finished_all_asap_scripts() { let document = Trusted::new(self); self.owner_global() .task_manager() .dom_manipulation_task_source() .queue(task!(transition_away_from_asap_scripts: move |cx| { let document = document.root(); // Ensure that if this task is fired multiple times, we only progress the // end of loading phase once. if document.current_the_end_loading_phase.get() != TheEndLoadingPhase::ProcessingAsSoonAsPossibleScripts { return; } // Check again if we still fulfil the goal if !document.has_finished_all_asap_scripts() { return; } document.current_the_end_loading_phase .set(TheEndLoadingPhase::WaitingForLoadEventBlockers); document.wait_until_load_blockers_have_resolved(cx); })); } } /// Step 8 of <https://html.spec.whatwg.org/multipage/#the-end> pub(crate) fn wait_until_load_blockers_have_resolved(&self, _cx: &mut JSContext) { if self.current_the_end_loading_phase.get() != TheEndLoadingPhase::WaitingForLoadEventBlockers { return; } // Step 8. Spin the event loop until there is nothing that delays the load event in the Document. { let loader = self.loader.borrow(); // Servo measures when the top-level content (not iframes) is loaded. if self .navigation_timing .top_level_dom_complete .get() .is_none() && loader.is_only_blocked_by_iframes() { update_with_current_instant(&self.navigation_timing.top_level_dom_complete); } let not_ready_for_load = loader.is_blocked() || loader.events_inhibited(); if not_ready_for_load { return; } } self.current_the_end_loading_phase .set(TheEndLoadingPhase::Done); // TODO(43149): Add when document replacement is implemented // self.queue_document_completion(cx); } /// <https://html.spec.whatwg.org/multipage/#destroy-a-document-and-its-descendants> pub(crate) fn destroy_document_and_its_descendants(&self, cx: &mut JSContext) { // Step 1. If document is not fully active, then: if !self.is_fully_active() { // Step 1.1. Let reason be a string from user-agent specific blocking reasons. // If none apply, then let reason be "masked". // TODO // Step 1.2. Make document unsalvageable given document and reason. self.salvageable.set(false); // Step 1.3. If document's node navigable is a top-level traversable, // build not restored reasons for a top-level traversable and its descendants given document's node navigable. // TODO } // TODO(#31973): all of the steps below are implemented synchronously at the moment. // They need to become asynchronous later, at which point the counting of // numberDestroyed becomes relevant. // Step 2. Let childNavigables be document's child navigables. // Step 3. Let numberDestroyed be 0. // Step 4. For each childNavigable of childNavigables, queue a global task on // the navigation and traversal task source given childNavigable's active // window to perform the following steps: // Step 4.1. Let incrementDestroyed be an algorithm step which increments numberDestroyed. // Step 4.2. Destroy a document and its descendants given childNavigable's active document and incrementDestroyed. // Step 5. Wait until numberDestroyed equals childNavigable's size. for exited_iframe in self.iframes().iter() { debug!("Destroying nested iframe document"); exited_iframe.destroy_document_and_its_descendants(cx); } // Step 6. Queue a global task on the navigation and traversal task source // given document's relevant global object to perform the following steps: // TODO // Step 6.1. Destroy document. self.destroy(cx); // Step 6.2. If afterAllDestruction was given, then run it. // TODO } /// <https://html.spec.whatwg.org/multipage/#destroy-a-document> pub(crate) fn destroy(&self, cx: &mut JSContext) { let exited_window = self.window(); // Step 2. Abort document. self.abort(cx); // Step 3. Set document's salvageable state to false. self.salvageable.set(false); // Step 4. Let ports be the list of MessagePorts whose relevant // global object's associated Document is document. // TODO // Step 5. For each port in ports, disentangle port. // TODO // Step 6. Run any unloading document cleanup steps for document that // are defined by this specification and other applicable specifications. self.unloading_cleanup_steps(); // Step 7. Remove any tasks whose document is document from any task queue // (without running those tasks). exited_window .as_global_scope() .task_manager() .cancel_all_tasks_and_ignore_future_tasks(); // Step 8. Set document's browsing context to null. exited_window.discard_browsing_context(); // Step 9. Set document's node navigable's active session history entry's // document state's document to null. // TODO // Step 10. Remove document from the owner set of each WorkerGlobalScope // object whose set contains document. exited_window .as_global_scope() .disable_owned_worker_animation_frame_providers(); // Step 11. For each workletGlobalScope in document's worklet global scopes, // terminate workletGlobalScope. // TODO } /// <https://fetch.spec.whatwg.org/#concept-fetch-group-terminate> fn terminate_fetch_group(&self) -> bool { let mut load_cancellers = self.loader.borrow_mut().cancel_all_loads(); // Step 1. For each fetch record record of fetchGroup’s fetch records, // if record’s controller is non-null and record’s request’s done flag // is unset and keepalive is false, terminate record’s controller. for canceller in &mut load_cancellers { if !canceller.keep_alive() { canceller.terminate(); } } // Step 2. Process deferred fetches for fetchGroup. self.owner_global().process_deferred_fetches(); !load_cancellers.is_empty() } /// <https://html.spec.whatwg.org/multipage/#active-parser> fn active_parser(&self) -> Option<DomRoot<ServoParser>> { // > A Document is said to have an active parser if it is associated with // > an HTML parser or an XML parser that has not yet been stopped or aborted. self.get_current_parser() .filter(|parser| !(parser.has_stopped() || parser.has_aborted())) } /// <https://html.spec.whatwg.org/multipage/#abort-a-document> pub(crate) fn abort(&self, cx: &mut JSContext) { // We need to inhibit the loader before anything else. self.loader.borrow_mut().inhibit_events(); // Step 1. Assert: this is running as part of a task queued on document's relevant agent's event loop. // TODO // Step 2. Cancel any instances of the fetch algorithm in the context of document, // discarding any tasks queued for them, and discarding any further data received // from the network for them. If this resulted in any instances of the fetch algorithm // being canceled or any queued tasks or any network data getting discarded, // then make document unsalvageable given document and "fetch". self.script_blocking_stylesheet_set.borrow_mut().clear(); *self.pending_parsing_blocking_script.borrow_mut() = None; *self.asap_scripts_set.borrow_mut() = vec![]; self.asap_in_order_scripts_list.clear(); self.deferred_scripts.clear(); let loads_cancelled = self.terminate_fetch_group(); let event_sources_canceled = self.window.as_global_scope().close_event_sources(); if loads_cancelled || event_sources_canceled { // If any loads were canceled. self.salvageable.set(false); }; // Also Step 2. // Note: the spec says to discard any tasks queued for fetch. // This cancels all tasks on the networking task source, which might be too broad. // See https://github.com/whatwg/html/issues/3837 self.owner_global() .task_manager() .cancel_pending_tasks_for_source(TaskSourceName::Networking); // Step 3. If document's during-loading navigation ID for WebDriver BiDi is non-null, then: // TODO // Step 4. If document has an active parser, then: if let Some(parser) = self.active_parser() { // Step 4.1. Set document's active parser was aborted to true. self.active_parser_was_aborted.set(true); // Step 4.2. Abort that parser. parser.abort(cx); // Step 4.3. Make document unsalvageable given document and "parser-aborted". self.salvageable.set(false); } } /// <https://html.spec.whatwg.org/multipage/#abort-a-document-and-its-descendants> pub(crate) fn abort_a_document_and_its_descendants(&self, cx: &mut JSContext) { // Step 1. Assert: this is running as part of a task queued on document's relevant agent's event loop. // TODO // Step 2. Let descendantNavigables be document's descendant navigables. // Step 3. For each descendantNavigable of descendantNavigables, // queue a global task on the navigation and traversal task source given // descendantNavigable's active window to perform the following steps: for iframe in self.iframes().iter() { if let Some(descendant_document) = iframe.GetContentDocument() { let trusted_descendant_document = Trusted::new(&*descendant_document); let document = Trusted::new(self); descendant_document .owner_global() .task_manager() .navigation_and_traversal_task_source() .queue(task!(abort_iframe_document: move |cx| { let descendant_document = trusted_descendant_document.root(); // Step 3.1. Abort descendantNavigable's active document. descendant_document.abort(cx); // Step 3.2. If descendantNavigable's active document's salvageable is false, then set document's salvageable to false. if !descendant_document.salvageable.get() { document.root().salvageable.set(false); } })); } } // Step 4. Abort document. self.abort(cx); } pub(crate) fn notify_constellation_load(&self) { self.window() .send_to_constellation(ScriptToConstellationMessage::LoadComplete); } pub(crate) fn set_current_parser(&self, script: Option<&ServoParser>) { self.current_parser.set(script); } pub(crate) fn get_current_parser(&self) -> Option<DomRoot<ServoParser>> { self.current_parser.get() } pub(crate) fn get_current_parser_line(&self) -> u32 { self.get_current_parser() .map(|parser| parser.get_current_line()) .unwrap_or(0) } /// A reference to the [`IFrameCollection`] of this [`Document`], holding information about /// `<iframe>`s found within it. pub(crate) fn iframes(&self) -> Ref<'_, IFrameCollection> { self.iframes.borrow() } /// A mutable reference to the [`IFrameCollection`] of this [`Document`], holding information about /// `<iframe>`s found within it. pub(crate) fn iframes_mut(&self) -> RefMut<'_, IFrameCollection> { self.iframes.borrow_mut() } pub(crate) fn set_navigation_start(&self, navigation_start: CrossProcessInstant) { self.interactive_time .borrow_mut() .set_navigation_start(navigation_start); } pub(crate) fn get_interactive_metrics(&self) -> Ref<'_, ProgressiveWebMetrics> { self.interactive_time.borrow() } pub(crate) fn has_recorded_tti_metric(&self) -> bool { self.get_interactive_metrics().get_tti().is_some() } pub(crate) fn start_tti(&self) { if self.get_interactive_metrics().needs_tti() { self.tti_window.borrow_mut().start_window(); } } /// check tti for this document /// if it's been 10s since this doc encountered a task over 50ms, then we consider the /// main thread available and try to set tti pub(crate) fn record_tti_if_necessary(&self) { if self.has_recorded_tti_metric() { return; } if self.tti_window.borrow().needs_check() { self.get_interactive_metrics() .maybe_set_tti(InteractiveFlag::TimeToInteractive( self.tti_window.borrow().get_start(), )); } } /// <https://html.spec.whatwg.org/multipage/#cookie-averse-document-object> pub(crate) fn is_cookie_averse(&self) -> bool { !self.has_browsing_context || !url_has_network_scheme(&self.url()) } pub(crate) fn custom_element_registry(&self) -> Option<DomRoot<CustomElementRegistry>> { self.document_or_shadow_root.custom_element_registry() } pub(crate) fn set_custom_element_registry(&self, registry: &CustomElementRegistry) { self.document_or_shadow_root .set_custom_element_registry(Some(registry)); } /// Cleans up any active promises /// <https://github.com/servo/servo/issues/15318> pub(crate) fn teardown_custom_element_registry(&self) { if let Some(custom_elements) = self.custom_element_registry() { custom_elements.teardown(); } } pub(crate) fn increment_throw_on_dynamic_markup_insertion_counter(&self) { let counter = self.throw_on_dynamic_markup_insertion_counter.get(); self.throw_on_dynamic_markup_insertion_counter .set(counter + 1); } pub(crate) fn decrement_throw_on_dynamic_markup_insertion_counter(&self) { let counter = self.throw_on_dynamic_markup_insertion_counter.get(); self.throw_on_dynamic_markup_insertion_counter .set(counter - 1); } pub(crate) fn react_to_environment_changes(&self, cx: &JSContext) { for image in self.responsive_images.borrow().iter() { image.react_to_environment_changes(cx); } } pub(crate) fn register_responsive_image(&self, img: &HTMLImageElement) { self.responsive_images.borrow_mut().push(Dom::from_ref(img)); } pub(crate) fn unregister_responsive_image(&self, img: &HTMLImageElement) { let index = self .responsive_images .borrow() .iter() .position(|x| **x == *img); if let Some(i) = index { self.responsive_images.borrow_mut().remove(i); } } pub(crate) fn register_media_controls(&self, id: &str, controls: &ShadowRoot) { let did_have_these_media_controls = self .media_controls .borrow_mut() .insert(id.to_string(), Dom::from_ref(controls)) .is_some(); debug_assert!( !did_have_these_media_controls, "Trying to register known media controls" ); } pub(crate) fn unregister_media_controls(&self, id: &str) { let did_have_these_media_controls = self.media_controls.borrow_mut().remove(id).is_some(); debug_assert!( did_have_these_media_controls, "Trying to unregister unknown media controls" ); } pub(crate) fn mark_canvas_as_dirty(&self, canvas: &Dom<HTMLCanvasElement>) { let mut dirty_canvases = self.dirty_canvases.borrow_mut(); if dirty_canvases .iter() .any(|dirty_canvas| dirty_canvas == canvas) { return; } dirty_canvases.push(canvas.clone()); } /// Whether or not this [`Document`] needs a rendering update, due to changed /// contents or pending events. This is used to decide whether or not to schedule /// a call to the "update the rendering" algorithm. pub(crate) fn needs_rendering_update(&self, no_gc: &NoGC) -> bool { if !self.is_fully_active() { return false; } if !self.window().layout_blocked() && (!self.restyle_reason(no_gc).is_empty() || self.window().layout().needs_new_display_list() || self.window().layout().needs_accessibility_update()) { return true; } if !self.rendering_update_reasons.get().is_empty() { return true; } if self.event_handler.has_pending_input_events() { return true; } if self.has_pending_scroll_events() { return true; } if self.window().has_unhandled_resize_event() { return true; } if self.has_pending_animated_image_update.get() || !self.dirty_canvases.borrow().is_empty() { return true; } if self.window().has_pending_media_query_evaluation() { return true; } if self .selection() .is_some_and(|selection| selection.visible_selection_dirty()) { return true; } false } /// An implementation of step 22 from /// <https://html.spec.whatwg.org/multipage/#update-the-rendering>: /// // > Step 22: For each doc of docs, update the rendering or user interface of // > doc and its node navigable to reflect the current state. // // Returns the set of reflow phases run as a [`ReflowPhasesRun`]. pub(crate) fn update_the_rendering( &self, cx: &mut JSContext, ) -> (ReflowPhasesRun, ReflowStatistics) { assert!(!self.is_render_blocked()); let mut phases = ReflowPhasesRun::empty(); if self.has_pending_animated_image_update.get() { self.image_animation_manager .borrow() .update_active_frames(&self.window, self.current_animation_timeline_value()); self.has_pending_animated_image_update.set(false); phases.insert(ReflowPhasesRun::UpdatedImageData); } self.current_rendering_epoch .set(self.current_rendering_epoch.get().next()); let current_rendering_epoch = self.current_rendering_epoch.get(); // All dirty canvases are flushed before updating the rendering. let image_keys: Vec<_> = self .dirty_canvases .borrow_mut() .drain(..) .filter_map(|canvas| canvas.update_rendering(current_rendering_epoch)) .collect(); // The renderer should wait to display the frame until all canvas images are // uploaded. This allows canvas image uploading to happen asynchronously. let pipeline_id = self.window().pipeline_id(); if !image_keys.is_empty() { phases.insert(ReflowPhasesRun::UpdatedImageData); self.waiting_on_canvas_image_updates.set(true); self.window().paint_api().delay_new_frame_for_canvas( self.webview_id(), self.window().pipeline_id(), current_rendering_epoch, image_keys, ); } let (reflow_phases, statistics) = self.window().reflow(cx, ReflowGoal::UpdateTheRendering); let phases = phases.union(reflow_phases); self.window().paint_api().update_epoch( self.webview_id(), pipeline_id, current_rendering_epoch, ); (phases, statistics) } pub(crate) fn handle_no_longer_waiting_on_asynchronous_image_updates(&self) { self.waiting_on_canvas_image_updates.set(false); } pub(crate) fn waiting_on_canvas_image_updates(&self) -> bool { self.waiting_on_canvas_image_updates.get() } /// From <https://drafts.csswg.org/css-font-loading/#fontfaceset-pending-on-the-environment>: /// /// > A FontFaceSet is pending on the environment if any of the following are true: /// > - the document is still loading /// > - the document has pending stylesheet requests /// > - the document has pending layout operations which might cause the user agent to request /// > a font, or which depend on recently-loaded fonts /// /// Returns true if the promise was fulfilled. pub(crate) fn maybe_fulfill_font_ready_promise(&self, cx: &mut JSContext) -> bool { if !self.is_fully_active() { return false; } let fonts = self.Fonts(cx); if !fonts.waiting_to_fullfill_promise() { return false; } if self.window().font_context().web_fonts_still_loading() != 0 { return false; } if self.ReadyState() != DocumentReadyState::Complete { return false; } if !self.restyle_reason(cx.no_gc()).is_empty() { return false; } if !self.rendering_update_reasons.get().is_empty() { return false; } let result = fonts.fulfill_ready_promise_if_needed(cx); // Add a rendering update after the `fonts.ready` promise is fulfilled just for // the sake of taking screenshots. This has the effect of delaying screenshots // until layout has taken a shot at updating the rendering. if result { self.add_rendering_update_reason(RenderingUpdateReason::FontReadyPromiseFulfilled); } result } pub(crate) fn id_map(&self) -> &TreeOrderedIndexMap { &self.id_map } /// <https://drafts.csswg.org/resize-observer/#dom-resizeobserver-resizeobserver> pub(crate) fn add_resize_observer(&self, resize_observer: &ResizeObserver) { self.resize_observers .borrow_mut() .push(Dom::from_ref(resize_observer)); } /// <https://drafts.csswg.org/resize-observer/#gather-active-observations-h> /// <https://drafts.csswg.org/resize-observer/#has-active-resize-observations> pub(crate) fn gather_active_resize_observations_at_depth( &self, depth: &ResizeObservationDepth, ) -> bool { let mut has_active_resize_observations = false; for observer in self.resize_observers.borrow_mut().iter_mut() { observer.gather_active_resize_observations_at_depth( depth, &mut has_active_resize_observations, ); } has_active_resize_observations } /// <https://drafts.csswg.org/resize-observer/#broadcast-active-resize-observations> #[expect(clippy::redundant_iter_cloned)] pub(crate) fn broadcast_active_resize_observations( &self, cx: &mut JSContext, ) -> ResizeObservationDepth { let mut shallowest = ResizeObservationDepth::max(); // Breaking potential re-borrow cycle on `resize_observers`: // broadcasting resize observations calls into a JS callback, // which can add new observers. let iterator: Vec<DomRoot<ResizeObserver>> = self .resize_observers .borrow() .iter() .cloned() .map(|obs| DomRoot::from_ref(&*obs)) .collect(); for observer in iterator { observer.broadcast_active_resize_observations(cx, &mut shallowest); } shallowest } /// <https://drafts.csswg.org/resize-observer/#has-skipped-observations-h> pub(crate) fn has_skipped_resize_observations(&self) -> bool { self.resize_observers .borrow() .iter() .any(|observer| observer.has_skipped_resize_observations()) } /// <https://drafts.csswg.org/resize-observer/#deliver-resize-loop-error-notification> pub(crate) fn deliver_resize_loop_error_notification(&self, cx: &mut JSContext) { let error_info: ErrorInfo = crate::dom::bindings::error::ErrorInfo { message: "ResizeObserver loop completed with undelivered notifications.".to_string(), ..Default::default() }; self.window .as_global_scope() .report_an_error(cx, error_info, HandleValue::null()); } pub(crate) fn status_code(&self) -> Option<u16> { self.status_code } /// <https://html.spec.whatwg.org/multipage/#encoding-parsing-a-url> pub(crate) fn encoding_parse_a_url(&self, url: &str) -> Result<ServoUrl, url::ParseError> { // NOTE: This algorithm is defined for both Document and environment settings objects. // This implementation is only for documents. // Step 1. Let encoding be UTF-8. // Step 2. If environment is a Document object, then set encoding to environment's character encoding. let encoding = self.encoding.get(); // Step 3. Otherwise, if environment's relevant global object is a Window object, set encoding to environment's // relevant global object's associated Document's character encoding. // Step 4. Let baseURL be environment's base URL, if environment is a Document object; // otherwise environment's API base URL. let base_url = self.base_url(); // Step 5. Return the result of applying the URL parser to url, with baseURL and encoding. url::Url::options() .base_url(Some(base_url.as_url())) .encoding_override(Some(&|input| { servo_url::encoding::encode_as_url_query_string(input, encoding) })) .parse(url) .map(ServoUrl::from) } /// <https://html.spec.whatwg.org/multipage/#allowed-to-use> pub(crate) fn allowed_to_use_feature(&self, _feature: PermissionName) -> bool { // Step 1. If document's browsing context is null, then return false. if !self.has_browsing_context { return false; } // Step 2. If document is not fully active, then return false. if !self.is_fully_active() { return false; } // Step 3. If the result of running is feature enabled in document for origin on // feature, document, and document's origin is "Enabled", then return true. // Step 4. Return false. // TODO: All features are currently enabled for `Document`s because we do not // implement the Permissions Policy specification. true } /// Add an [`IntersectionObserver`] to the [`Document`], to be processed in the [`Document`]'s event loop. /// <https://github.com/w3c/IntersectionObserver/issues/525> pub(crate) fn add_intersection_observer(&self, intersection_observer: &IntersectionObserver) { self.intersection_observers .borrow_mut() .push(Dom::from_ref(intersection_observer)); } /// Remove an [`IntersectionObserver`] from [`Document`], ommiting it from the event loop. /// An observer without any target, ideally should be removed to be conformant with /// <https://w3c.github.io/IntersectionObserver/#lifetime>. pub(crate) fn remove_intersection_observer( &self, intersection_observer: &IntersectionObserver, ) { self.intersection_observers .borrow_mut() .retain(|observer| *observer != intersection_observer) } /// <https://w3c.github.io/IntersectionObserver/#update-intersection-observations-algo> pub(crate) fn update_intersection_observer_steps( &self, cx: &mut JSContext, time: CrossProcessInstant, ) { if self.intersection_observers.borrow().is_empty() { return; } // Ensure that any layout changes are flushed for subsequent queries. self.window() .reflow_for_non_flushing_update_the_rendering_queries(cx); // Step 1-2 for intersection_observer in &*self.intersection_observers.borrow() { self.update_single_intersection_observer_steps(cx, intersection_observer, time); } } /// Step 2.1-2.2 of <https://w3c.github.io/IntersectionObserver/#update-intersection-observations-algo> fn update_single_intersection_observer_steps( &self, cx: &mut JSContext, intersection_observer: &IntersectionObserver, time: CrossProcessInstant, ) { // Step 1 // > Let rootBounds be observer’s root intersection rectangle. let root_bounds = intersection_observer.root_intersection_rectangle(); // Step 2 // > For each target in observer’s internal [[ObservationTargets]] slot, // > processed in the same order that observe() was called on each target: intersection_observer.update_intersection_observations_steps(cx, self, time, root_bounds); } /// <https://w3c.github.io/IntersectionObserver/#notify-intersection-observers-algo> pub(crate) fn notify_intersection_observers(&self, cx: &mut JSContext) { // Step 1 // > Set document’s IntersectionObserverTaskQueued flag to false. self.intersection_observer_task_queued.set(false); // Step 2 // > Let notify list be a list of all IntersectionObservers whose root is in the DOM tree of document. // We will copy the observers because callback could modify the current list. // It will rooted to prevent GC in the iteration. rooted_vec!(let notify_list <- self.intersection_observers.clone().take().into_iter()); // Step 3 // > For each IntersectionObserver object observer in notify list, run these steps: for intersection_observer in notify_list.iter() { // Step 3.1-3.5 intersection_observer.invoke_callback_if_necessary(cx); } } /// <https://w3c.github.io/IntersectionObserver/#queue-intersection-observer-task> pub(crate) fn queue_an_intersection_observer_task(&self) { // Step 1 // > If document’s IntersectionObserverTaskQueued flag is set to true, return. if self.intersection_observer_task_queued.get() { return; } // Step 2 // > Set document’s IntersectionObserverTaskQueued flag to true. self.intersection_observer_task_queued.set(true); // Step 3 // > Queue a task on the IntersectionObserver task source associated with // > the document's event loop to notify intersection observers. let document = Trusted::new(self); self.owner_global() .task_manager() .intersection_observer_task_source() .queue(task!(notify_intersection_observers: move |cx| { document.root().notify_intersection_observers(cx); })); } pub(crate) fn store_lcp_candidate(&self, id: LCPCandidateID, element: &Element) { self.lcp_candidates .borrow_mut() .insert(id, Dom::from_ref(element)); } pub(crate) fn handle_paint_metric( &self, cx: &mut JSContext, metric_type: ProgressiveWebMetricType, metric_value: CrossProcessInstant, first_reflow: bool, ) { let metrics = self.interactive_time.borrow(); match metric_type { ProgressiveWebMetricType::FirstPaint | ProgressiveWebMetricType::FirstContentfulPaint => { let binding = PerformancePaintTiming::new( cx, self.window.as_global_scope(), metric_type.clone(), metric_value, ); metrics.set_performance_paint_metric(metric_value, first_reflow, metric_type); let entry = binding.upcast::<PerformanceEntry>(); self.window.Performance(cx).queue_entry(entry); }, ProgressiveWebMetricType::LargestContentfulPaint { area, url, id } => { let binding = LargestContentfulPaint::new( cx, self.window.as_global_scope(), metric_value, area, url, self.lcp_candidates.borrow_mut().remove(&id).as_deref(), ); metrics.set_largest_contentful_paint(id, metric_value, area); let entry = binding.upcast::<PerformanceEntry>(); self.window.Performance(cx).queue_entry(entry); }, ProgressiveWebMetricType::TimeToInteractive => { unreachable!("Unexpected non-paint metric.") }, } } /// <https://html.spec.whatwg.org/multipage/#document-write-steps> fn write( &self, cx: &mut JSContext, text: Vec<TrustedHTMLOrString>, line_feed: bool, containing_class: &str, field: &str, ) -> ErrorResult { // Step 1: Let string be the empty string. let mut strings: Vec<String> = Vec::with_capacity(text.len()); // Step 2: Let isTrusted be false if text contains a string; otherwise true. let mut is_trusted = true; // Step 3: For each value of text: for value in text { match value { // Step 3.1: If value is a TrustedHTML object, then append value's associated data to string. TrustedHTMLOrString::TrustedHTML(trusted_html) => { strings.push(trusted_html.to_string()); }, TrustedHTMLOrString::String(str_) => { // Step 2: Let isTrusted be false if text contains a string; otherwise true. is_trusted = false; // Step 3.2: Otherwise, append value to string. strings.push(str_.into()); }, }; } let mut string = itertools::join(strings, ""); // Step 4: If isTrusted is false, set string to the result of invoking the // Get Trusted Type compliant string algorithm with TrustedHTML, // this's relevant global object, string, sink, and "script". if !is_trusted { string = TrustedHTML::get_trusted_type_compliant_string( cx, &self.global(), TrustedHTMLOrString::String(string.into()), &format!("{} {}", containing_class, field), )? .str() .to_owned(); } // Step 5: If lineFeed is true, append U+000A LINE FEED to string. if line_feed { string.push('\n'); } // Step 6: If document is an XML document, then throw an "InvalidStateError" DOMException. if !self.is_html_document() { return Err(Error::InvalidState(None)); } // Step 7: If document's throw-on-dynamic-markup-insertion counter is greater than 0, // then throw an "InvalidStateError" DOMException. if self.throw_on_dynamic_markup_insertion_counter.get() > 0 { return Err(Error::InvalidState(None)); } // Step 8: If document's active parser was aborted is true, then return. if self.active_parser_was_aborted.get() { return Ok(()); } let parser = match self.get_current_parser() { Some(ref parser) if parser.can_write() => DomRoot::from_ref(&**parser), // Step 9: If the insertion point is undefined, then: _ => { // Step 9.1: If document's unload counter is greater than 0 or // document's ignore-destructive-writes counter is greater than 0, then return. if self.is_prompting_or_unloading() || self.ignore_destructive_writes_counter.get() > 0 { return Ok(()); } // Step 9.2: Run the document open steps with document. self.Open(cx, None, None)?; self.get_current_parser().unwrap() }, }; // Steps 10-11. parser.write(cx, string.into()); Ok(()) } pub(crate) fn details_name_groups(&self) -> RefMut<'_, DetailsNameGroups> { RefMut::map( self.details_name_groups.borrow_mut(), |details_name_groups| details_name_groups.get_or_insert_default(), ) } pub(crate) fn accessibility_data_mut(&self) -> RefMut<'_, AccessibilityData> { self.accessibility_data.borrow_mut() } pub(crate) fn accessibility_active(&self) -> bool { self.window().layout().accessibility_active() } pub(crate) fn rooted_nodes_for_accessibility_integrity_check( &self, ) -> Option<FxHashSet<OpaqueNode>> { if !self.accessibility_active() { return None; } let mut accessibility_data = self.accessibility_data_mut(); if pref!(expensive_accessibility_test_assertions_enabled) { return Some(accessibility_data.unroot_and_drain_all_removed_nodes()); } accessibility_data.unroot_all_removed_nodes(); None } pub(crate) fn get_document_element_unrooted<'a>( &self, no_gc: &'a NoGC, ) -> Option<UnrootedDom<'a, Element>> { self.upcast::<Node>().child_elements_unrooted(no_gc).next() } } #[derive(MallocSizeOf, PartialEq)] pub(crate) enum DocumentSource { FromParser, NotFromParser, } impl<'dom> LayoutDom<'dom, Document> { #[inline] pub(crate) fn is_html_document_for_layout(&self) -> bool { self.unsafe_get().is_html_document } #[inline] pub(crate) fn quirks_mode(self) -> QuirksMode { self.unsafe_get().quirks_mode.get() } #[inline] pub(crate) fn shared_style_locks(self) -> &'dom SharedRwLocks { self.unsafe_get().shared_style_locks() } #[inline] pub(crate) fn flush_shadow_root_stylesheets_if_necessary( self, stylist: &mut Stylist, guard: &SharedRwLockReadGuard, ) { (*self.unsafe_get()).flush_shadow_root_stylesheets_if_necessary_for_layout(stylist, guard) } pub(crate) fn elements_with_id(self, id: &Atom) -> &[LayoutDom<'dom, Element>] { self.unsafe_get().id_map.get_all_for_layout(id) } #[expect(unsafe_code)] pub(crate) fn url_for_layout(self) -> ServoUrl { unsafe { self.unsafe_get().url.borrow_for_layout() }.clone() } #[expect(unsafe_code)] pub(crate) fn selection_for_layout(&self) -> Option<LayoutDom<'dom, Selection>> { unsafe { self.unsafe_get().selection.to_layout() } } } // https://html.spec.whatwg.org/multipage/#is-a-registrable-domain-suffix-of-or-is-equal-to // The spec says to return a bool, we actually return an Option<Host> containing // the parsed host in the successful case, to avoid having to re-parse the host. pub(crate) fn get_registrable_domain_suffix_of_or_is_equal_to( host_suffix_string: &str, original_host: Host, ) -> Option<Host> { // Step 1 if host_suffix_string.is_empty() { return None; } // Step 2-3. let host = match Host::parse(host_suffix_string) { Ok(host) => host, Err(_) => return None, }; // Step 4. if host != original_host { // Step 4.1 let host = match host { Host::Domain(ref host) => host, _ => return None, }; let original_host = match original_host { Host::Domain(ref original_host) => original_host, _ => return None, }; // Step 4.2 let index = original_host.len().checked_sub(host.len())?; let (prefix, suffix) = original_host.split_at(index); if !prefix.ends_with('.') { return None; } if suffix != host { return None; } // Step 4.3 if is_pub_domain(host) { return None; } } // Step 5 Some(host) } /// <https://url.spec.whatwg.org/#network-scheme> fn url_has_network_scheme(url: &ServoUrl) -> bool { matches!(url.scheme(), "ftp" | "http" | "https") } #[derive(Clone, Copy, Eq, JSTraceable, MallocSizeOf, PartialEq)] pub(crate) enum HasBrowsingContext { No, Yes, } impl Document { #[allow(clippy::too_many_arguments)] pub(crate) fn new_inherited( window: &Window, has_browsing_context: HasBrowsingContext, url: Option<ServoUrl>, about_base_url: Option<ServoUrl>, origin: MutableOrigin, is_html_document: IsHTMLDocument, content_type: Option<Mime>, last_modified: Option<String>, activity: DocumentActivity, source: DocumentSource, doc_loader: DocumentLoader, referrer: Option<String>, status_code: Option<u16>, canceller: FetchCanceller, is_initial_about_blank: bool, allow_declarative_shadow_roots: bool, inherited_insecure_requests_policy: Option<InsecureRequestsPolicy>, has_trustworthy_ancestor_origin: bool, custom_element_reaction_stack: Rc<CustomElementReactionStack>, creation_sandboxing_flag_set: SandboxingFlagSet, timeline: &DocumentTimeline, pipeline_id: PipelineId, image_cache: StdArc<dyn ImageCache>, ) -> Document { let url = url.unwrap_or_else(|| ServoUrl::parse("about:blank").unwrap()); let (ready_state, domcontentloaded_dispatched) = if source == DocumentSource::FromParser { (DocumentReadyState::Loading, false) } else { (DocumentReadyState::Complete, true) }; let frame_type = match window.is_top_level() { true => TimerMetadataFrameType::RootWindow, false => TimerMetadataFrameType::IFrame, }; let interactive_time = ProgressiveWebMetrics::new( window.time_profiler_chan().clone(), url.clone(), frame_type, ); let content_type = content_type.unwrap_or_else(|| { match is_html_document { // https://dom.spec.whatwg.org/#dom-domimplementation-createhtmldocument IsHTMLDocument::HTMLDocument => "text/html", // https://dom.spec.whatwg.org/#concept-document-content-type IsHTMLDocument::NonHTMLDocument => "application/xml", } .parse() .unwrap() }); let encoding = content_type .get_parameter(CHARSET) .and_then(|charset| Encoding::for_label(charset.as_bytes())) .unwrap_or(UTF_8); let has_focus = window.parent_info().is_none(); let has_browsing_context = has_browsing_context == HasBrowsingContext::Yes; let shared_style_locks = window.script_thread().shared_style_locks().clone(); Document { node: Node::new_document_node(), document_or_shadow_root: DocumentOrShadowRoot::new(window), window: Dom::from_ref(window), has_browsing_context, implementation: Default::default(), content_type, last_modified, url: DomRefCell::new(url), about_base_url: DomRefCell::new(about_base_url), // https://dom.spec.whatwg.org/#concept-document-quirks quirks_mode: Cell::new(QuirksMode::NoQuirks), event_handler: DocumentEventHandler::new(window), focus_handler: DocumentFocusHandler::new(window, has_focus), embedder_controls: DocumentEmbedderControls::new(window), id_map: TreeOrderedIndexMap::id(), name_map: TreeOrderedIndexMap::name(), // https://dom.spec.whatwg.org/#concept-document-encoding encoding: Cell::new(encoding), is_html_document: is_html_document == IsHTMLDocument::HTMLDocument, activity: Cell::new(activity), tag_map: DomRefCell::new(HashMapTracedValues::new_fx()), tagns_map: DomRefCell::new(HashMapTracedValues::new_fx()), classes_map: DomRefCell::new(HashMapTracedValues::new()), images: Default::default(), embeds: Default::default(), links: Default::default(), forms: Default::default(), scripts: Default::default(), anchors: Default::default(), applets: Default::default(), iframes: RefCell::new(IFrameCollection::new()), shared_style_locks, stylesheets: DomRefCell::new(DocumentStylesheetSet::new()), stylesheet_list: MutNullableDom::new(None), ready_state: Cell::new(ready_state), domcontentloaded_dispatched: Cell::new(domcontentloaded_dispatched), current_script: Default::default(), current_the_end_loading_phase: Default::default(), pending_parsing_blocking_script: Default::default(), script_blocking_stylesheet_set: Default::default(), render_blocking_element_count: Default::default(), deferred_scripts: Default::default(), asap_in_order_scripts_list: Default::default(), asap_scripts_set: Default::default(), animation_frame_ident: Cell::new(0), animation_frame_list: DomRefCell::new(VecDeque::new()), running_animation_callbacks: Cell::new(false), loader: DomRefCell::new(doc_loader), current_parser: Default::default(), base_element: Default::default(), target_base_element: Default::default(), appropriate_template_contents_owner_document: Default::default(), pending_restyles: DomRefCell::new(FxHashMap::default()), needs_restyle: Cell::new(RestyleReason::DOMChanged), origin: DomRefCell::new(origin), referrer, target_element: MutNullableDom::new(None), policy_container: DomRefCell::new(PolicyContainer::default()), preloaded_resources: Default::default(), ignore_destructive_writes_counter: Default::default(), ignore_opens_during_unload_counter: Default::default(), spurious_animation_frames: Cell::new(0), fullscreen_element: MutNullableDom::new(None), form_id_listener_map: Default::default(), interactive_time: DomRefCell::new(interactive_time), tti_window: DomRefCell::new(InteractiveWindow::default()), canceller, throw_on_dynamic_markup_insertion_counter: Cell::new(0), page_showing: Cell::new(false), salvageable: Cell::new(true), active_parser_was_aborted: Cell::new(false), fired_unload: Cell::new(false), responsive_images: Default::default(), navigation_timing: Default::default(), resource_fetch_timing: RefCell::new(None), completely_loaded: Cell::new(false), script_and_layout_blockers: Cell::new(0), delayed_tasks: Default::default(), shadow_roots: DomRefCell::new(HashSet::new()), shadow_roots_styles_changed: Cell::new(false), media_controls: DomRefCell::new(HashMap::new()), dirty_canvases: DomRefCell::new(Default::default()), has_pending_animated_image_update: Cell::new(false), selection: MutNullableDom::new(None), timeline: Dom::from_ref(timeline), animations: Animations::new(), image_animation_manager: DomRefCell::new(ImageAnimationManager::default()), dirty_root: Default::default(), declarative_refresh: Default::default(), resize_observers: Default::default(), fonts: Default::default(), visibility_state: Cell::new(DocumentVisibilityState::Hidden), status_code, is_initial_about_blank: Cell::new(is_initial_about_blank), allow_declarative_shadow_roots: Cell::new(allow_declarative_shadow_roots), inherited_insecure_requests_policy: Cell::new(inherited_insecure_requests_policy), has_trustworthy_ancestor_origin: Cell::new(has_trustworthy_ancestor_origin), intersection_observer_task_queued: Cell::new(false), intersection_observers: Default::default(), highlighted_dom_node: Default::default(), lcp_candidates: DomRefCell::new(Default::default()), adopted_stylesheets: Default::default(), adopted_stylesheets_frozen_types: CachedFrozenArray::new(), pending_scroll_events: Default::default(), rendering_update_reasons: Default::default(), waiting_on_canvas_image_updates: Cell::new(false), root_removal_noted: Cell::new(true), current_rendering_epoch: Default::default(), custom_element_reaction_stack, active_sandboxing_flag_set: Cell::new(creation_sandboxing_flag_set), creation_sandboxing_flag_set: Cell::new(creation_sandboxing_flag_set), favicon: RefCell::new(None), websockets: DOMTracker::new(), details_name_groups: Default::default(), protocol_handler_automation_mode: Default::default(), layout_animations_test_enabled: pref!(layout_animations_test_enabled), state_override: Default::default(), value_override: Default::default(), default_single_line_container_name: Default::default(), css_styling_flag: Default::default(), accessibility_data: Default::default(), iframe_load_in_progress: Default::default(), mute_iframe_load: Default::default(), timers: OneshotTimers::new(window.upcast()), pipeline_id, task_manager: Rc::new(TaskManager::new( Some(window.event_loop_sender()), pipeline_id, None, )), image_cache, history: Default::default(), } } /// Returns a policy value that should be used for fetches initiated by this document. pub(crate) fn insecure_requests_policy(&self) -> InsecureRequestsPolicy { if let Some(csp_list) = self.get_csp_list().as_ref() { for policy in &csp_list.0 { if policy.contains_a_directive_whose_name_is("upgrade-insecure-requests") && policy.disposition == PolicyDisposition::Enforce { return InsecureRequestsPolicy::Upgrade; } } } self.inherited_insecure_requests_policy .get() .unwrap_or(InsecureRequestsPolicy::DoNotUpgrade) } /// Get the [`Document`]'s [`DocumentEventHandler`]. pub(crate) fn event_handler(&self) -> &DocumentEventHandler { &self.event_handler } /// Get the [`Document`]'s [`DocumentFocusHandler`]. pub(crate) fn focus_handler(&self) -> &DocumentFocusHandler { &self.focus_handler } /// Get the [`Document`]'s [`DocumentEmbedderControls`]. pub(crate) fn embedder_controls(&self) -> &DocumentEmbedderControls { &self.embedder_controls } /// Whether or not this [`Document`] has any pending scroll events to be processed during /// "update the rendering." fn has_pending_scroll_events(&self) -> bool { !self.pending_scroll_events.borrow().is_empty() } /// Add a [`RenderingUpdateReason`] to this [`Document`] which will trigger a /// rendering update at a later time. pub(crate) fn add_rendering_update_reason(&self, reason: RenderingUpdateReason) { self.rendering_update_reasons .set(self.rendering_update_reasons.get().union(reason)); } /// Clear all [`RenderingUpdateReason`]s from this [`Document`]. pub(crate) fn clear_rendering_update_reasons(&self) { self.rendering_update_reasons .set(RenderingUpdateReason::empty()) } /// Prevent any JS or layout from running until the corresponding call to /// `remove_script_and_layout_blocker`. Used to isolate periods in which /// the DOM is in an unstable state and should not be exposed to arbitrary /// web content. Any attempts to invoke content JS or query layout during /// that time will trigger a panic. `add_delayed_task` will cause the /// provided task to be executed as soon as the last blocker is removed. pub(crate) fn add_script_and_layout_blocker(&self) { self.script_and_layout_blockers .set(self.script_and_layout_blockers.get() + 1); } /// Terminate the period in which JS or layout is disallowed from running. /// If no further blockers remain, any delayed tasks in the queue will /// be executed in queue order until the queue is empty. pub(crate) fn remove_script_and_layout_blocker(&self, cx: &mut JSContext) { assert!(self.script_and_layout_blockers.get() > 0); self.script_and_layout_blockers .set(self.script_and_layout_blockers.get() - 1); while self.script_and_layout_blockers.get() == 0 && !self.delayed_tasks.borrow().is_empty() { let task = self.delayed_tasks.borrow_mut().remove(0); task.run_box(cx); } } /// Enqueue a task to run as soon as any JS and layout blockers are removed. pub(crate) fn add_delayed_task<T: 'static + NonSendTaskBox>(&self, task: T) { self.delayed_tasks.borrow_mut().push(Box::new(task)); } /// Returns true if the DOM is in a state that will allow running content JS or /// performing a layout operation. pub(crate) fn is_safe_to_run_script_or_layout(&self) -> bool { self.script_and_layout_blockers.get() == 0 } /// Assert that the DOM is in a state that will allow running content JS or /// performing a layout operation. pub(crate) fn ensure_safe_to_run_script_or_layout(&self) { assert!( self.is_safe_to_run_script_or_layout(), "Attempt to use script or layout while DOM not in a stable state" ); } #[allow(clippy::too_many_arguments)] pub(crate) fn new( cx: &mut JSContext, window: &Window, has_browsing_context: HasBrowsingContext, url: Option<ServoUrl>, about_base_url: Option<ServoUrl>, origin: MutableOrigin, doctype: IsHTMLDocument, content_type: Option<Mime>, last_modified: Option<String>, activity: DocumentActivity, source: DocumentSource, doc_loader: DocumentLoader, referrer: Option<String>, status_code: Option<u16>, canceller: FetchCanceller, is_initial_about_blank: bool, allow_declarative_shadow_roots: bool, inherited_insecure_requests_policy: Option<InsecureRequestsPolicy>, has_trustworthy_ancestor_origin: bool, custom_element_reaction_stack: Rc<CustomElementReactionStack>, creation_sandboxing_flag_set: SandboxingFlagSet, pipeline_id: PipelineId, image_cache: StdArc<dyn ImageCache>, ) -> DomRoot<Document> { Self::new_with_proto( cx, window, None, has_browsing_context, url, about_base_url, origin, doctype, content_type, last_modified, activity, source, doc_loader, referrer, status_code, canceller, is_initial_about_blank, allow_declarative_shadow_roots, inherited_insecure_requests_policy, has_trustworthy_ancestor_origin, custom_element_reaction_stack, creation_sandboxing_flag_set, pipeline_id, image_cache, ) } #[allow(clippy::too_many_arguments)] fn new_with_proto( cx: &mut JSContext, window: &Window, proto: Option<HandleObject>, has_browsing_context: HasBrowsingContext, url: Option<ServoUrl>, about_base_url: Option<ServoUrl>, origin: MutableOrigin, doctype: IsHTMLDocument, content_type: Option<Mime>, last_modified: Option<String>, activity: DocumentActivity, source: DocumentSource, doc_loader: DocumentLoader, referrer: Option<String>, status_code: Option<u16>, canceller: FetchCanceller, is_initial_about_blank: bool, allow_declarative_shadow_roots: bool, inherited_insecure_requests_policy: Option<InsecureRequestsPolicy>, has_trustworthy_ancestor_origin: bool, custom_element_reaction_stack: Rc<CustomElementReactionStack>, creation_sandboxing_flag_set: SandboxingFlagSet, pipeline_id: PipelineId, image_cache: StdArc<dyn ImageCache>, ) -> DomRoot<Document> { let timeline = DocumentTimeline::new(cx, window); let document = reflect_dom_object_with_proto( cx, Box::new(Document::new_inherited( window, has_browsing_context, url, about_base_url, origin, doctype, content_type, last_modified, activity, source, doc_loader, referrer, status_code, canceller, is_initial_about_blank, allow_declarative_shadow_roots, inherited_insecure_requests_policy, has_trustworthy_ancestor_origin, custom_element_reaction_stack, creation_sandboxing_flag_set, &timeline, pipeline_id, image_cache, )), window, proto, ); { let node = document.upcast::<Node>(); node.set_owner_doc(&document); } document } pub(crate) fn get_redirect_count(&self) -> u16 { self.resource_fetch_timing() .as_ref() .map_or(0, |resource_fetch_timing| { resource_fetch_timing.redirect_count }) } pub(crate) fn set_resource_fetch_timing(&self, timing: ResourceFetchTiming) { self.resource_fetch_timing.replace(Some(timing)); } pub(crate) fn resource_fetch_timing(&self) -> Ref<'_, Option<ResourceFetchTiming>> { self.resource_fetch_timing.borrow() } pub(crate) fn navigation_timing(&self) -> Rc<NavigationTiming> { self.navigation_timing.clone() } pub(crate) fn performance_timing_attribute( &self, name: &str, ) -> Fallible<Option<CrossProcessInstant>> { Ok(match name { "unloadEventStart" => self.navigation_timing().unload_event_start.get(), "unloadEventEnd" => self.navigation_timing().unload_event_end.get(), "domInteractive" => self.navigation_timing().dom_interactive.get(), "domContentLoadedEventStart" => self .navigation_timing() .dom_content_loaded_event_start .get(), "domContentLoadedEventEnd" => { self.navigation_timing().dom_content_loaded_event_end.get() }, "domComplete" => self.navigation_timing().dom_complete.get(), "loadEventStart" => self.navigation_timing().load_event_start.get(), "loadEventEnd" => self.navigation_timing().load_event_end.get(), "redirectStart" | "redirectEnd" | "secureConnectionStart" | "responseEnd" => self .resource_fetch_timing() .as_ref() .and_then(|resource_fetch_timing| match name { "redirectStart" => resource_fetch_timing.redirect_start, "redirectEnd" => resource_fetch_timing.redirect_end, "secureConnectionStart" => resource_fetch_timing.secure_connection_start, "responseEnd" => resource_fetch_timing.response_end, _ => None, }), _ => { return Err(Error::Operation(Some(format!( "{name} hasn't been implemented." )))); }, }) } pub(crate) fn elements_by_name_count(&self, name: &DOMString) -> u32 { if name.is_empty() { return 0; } self.count_node_list(|n| Document::is_element_in_get_by_name(n, name)) } pub(crate) fn nth_element_by_name<'a>( &self, no_gc: &'a NoGC, index: u32, name: &DOMString, ) -> Option<UnrootedDom<'a, Node>> { if name.is_empty() { return None; } self.nth_in_node_list(no_gc, index, |n| { Document::is_element_in_get_by_name(n, name) }) } // Note that document.getByName does not match on the same conditions // as the document named getter. fn is_element_in_get_by_name(node: &Node, name: &DOMString) -> bool { let element = match node.downcast::<Element>() { Some(element) => element, None => return false, }; if element.namespace() != &ns!(html) { return false; } element.get_name().is_some_and(|n| &*n == name) } fn count_node_list<F: Fn(&Node) -> bool>(&self, callback: F) -> u32 { let doc = self.GetDocumentElement(); let maybe_node = doc.as_deref().map(Castable::upcast::<Node>); maybe_node .iter() .flat_map(|node| node.traverse_preorder(ShadowIncluding::No)) .filter(|node| callback(node)) .count() as u32 } fn nth_in_node_list<'a, F: Fn(&Node) -> bool>( &self, no_gc: &'a NoGC, index: u32, callback: F, ) -> Option<UnrootedDom<'a, Node>> { let doc = self.get_document_element_unrooted(no_gc)?; doc.upcast::<Node>() .traverse_preorder_non_rooting(no_gc, ShadowIncluding::No) .filter(|node| callback(node)) .nth(index as usize) } fn get_html_element(&self) -> Option<DomRoot<HTMLHtmlElement>> { self.GetDocumentElement().and_then(DomRoot::downcast) } /// Return a reference to the per-ScriptThread shared locks used for stylesheets. pub(crate) fn shared_style_locks(&self) -> &SharedRwLocks { &self.shared_style_locks } /// Return a reference to the per-ScriptThread shared lock used for author stylesheets. pub(crate) fn style_shared_author_lock(&self) -> &SharedRwLock { &self.shared_style_locks.author } /// Flushes the stylesheet list, and returns whether any stylesheet changed. pub(crate) fn flush_stylesheets_for_reflow(&self) -> bool { // NOTE(emilio): The invalidation machinery is used on the replicated // list in layout. // // FIXME(emilio): This really should differentiate between CSSOM changes // and normal stylesheets additions / removals, because in the last case // layout already has that information and we could avoid dirtying the whole thing. let mut stylesheets = self.stylesheets.borrow_mut(); let have_changed = stylesheets.has_changed(); stylesheets.flush_without_invalidation(); have_changed } pub(crate) fn salvageable(&self) -> bool { self.salvageable.get() } /// <https://html.spec.whatwg.org/multipage/#make-document-unsalvageable> pub(crate) fn make_document_unsalvageable(&self) { // Step 1. Let details be a new not restored reason details whose reason is reason. // TODO // Step 2. Append details to document's bfcache blocking details. // TODO // Step 3. Set document's salvageable state to false. self.salvageable.set(false); } /// <https://html.spec.whatwg.org/multipage/#appropriate-template-contents-owner-document> pub(crate) fn appropriate_template_contents_owner_document( &self, cx: &mut JSContext, ) -> DomRoot<Document> { self.appropriate_template_contents_owner_document .or_init(|| { let doctype = if self.is_html_document { IsHTMLDocument::HTMLDocument } else { IsHTMLDocument::NonHTMLDocument }; let new_doc = Document::new( cx, self.window(), HasBrowsingContext::No, None, None, // https://github.com/whatwg/html/issues/2109 MutableOrigin::new(ImmutableOrigin::new_opaque()), doctype, None, None, DocumentActivity::Inactive, DocumentSource::NotFromParser, DocumentLoader::new(&self.loader()), None, None, Default::default(), false, self.allow_declarative_shadow_roots(), Some(self.insecure_requests_policy()), self.has_trustworthy_ancestor_or_current_origin(), self.custom_element_reaction_stack.clone(), self.creation_sandboxing_flag_set(), self.pipeline_id(), self.image_cache.clone(), ); new_doc .appropriate_template_contents_owner_document .set(Some(&new_doc)); new_doc }) } pub(crate) fn get_element_by_id(&self, no_gc: &NoGC, id: &Atom) -> Option<DomRoot<Element>> { self.id_map.get(no_gc, self.upcast(), id) } pub(crate) fn ensure_pending_restyle(&self, el: &Element) -> RefMut<'_, PendingRestyle> { let map = self.pending_restyles.borrow_mut(); RefMut::map(map, |m| { &mut m .entry(Dom::from_ref(el)) .or_insert_with(|| NoTrace(PendingRestyle::default())) .0 }) } pub(crate) fn element_attr_will_change(&self, el: &Element, attr: AttrRef<'_>) { // FIXME(emilio): Kind of a shame we have to duplicate this. // // I'm getting rid of the whole hashtable soon anyway, since all it does // right now is populate the element restyle data in layout, and we // could in theory do it in the DOM I think. let mut entry = self.ensure_pending_restyle(el); if entry.snapshot.is_none() { entry.snapshot = Some(Snapshot::new()); } if attr.local_name() == &local_name!("style") { entry.hint.insert(RestyleHint::RESTYLE_STYLE_ATTRIBUTE); } if vtable_for(el.upcast()).attribute_affects_presentational_hints(attr) || el.check_style_on_self_or_eager_pseudos(|style| { if let Some(ref attribute_references) = style.attribute_references { return attribute_references.contains_key(attr.local_name()); } false }) { entry.hint.insert(RestyleHint::RESTYLE_SELF); } let snapshot = entry.snapshot.as_mut().unwrap(); if attr.local_name() == &local_name!("id") { if snapshot.id_changed { return; } snapshot.id_changed = true; } else if attr.local_name() == &local_name!("class") { if snapshot.class_changed { return; } snapshot.class_changed = true; } else { snapshot.other_attributes_changed = true; } let local_name = style::LocalName::cast(attr.local_name()); if !snapshot.changed_attrs.contains(local_name) { snapshot.changed_attrs.push(local_name.clone()); } if snapshot.attrs.is_none() { let attrs = el .attrs() .borrow() .iter() .map(|attr| (attr.identifier().clone(), attr.value().clone())) .collect(); snapshot.attrs = Some(attrs); } } pub(crate) fn set_referrer_policy(&self, policy: ReferrerPolicy) { self.policy_container .borrow_mut() .set_referrer_policy(policy); } pub(crate) fn get_referrer_policy(&self) -> ReferrerPolicy { self.policy_container.borrow().get_referrer_policy() } pub(crate) fn set_target_element(&self, node: Option<&Element>) { if let Some(ref element) = self.target_element.get() { element.set_target_state(false); } self.target_element.set(node); if let Some(ref element) = self.target_element.get() { element.set_target_state(true); } } pub(crate) fn incr_ignore_destructive_writes_counter(&self) { self.ignore_destructive_writes_counter .set(self.ignore_destructive_writes_counter.get() + 1); } pub(crate) fn decr_ignore_destructive_writes_counter(&self) { self.ignore_destructive_writes_counter .set(self.ignore_destructive_writes_counter.get() - 1); } pub(crate) fn is_prompting_or_unloading(&self) -> bool { self.ignore_opens_during_unload_counter.get() > 0 } fn incr_ignore_opens_during_unload_counter(&self) { self.ignore_opens_during_unload_counter .set(self.ignore_opens_during_unload_counter.get() + 1); } fn decr_ignore_opens_during_unload_counter(&self) { self.ignore_opens_during_unload_counter .set(self.ignore_opens_during_unload_counter.get() - 1); } pub(crate) fn set_fullscreen_element(&self, element: Option<&Element>) { self.fullscreen_element.set(element); } fn reset_form_owner_for_listeners(&self, cx: &mut JSContext, id: &Atom) { let map = self.form_id_listener_map.borrow(); if let Some(listeners) = map.get(id) { for listener in listeners { listener .as_maybe_form_control() .expect("Element must be a form control") .reset_form_owner(cx); } } } pub(crate) fn register_shadow_root(&self, shadow_root: &ShadowRoot) { self.shadow_roots .borrow_mut() .insert(Dom::from_ref(shadow_root)); self.invalidate_shadow_roots_stylesheets(); } pub(crate) fn unregister_shadow_root(&self, shadow_root: &ShadowRoot) { let mut shadow_roots = self.shadow_roots.borrow_mut(); shadow_roots.remove(&Dom::from_ref(shadow_root)); } pub(crate) fn invalidate_shadow_roots_stylesheets(&self) { self.shadow_roots_styles_changed.set(true); } pub(crate) fn flush_shadow_root_stylesheets_if_necessary_for_layout( &self, stylist: &mut Stylist, guard: &SharedRwLockReadGuard, ) { if !self.shadow_roots_styles_changed.get() { return; } #[expect(unsafe_code)] unsafe { for shadow_root in self.shadow_roots.borrow_for_layout().iter() { let layout: LayoutDom<'_, _> = shadow_root.to_layout(); layout.flush_stylesheets_for_layout(stylist, guard); } } self.shadow_roots_styles_changed.set(false); } pub(crate) fn stylesheet_count(&self) -> usize { self.stylesheets.borrow().len() } pub(crate) fn stylesheet_at( &self, cx: &mut JSContext, index: usize, ) -> Option<DomRoot<CSSStyleSheet>> { let stylesheets = self.stylesheets.borrow(); stylesheets .get(Origin::Author, index) .and_then(|s| s.owner.get_cssom_object(cx)) } /// Add a stylesheet owned by `owner_node` to the list of document sheets, in the /// correct tree position. Additionally, ensure that the owned stylesheet is inserted /// before any constructed stylesheet. /// /// <https://drafts.csswg.org/cssom/#documentorshadowroot-final-css-style-sheets> #[cfg_attr(crown, expect(crown::unrooted_must_root))] // Owner needs to be rooted already necessarily. pub(crate) fn add_owned_stylesheet(&self, owner_node: &Element, sheet: Arc<Stylesheet>) { let insertion_point = { let stylesheets = &mut *self.stylesheets.borrow_mut(); // FIXME(stevennovaryo): This is almost identical with the one in ShadowRoot::add_stylesheet. stylesheets .iter() .map(|(sheet, _origin)| sheet) .find(|sheet_in_doc| { match &sheet_in_doc.owner { StylesheetSource::Element(other_node) => { owner_node.upcast::<Node>().is_before(other_node.upcast()) }, // Non-constructed stylesheet should be ordered before the // constructed ones. StylesheetSource::Constructed(_) => true, } }) .cloned() }; if self.has_browsing_context() { self.add_stylesheet_to_stylist( sheet.clone(), insertion_point.as_ref().map(|s| s.sheet.clone()), ); } let stylesheets = &mut *self.stylesheets.borrow_mut(); DocumentOrShadowRoot::add_stylesheet( StylesheetSource::Element(Dom::from_ref(owner_node)), StylesheetSetRef::Document(stylesheets), sheet, insertion_point, self.style_shared_author_lock(), ); } /// Append a constructed stylesheet to the back of document stylesheet set. Because /// it would be the last element, we therefore would not mess with the ordering. /// /// <https://drafts.csswg.org/cssom/#documentorshadowroot-final-css-style-sheets> #[cfg_attr(crown, expect(crown::unrooted_must_root))] pub(crate) fn append_constructed_stylesheet(&self, cssom_stylesheet: &CSSStyleSheet) { debug_assert!(cssom_stylesheet.is_constructed()); let sheet = cssom_stylesheet.style_stylesheet().clone(); let insertion_point = { let stylesheets = &mut *self.stylesheets.borrow_mut(); stylesheets .iter() .last() .map(|(sheet, _origin)| sheet) .cloned() }; if self.has_browsing_context() { self.add_stylesheet_to_stylist( sheet.clone(), insertion_point.as_ref().map(|s| s.sheet.clone()), ); } let stylesheets = &mut *self.stylesheets.borrow_mut(); DocumentOrShadowRoot::add_stylesheet( StylesheetSource::Constructed(Dom::from_ref(cssom_stylesheet)), StylesheetSetRef::Document(stylesheets), sheet, insertion_point, self.style_shared_author_lock(), ); } pub(crate) fn add_stylesheet_to_stylist( &self, stylesheet: Arc<Stylesheet>, before_stylesheet: Option<Arc<Stylesheet>>, ) { self.window .layout_mut() .add_stylesheet(stylesheet, before_stylesheet); } /// Remove a stylesheet owned by `owner` from the list of document sheets. #[cfg_attr(crown, expect(crown::unrooted_must_root))] // Owner needs to be rooted already necessarily. pub(crate) fn remove_stylesheet(&self, owner: StylesheetSource, stylesheet: &Arc<Stylesheet>) { if self.has_browsing_context() { self.window .layout_mut() .remove_stylesheet(stylesheet.clone()); } DocumentOrShadowRoot::remove_stylesheet( owner, stylesheet, StylesheetSetRef::Document(&mut *self.stylesheets.borrow_mut()), ) } pub(crate) fn get_elements_with_id( &self, cx: &mut JSContext, id: &Atom, ) -> Ref<'_, [Dom<Element>]> { self.id_map.get_all(cx.no_gc(), self.upcast(), id) } pub(crate) fn get_elements_with_name( &self, cx: &mut JSContext, name: &Atom, ) -> Ref<'_, [Dom<Element>]> { self.name_map.get_all(cx.no_gc(), self.upcast(), name) } pub(crate) fn drain_pending_restyles( &self, no_gc: &NoGC, ) -> Vec<(TrustedNodeAddress, PendingRestyle)> { self.pending_restyles .borrow_mut() .drain() .filter_map(|(element, restyle)| { let node = element.upcast::<Node>(); if !node.get_flag(NodeFlags::IS_CONNECTED) { return None; } element.note_dirty_descendants(no_gc); Some((node.to_trusted_node_address(), restyle.0)) }) .collect() } pub(crate) fn advance_animation_timeline_for_testing(&self, delta: TimeDuration) { self.timeline.advance_specific(delta); let current_timeline_value = self.current_animation_timeline_value(); self.animations .update_for_new_timeline_value(&self.window, current_timeline_value); } pub(crate) fn maybe_mark_animating_nodes_as_dirty(&self, no_gc: &NoGC) { let current_timeline_value = self.current_animation_timeline_value(); self.animations .mark_animating_nodes_as_dirty(no_gc, current_timeline_value); } pub(crate) fn current_animation_timeline_value(&self) -> f64 { self.timeline .upcast::<AnimationTimeline>() .current_time_in_seconds() } pub(crate) fn animations(&self) -> &Animations { &self.animations } pub(crate) fn update_animations_post_reflow(&self) { let current_timeline_value = self.current_animation_timeline_value(); self.animations .do_post_reflow_update(&self.window, current_timeline_value); self.image_animation_manager .borrow_mut() .do_post_reflow_update(&self.window, current_timeline_value); } pub(crate) fn cancel_animations_for_node(&self, node: &Node) { self.animations.cancel_animations_for_node(node); self.image_animation_manager .borrow_mut() .cancel_animations_for_node(node); } /// Clear style and layout data on this [`Node`] and all descendants. This is used to clean /// up the data when a [`Node`] becomes detached from the flat tree. Note that this /// operates on shadow-including descendants. pub(crate) fn remove_style_and_layout_data_from_subtree( &self, no_gc: &NoGC, subtree_root: &Node, ) { for node in subtree_root.traverse_preorder_non_rooting(no_gc, ShadowIncluding::Yes) { self.clean_up_style_and_layout_data_for_node(&node); } } pub(crate) fn clean_up_style_and_layout_data_for_node(&self, node: &Node) { node.clear_layout_data(); if let Some(element) = node.downcast::<Element>() { element.clean_up_style_data(); // If this element no longer has any layout or style data, nothing underneath it // can either, and it no longer needs to serve as a layout root. This method is // generally called when a node is leaving the flat tree and no longer takes part // in layout. if self.dirty_root == Some(element) { self.dirty_root.clear(); } } node.set_flag(NodeFlags::OVERLAPS_DOCUMENT_SELECTION, false); node.set_flag(NodeFlags::HAS_DIRTY_DESCENDANTS, false); } /// An implementation of <https://drafts.csswg.org/web-animations-1/#update-animations-and-send-events>. pub(crate) fn update_animations_and_send_events(&self, cx: &mut CurrentRealm) { // Only update the time if it isn't being managed by a test. if !self.layout_animations_test_enabled { self.timeline.update(self.window()); } // > 1. Update the current time of all timelines associated with doc passing now // > as the timestamp. // > 2. Remove replaced animations for doc. // // We still want to update the animations, because our timeline // value might have been advanced previously via the TestBinding. let current_timeline_value = self.current_animation_timeline_value(); self.animations .update_for_new_timeline_value(&self.window, current_timeline_value); self.maybe_mark_animating_nodes_as_dirty(cx.no_gc()); // > 3. Perform a microtask checkpoint. self.window().perform_a_microtask_checkpoint(cx); // Steps 4 through 7 occur inside `send_pending_events().` self.animations().send_pending_events(self.window(), cx); } pub(crate) fn image_animation_manager(&self) -> Ref<'_, ImageAnimationManager> { self.image_animation_manager.borrow() } pub(crate) fn set_has_pending_animated_image_update(&self) { self.has_pending_animated_image_update.set(true); } /// <https://html.spec.whatwg.org/multipage/#shared-declarative-refresh-steps> pub(crate) fn shared_declarative_refresh_steps(&self, content: &[u8], from_meta_element: bool) { // 1. If document's will declaratively refresh is true, then return. if self.will_declaratively_refresh() { return; } // 2-11 Parsing static REFRESH_REGEX: LazyLock<Regex> = LazyLock::new(|| { // s flag is used to match . on newlines since the only places we use . in the // regex is to go "to end of the string" // (?s-u:.) is used to consume invalid unicode bytes Regex::new( r#"(?xs) ^ \s* # 3 ((?<time>[0-9]+)|\.) # 5-6 [0-9.]* # 8 ( ( (\s*;|\s*,|\s) # 10.3 \s* # 10.4 ) ( ( (U|u)(R|r)(L|l) # 11.2-11.4 \s*=\s* # 11.5-11.7 )? ('(?<url1>[^']*)'(?s-u:.)*|"(?<url2>[^"]*)"(?s-u:.)*|['"]?(?<url3>(?s-u:.)*)) # 11.8 - 11.10 | (?<url4>(?s-u:.)*) ) )? $ "#, ) .unwrap() }); // 9. Let urlRecord be document's URL. let mut url_record = self.url(); let captures = if let Some(captures) = REFRESH_REGEX.captures(content) { captures } else { return; }; let time = if let Some(time_string) = captures.name("time") { u64::from_str(&String::from_utf8_lossy(time_string.as_bytes())).unwrap_or(0) } else { 0 }; let captured_url = captures.name("url1").or(captures .name("url2") .or(captures.name("url3").or(captures.name("url4")))); // 11.11 Parse: Set urlRecord to the result of encoding-parsing a URL given urlString, relative to document. if let Some(url_match) = captured_url { url_record = if let Ok(url) = ServoUrl::parse_with_base( Some(&url_record), &String::from_utf8_lossy(url_match.as_bytes()), ) { info!("Refresh to {}", url.debug_compact()); url } else { // 11.12 If urlRecord is failure, then return. return; }; // 11.13 If urlRecord's scheme is "javascript", then return. if url_record.scheme() == "javascript" { return; } } // 12. Set document's will declaratively refresh to true. if self.completely_loaded() { self.window.as_global_scope().schedule_callback( OneshotTimerCallback::RefreshRedirectDue(RefreshRedirectDue { url: url_record, from_meta_element, }), Duration::from_secs(time), ); self.set_declarative_refresh(DeclarativeRefresh::CreatedAfterLoad); } else { self.set_declarative_refresh(DeclarativeRefresh::PendingLoad { url: url_record, time, from_meta_element, }); } } pub(crate) fn will_declaratively_refresh(&self) -> bool { self.declarative_refresh.borrow().is_some() } pub(crate) fn set_declarative_refresh(&self, refresh: DeclarativeRefresh) { *self.declarative_refresh.borrow_mut() = Some(refresh); } /// <https://html.spec.whatwg.org/multipage/#visibility-state> fn update_visibility_state( &self, cx: &mut JSContext, visibility_state: DocumentVisibilityState, ) { // Step 1 If document's visibility state equals visibilityState, then return. if self.visibility_state.get() == visibility_state { return; } // Step 2 Set document's visibility state to visibilityState. self.visibility_state.set(visibility_state); // Step 3 Queue a new VisibilityStateEntry whose visibility state is visibilityState and whose timestamp is // the current high resolution time given document's relevant global object. let entry = VisibilityStateEntry::new( cx, &self.global(), visibility_state, CrossProcessInstant::now(), ); self.window .Performance(cx) .queue_entry(entry.upcast::<PerformanceEntry>()); // Step 4 Run the screen orientation change steps with document. // TODO ScreenOrientation hasn't implemented yet // Step 5 Run the view transition page visibility change steps with document. // TODO ViewTransition hasn't implemented yet // Step 6 Run any page visibility change steps which may be defined in other specifications, with visibility // state and document. Any other specs' visibility steps will go here. // <https://www.w3.org/TR/gamepad/#handling-visibility-change> #[cfg(feature = "gamepad")] if visibility_state == DocumentVisibilityState::Hidden { self.window .Navigator(cx) .GetGamepads(cx) .unwrap_or_default() .iter_mut() .for_each(|gamepad| { if let Some(g) = gamepad { g.vibration_actuator().handle_visibility_change(); } }); } // Step 7 Fire an event named visibilitychange at document, with its bubbles attribute initialized to true. self.upcast::<EventTarget>() .fire_bubbling_event(cx, atom!("visibilitychange")); } /// <https://html.spec.whatwg.org/multipage/#is-initial-about:blank> pub(crate) fn is_initial_about_blank(&self) -> bool { self.is_initial_about_blank.get() } /// <https://dom.spec.whatwg.org/#document-allow-declarative-shadow-roots> pub(crate) fn allow_declarative_shadow_roots(&self) -> bool { self.allow_declarative_shadow_roots.get() } pub(crate) fn has_trustworthy_ancestor_origin(&self) -> bool { self.has_trustworthy_ancestor_origin.get() } pub(crate) fn has_trustworthy_ancestor_or_current_origin(&self) -> bool { self.has_trustworthy_ancestor_origin.get() || self.origin().immutable().is_potentially_trustworthy() } pub(crate) fn highlight_dom_node(&self, node: Option<&Node>) { self.highlighted_dom_node.set(node); self.add_restyle_reason(RestyleReason::HighlightedDOMNodeChanged); } pub(crate) fn highlighted_dom_node(&self) -> Option<DomRoot<Node>> { self.highlighted_dom_node.get() } pub(crate) fn custom_element_reaction_stack(&self) -> Rc<CustomElementReactionStack> { self.custom_element_reaction_stack.clone() } pub(crate) fn active_sandboxing_flag_set(&self) -> SandboxingFlagSet { self.active_sandboxing_flag_set.get() } pub(crate) fn has_active_sandboxing_flag(&self, flag: SandboxingFlagSet) -> bool { self.active_sandboxing_flag_set.get().contains(flag) } pub(crate) fn set_active_sandboxing_flag_set(&self, flags: SandboxingFlagSet) { self.active_sandboxing_flag_set.set(flags) } pub(crate) fn creation_sandboxing_flag_set(&self) -> SandboxingFlagSet { self.creation_sandboxing_flag_set.get() } pub(crate) fn creation_sandboxing_flag_set_considering_parent_iframe( &self, ) -> SandboxingFlagSet { self.window() .window_proxy() .frame_element() .and_then(|element| element.downcast::<HTMLIFrameElement>()) .map(HTMLIFrameElement::sandboxing_flag_set) .unwrap_or_else(|| self.creation_sandboxing_flag_set()) } pub(crate) fn viewport_scrolling_box(&self, flags: ScrollContainerQueryFlags) -> ScrollingBox { self.window() .scrolling_box_query(None, flags) .expect("We should always have a ScrollingBox for the Viewport") } pub(crate) fn notify_embedder_favicon(&self) { if let Some(ref image) = *self.favicon.borrow() { self.send_to_embedder(EmbedderMsg::NewFavicon(self.webview_id(), image.clone())); } } pub(crate) fn set_favicon(&self, favicon: Image) { *self.favicon.borrow_mut() = Some(favicon); self.notify_embedder_favicon(); } pub(crate) fn fullscreen_element(&self) -> Option<DomRoot<Element>> { self.fullscreen_element.get() } /// <https://w3c.github.io/editing/docs/execCommand/#state-override> pub(crate) fn state_override(&self, command_name: &CommandName) -> Option<bool> { self.state_override.borrow().get(command_name).copied() } /// <https://w3c.github.io/editing/docs/execCommand/#state-override> pub(crate) fn set_state_override(&self, command_name: CommandName, state: Option<bool>) { if let Some(state) = state { self.state_override.borrow_mut().insert(command_name, state); } else { self.value_override.borrow_mut().remove(&command_name); } } /// <https://w3c.github.io/editing/docs/execCommand/#value-override> pub(crate) fn value_override(&self, command_name: &CommandName) -> Option<DOMString> { self.value_override.borrow().get(command_name).cloned() } /// <https://w3c.github.io/editing/docs/execCommand/#value-override> pub(crate) fn set_value_override(&self, command_name: CommandName, value: Option<DOMString>) { if let Some(value) = value { self.value_override.borrow_mut().insert(command_name, value); } else { self.value_override.borrow_mut().remove(&command_name); } } /// <https://w3c.github.io/editing/docs/execCommand/#value-override> /// and <https://w3c.github.io/editing/docs/execCommand/#state-override> pub(crate) fn clear_command_overrides(&self) { self.state_override.borrow_mut().clear(); self.value_override.borrow_mut().clear(); } /// <https://w3c.github.io/editing/docs/execCommand/#default-single-line-container-name> pub(crate) fn default_single_line_container_name(&self) -> DefaultSingleLineContainerName { self.default_single_line_container_name.get() } /// <https://w3c.github.io/editing/docs/execCommand/#default-single-line-container-name> pub(crate) fn set_default_single_line_container_name( &self, value: DefaultSingleLineContainerName, ) { self.default_single_line_container_name.set(value) } /// <https://w3c.github.io/editing/docs/execCommand/#css-styling-flag> pub(crate) fn css_styling_flag(&self) -> bool { self.css_styling_flag.get() } /// <https://w3c.github.io/editing/docs/execCommand/#css-styling-flag> pub(crate) fn set_css_styling_flag(&self, value: bool) { self.css_styling_flag.set(value) } pub(crate) fn mute_iframe_load_flag(&self) -> bool { self.mute_iframe_load.get() } pub(crate) fn set_iframe_load_in_progress(&self, value: bool) { self.iframe_load_in_progress.set(value) } } impl DocumentMethods<crate::DomTypeHolder> for Document { /// <https://dom.spec.whatwg.org/#dom-document-document> fn Constructor( cx: &mut JSContext, window: &Window, proto: Option<HandleObject>, ) -> Fallible<DomRoot<Document>> { // The new Document() constructor steps are to set this’s origin to the origin of current global object’s associated Document. [HTML] let doc = window.Document(); let docloader = DocumentLoader::new(&doc.loader()); Ok(Document::new_with_proto( cx, window, proto, HasBrowsingContext::No, None, None, doc.origin().clone(), IsHTMLDocument::NonHTMLDocument, None, None, DocumentActivity::Inactive, DocumentSource::NotFromParser, docloader, None, None, Default::default(), false, doc.allow_declarative_shadow_roots(), Some(doc.insecure_requests_policy()), doc.has_trustworthy_ancestor_or_current_origin(), doc.custom_element_reaction_stack(), doc.active_sandboxing_flag_set.get(), doc.pipeline_id(), doc.image_cache(), )) } /// <https://html.spec.whatwg.org/multipage/#dom-parsehtmlunsafe> fn ParseHTMLUnsafe( cx: &mut JSContext, window: &Window, s: TrustedHTMLOrString, options: &SetHTMLUnsafeOptions, ) -> Fallible<DomRoot<Self>> { // Step 1. Let compliantHTML be the result of invoking the // Get Trusted Type compliant string algorithm with TrustedHTML, the current global object, // html, "Document parseHTMLUnsafe", and "script". let compliant_html = TrustedHTML::get_trusted_type_compliant_string( cx, window.as_global_scope(), s, "Document parseHTMLUnsafe", )?; let url = window.get_url(); let doc = window.Document(); let loader = DocumentLoader::new(&doc.loader()); let content_type = "text/html" .parse() .expect("Supported type is not a MIME type"); // Step 2. Let document be a new Document, whose content type is "text/html". // Step 3. Set document's allow declarative shadow roots to true. let document = Document::new( cx, window, HasBrowsingContext::No, Some(ServoUrl::parse("about:blank").unwrap()), None, doc.origin().clone(), IsHTMLDocument::HTMLDocument, Some(content_type), None, DocumentActivity::Inactive, DocumentSource::FromParser, loader, None, None, Default::default(), false, true, Some(doc.insecure_requests_policy()), doc.has_trustworthy_ancestor_or_current_origin(), doc.custom_element_reaction_stack(), doc.creation_sandboxing_flag_set(), doc.pipeline_id(), doc.image_cache(), ); // Step 4. Parse HTML from string given document and compliantHTML. ServoParser::parse_html_document(cx, &document, Some(compliant_html), url, None, None); // Step 5. Let sanitizer be the result of calling get a sanitizer instance from options with // options and false. let sanitizer = Sanitizer::get_sanitizer_instance_from_options(cx, window, options, false)?; // Step 6. Call sanitize on document with sanitizer and false. sanitizer.sanitize(cx, document.upcast(), false)?; // Step 7. Return document. document.set_ready_state(cx, DocumentReadyState::Complete); Ok(document) } /// <https://wicg.github.io/sanitizer-api/#dom-document-parsehtml> fn ParseHTML( cx: &mut JSContext, window: &Window, html: DOMString, options: &SetHTMLOptions, ) -> Fallible<DomRoot<Document>> { // Step 1. Let document be a new Document, whose content type is "text/html". // Step 2. Set document's allow declarative shadow roots to true. let url = window.get_url(); let doc = window.Document(); let loader = DocumentLoader::new(&doc.loader()); let content_type = "text/html" .parse() .expect("Supported type is not a MIME type"); let document = Document::new( cx, window, HasBrowsingContext::No, Some(ServoUrl::parse("about:blank").unwrap()), None, doc.origin().clone(), IsHTMLDocument::HTMLDocument, Some(content_type), None, DocumentActivity::Inactive, DocumentSource::FromParser, loader, None, None, Default::default(), false, true, Some(doc.insecure_requests_policy()), doc.has_trustworthy_ancestor_or_current_origin(), doc.custom_element_reaction_stack(), doc.creation_sandboxing_flag_set(), doc.pipeline_id(), doc.image_cache(), ); // Step 3. Parse HTML from a string given document and html. ServoParser::parse_html_document(cx, &document, Some(html), url, None, None); // Step 4. Let sanitizer be the result of calling get a sanitizer instance from options with // options and true. let sanitizer = Sanitizer::get_sanitizer_instance_from_options(cx, window, options, true)?; // Step 5. Call sanitize on document with sanitizer and true. sanitizer.sanitize(cx, document.upcast(), true)?; // Step 6. Return document. Ok(document) } /// <https://drafts.csswg.org/cssom/#dom-document-stylesheets> fn StyleSheets(&self, cx: &mut JSContext) -> DomRoot<StyleSheetList> { self.stylesheet_list.or_init(|| { StyleSheetList::new( cx, &self.window, StyleSheetListOwner::Document(Dom::from_ref(self)), ) }) } /// <https://dom.spec.whatwg.org/#dom-document-implementation> fn Implementation(&self, cx: &mut JSContext) -> DomRoot<DOMImplementation> { self.implementation .or_init(|| DOMImplementation::new(cx, self)) } /// <https://dom.spec.whatwg.org/#dom-document-url> fn URL(&self) -> USVString { USVString(String::from(self.url().as_str())) } /// <https://html.spec.whatwg.org/multipage/#dom-document-activeelement> fn GetActiveElement(&self) -> Option<DomRoot<Element>> { self.document_or_shadow_root.active_element(self.upcast()) } /// <https://dom.spec.whatwg.org/#dom-documentorshadowroot-customelementregistry> fn GetCustomElementRegistry(&self) -> Option<DomRoot<CustomElementRegistry>> { self.custom_element_registry() } /// <https://html.spec.whatwg.org/multipage/#dom-document-hasfocus> fn HasFocus(&self) -> bool { // <https://html.spec.whatwg.org/multipage/#has-focus-steps> // // > The has focus steps, given a `Document` object `target`, are as // > follows: // > // > 1. If `target`'s browsing context's top-level browsing context does // > not have system focus, then return false. // > 2. Let `candidate` be `target`'s browsing context's top-level // > browsing context's active document. // > // > 3. While true: // > // > 3.1. If `candidate` is target, then return true. // > // > 3.2. If the focused area of `candidate` is a browsing context // > container with a non-null nested browsing context, then set // > `candidate` to the active document of that browsing context // > container's nested browsing context. // > // > 3.3. Otherwise, return false. if self.window().parent_info().is_none() { // 2 → 3 → (3.1 || ⋯ → 3.3) self.is_fully_active() } else { // 2 → 3 → 3.2 → (⋯ → 3.1 || ⋯ → 3.3) self.is_fully_active() && self.focus_handler.has_focus() } } /// <https://html.spec.whatwg.org/multipage/#dom-document-domain> fn Domain(&self) -> DOMString { // Step 1. Let effectiveDomain be this's origin's effective domain. match self.origin().effective_domain() { // Step 2. If effectiveDomain is null, then return the empty string. None => DOMString::new(), // Step 3. Return effectiveDomain, serialized. Some(Host::Domain(domain)) => DOMString::from(domain), Some(host) => DOMString::from(host.to_string()), } } /// <https://html.spec.whatwg.org/multipage/#dom-document-domain> fn SetDomain(&self, value: DOMString) -> ErrorResult { // Step 1. If this's browsing context is null, then throw a "SecurityError" DOMException. if !self.has_browsing_context { return Err(Error::Security(None)); } // Step 2. If this Document object's active sandboxing flag set has its sandboxed // document.domain browsing context flag set, then throw a "SecurityError" DOMException. if self.has_active_sandboxing_flag( SandboxingFlagSet::SANDBOXED_DOCUMENT_DOMAIN_BROWSING_CONTEXT_FLAG, ) { return Err(Error::Security(None)); } // Step 3. Let effectiveDomain be this's origin's effective domain. let effective_domain = match self.origin().effective_domain() { Some(effective_domain) => effective_domain, // Step 4. If effectiveDomain is null, then throw a "SecurityError" DOMException. None => return Err(Error::Security(None)), }; // Step 5. If the given value is not a registrable domain suffix of and is not equal to effectiveDomain, then throw a "SecurityError" DOMException. let host = match get_registrable_domain_suffix_of_or_is_equal_to(&value.str(), effective_domain) { None => return Err(Error::Security(None)), Some(host) => host, }; // Step 6. If the surrounding agent's agent cluster's is origin-keyed is true, then return. // TODO // Step 7. Set this's origin's domain to the result of parsing the given value. self.origin().set_domain(host); Ok(()) } /// <https://html.spec.whatwg.org/multipage/#dom-document-referrer> fn Referrer(&self) -> DOMString { match self.referrer { Some(ref referrer) => DOMString::from(referrer.to_string()), None => DOMString::new(), } } /// <https://dom.spec.whatwg.org/#dom-document-documenturi> fn DocumentURI(&self) -> USVString { self.URL() } /// <https://dom.spec.whatwg.org/#dom-document-compatmode> fn CompatMode(&self) -> DOMString { DOMString::from(match self.quirks_mode.get() { QuirksMode::LimitedQuirks | QuirksMode::NoQuirks => "CSS1Compat", QuirksMode::Quirks => "BackCompat", }) } /// <https://dom.spec.whatwg.org/#dom-document-characterset> fn CharacterSet(&self) -> DOMString { DOMString::from(self.encoding.get().name()) } /// <https://dom.spec.whatwg.org/#dom-document-charset> fn Charset(&self) -> DOMString { self.CharacterSet() } /// <https://dom.spec.whatwg.org/#dom-document-inputencoding> fn InputEncoding(&self) -> DOMString { self.CharacterSet() } /// <https://dom.spec.whatwg.org/#dom-document-content_type> fn ContentType(&self) -> DOMString { DOMString::from(self.content_type.to_string()) } /// <https://dom.spec.whatwg.org/#dom-document-doctype> fn GetDoctype(&self) -> Option<DomRoot<DocumentType>> { self.upcast::<Node>().children().find_map(DomRoot::downcast) } /// <https://dom.spec.whatwg.org/#dom-document-documentelement> fn GetDocumentElement(&self) -> Option<DomRoot<Element>> { self.upcast::<Node>().child_elements().next() } /// <https://dom.spec.whatwg.org/#dom-document-getelementsbytagname> fn GetElementsByTagName( &self, cx: &mut JSContext, qualified_name: DOMString, ) -> DomRoot<HTMLCollection> { let qualified_name = LocalName::from(qualified_name); if let Some(entry) = self.tag_map.borrow_mut().get(&qualified_name) { return DomRoot::from_ref(entry); } let result = HTMLCollection::by_qualified_name( cx, &self.window, self.upcast(), qualified_name.clone(), ); self.tag_map .borrow_mut() .insert(qualified_name, Dom::from_ref(&*result)); result } /// <https://dom.spec.whatwg.org/#dom-document-getelementsbytagnamens> fn GetElementsByTagNameNS( &self, cx: &mut JSContext, maybe_ns: Option<DOMString>, tag_name: DOMString, ) -> DomRoot<HTMLCollection> { let ns = namespace_from_domstring(maybe_ns); let local = LocalName::from(tag_name); let qname = QualName::new(None, ns, local); if let Some(collection) = self.tagns_map.borrow().get(&qname) { return DomRoot::from_ref(collection); } let result = HTMLCollection::by_qual_tag_name(cx, &self.window, self.upcast(), qname.clone()); self.tagns_map .borrow_mut() .insert(qname, Dom::from_ref(&*result)); result } /// <https://dom.spec.whatwg.org/#dom-document-getelementsbyclassname> fn GetElementsByClassName( &self, cx: &mut JSContext, classes: DOMString, ) -> DomRoot<HTMLCollection> { let class_atoms: Vec<Atom> = split_html_space_chars(&classes.str()) .map(Atom::from) .collect(); if let Some(collection) = self.classes_map.borrow().get(&class_atoms) { return DomRoot::from_ref(collection); } let result = HTMLCollection::by_atomic_class_name( cx, &self.window, self.upcast(), class_atoms.clone(), ); self.classes_map .borrow_mut() .insert(class_atoms, Dom::from_ref(&*result)); result } /// <https://dom.spec.whatwg.org/#dom-nonelementparentnode-getelementbyid> fn GetElementById( &self, cx: &js::context::JSContext, id: DOMString, ) -> Option<DomRoot<Element>> { self.get_element_by_id(cx, &Atom::from(id)) } /// <https://dom.spec.whatwg.org/#dom-document-createelement> fn CreateElement( &self, cx: &mut JSContext, mut local_name: DOMString, options: StringOrElementCreationOptions, ) -> Fallible<DomRoot<Element>> { // Step 1. If localName is not a valid element local name, // then throw an "InvalidCharacterError" DOMException. if !is_valid_element_local_name(&local_name.str()) { debug!("Not a valid element name"); return Err(Error::InvalidCharacter(None)); } if self.is_html_document { local_name.make_ascii_lowercase(); } let ns = if self.is_html_document || self.is_xhtml_document() { ns!(html) } else { ns!() }; let name = QualName::new(None, ns, LocalName::from(local_name)); let is = match options { StringOrElementCreationOptions::String(_) => None, StringOrElementCreationOptions::ElementCreationOptions(options) => { options.is.as_ref().map(LocalName::from) }, }; Ok(Element::create( cx, name, is, self, ElementCreator::ScriptCreated, CustomElementCreationMode::Synchronous, None, )) } /// <https://dom.spec.whatwg.org/#dom-document-createelementns> fn CreateElementNS( &self, cx: &mut JSContext, namespace: Option<DOMString>, qualified_name: DOMString, options: StringOrElementCreationOptions, ) -> Fallible<DomRoot<Element>> { // Step 1. Let (namespace, prefix, localName) be the result of // validating and extracting namespace and qualifiedName given "element". let context = domname::Context::Element; let (namespace, prefix, local_name) = domname::validate_and_extract(namespace, &qualified_name, context)?; // Step 2. Let is be null. // Step 3. If options is a dictionary and options["is"] exists, then set is to it. let name = QualName::new(prefix, namespace, local_name); let is = match options { StringOrElementCreationOptions::String(_) => None, StringOrElementCreationOptions::ElementCreationOptions(options) => { options.is.as_ref().map(LocalName::from) }, }; // Step 4. Return the result of creating an element given document, localName, namespace, prefix, is, and true. Ok(Element::create( cx, name, is, self, ElementCreator::ScriptCreated, CustomElementCreationMode::Synchronous, None, )) } /// <https://dom.spec.whatwg.org/#dom-document-createattribute> fn CreateAttribute( &self, cx: &mut JSContext, mut local_name: DOMString, ) -> Fallible<DomRoot<Attr>> { // Step 1. If localName is not a valid attribute local name, // then throw an "InvalidCharacterError" DOMException if !is_valid_attribute_local_name(&local_name.str()) { debug!("Not a valid attribute name"); return Err(Error::InvalidCharacter(None)); } if self.is_html_document { local_name.make_ascii_lowercase(); } let name = LocalName::from(local_name); let value = AttrValue::String("".to_owned()); Ok(Attr::new( cx, self, name.clone(), value, name, ns!(), None, None, )) } /// <https://dom.spec.whatwg.org/#dom-document-createattributens> fn CreateAttributeNS( &self, cx: &mut JSContext, namespace: Option<DOMString>, qualified_name: DOMString, ) -> Fallible<DomRoot<Attr>> { // Step 1. Let (namespace, prefix, localName) be the result of validating and // extracting namespace and qualifiedName given "attribute". let context = domname::Context::Attribute; let (namespace, prefix, local_name) = domname::validate_and_extract(namespace, &qualified_name, context)?; let value = AttrValue::String("".to_owned()); let qualified_name = LocalName::from(qualified_name); Ok(Attr::new( cx, self, local_name, value, qualified_name, namespace, prefix, None, )) } /// <https://dom.spec.whatwg.org/#dom-document-createdocumentfragment> fn CreateDocumentFragment(&self, cx: &mut JSContext) -> DomRoot<DocumentFragment> { DocumentFragment::new(cx, self) } /// <https://dom.spec.whatwg.org/#dom-document-createtextnode> fn CreateTextNode(&self, cx: &mut JSContext, data: DOMString) -> DomRoot<Text> { Text::new(cx, data, self) } /// <https://dom.spec.whatwg.org/#dom-document-createcdatasection> fn CreateCDATASection( &self, cx: &mut JSContext, data: DOMString, ) -> Fallible<DomRoot<CDATASection>> { // Step 1 if self.is_html_document { return Err(Error::NotSupported(None)); } // Step 2 if data.contains("]]>") { return Err(Error::InvalidCharacter(None)); } // Step 3 Ok(CDATASection::new(cx, data, self)) } /// <https://dom.spec.whatwg.org/#dom-document-createcomment> fn CreateComment(&self, cx: &mut JSContext, data: DOMString) -> DomRoot<Comment> { Comment::new(cx, data, self, None) } /// <https://dom.spec.whatwg.org/#dom-document-createprocessinginstruction> fn CreateProcessingInstruction( &self, cx: &mut JSContext, target: DOMString, data: DOMString, ) -> Fallible<DomRoot<ProcessingInstruction>> { // Step 1. If target does not match the Name production, then throw an "InvalidCharacterError" DOMException. if !matches_name_production(&target.str()) { return Err(Error::InvalidCharacter(None)); } // Step 2. if data.contains("?>") { return Err(Error::InvalidCharacter(None)); } // Step 3. Ok(ProcessingInstruction::new(cx, target, data, self)) } /// <https://dom.spec.whatwg.org/#dom-document-importnode> fn ImportNode( &self, cx: &mut JSContext, node: &Node, options: BooleanOrImportNodeOptions, ) -> Fallible<DomRoot<Node>> { // Step 1. If node is a document or shadow root, then throw a "NotSupportedError" DOMException. if node.is::<Document>() || node.is::<ShadowRoot>() { return Err(Error::NotSupported(None)); } // Step 2. Let subtree be false. let (subtree, registry) = match options { // Step 3. Let registry be null. // Step 4. If options is a boolean, then set subtree to options. BooleanOrImportNodeOptions::Boolean(boolean) => (boolean.into(), None), // Step 5. Otherwise: BooleanOrImportNodeOptions::ImportNodeOptions(options) => { // Step 5.1. Set subtree to the negation of options["selfOnly"]. let subtree = (!options.selfOnly).into(); // Step 5.2. If options["customElementRegistry"] exists, then set registry to it. let registry = if let Some(registry) = options.customElementRegistry { // Step 5.3. If registry's is scoped is false and registry // is not this's custom element registry, then throw a "NotSupportedError" DOMException. let this_registry = self .custom_element_registry() .expect("Document must have a custom element registry"); if !registry.is_scoped() && registry != this_registry { return Err(Error::NotSupported(Some( "Imported customElementRegistry is not scoped and does not match existing registry.".into() ))); } Some(registry) } else { None }; (subtree, registry) }, }; // Step 6. If registry is null, then set registry to the // result of looking up a custom element registry given this. let registry = registry .or_else(|| CustomElementRegistry::lookup_a_custom_element_registry(self.upcast())); // Step 7. Return the result of cloning a node given node with // document set to this, subtree set to subtree, and fallbackRegistry set to registry. Ok(Node::clone(cx, node, Some(self), subtree, registry)) } /// <https://dom.spec.whatwg.org/#dom-document-adoptnode> fn AdoptNode(&self, cx: &mut JSContext, node: &Node) -> Fallible<DomRoot<Node>> { // Step 1. if node.is::<Document>() { return Err(Error::NotSupported(None)); } // Step 2. if node.is::<ShadowRoot>() { return Err(Error::HierarchyRequest(None)); } // Step 3. Node::adopt(cx, node, self); // Step 4. Ok(DomRoot::from_ref(node)) } /// <https://dom.spec.whatwg.org/#dom-document-createevent> fn CreateEvent( &self, cx: &mut JSContext, mut interface: DOMString, ) -> Fallible<DomRoot<Event>> { interface.make_ascii_lowercase(); match &*interface.str() { "beforeunloadevent" => Ok(DomRoot::upcast(BeforeUnloadEvent::new_uninitialized( cx, &self.window, ))), "compositionevent" | "textevent" => Ok(DomRoot::upcast( CompositionEvent::new_uninitialized(cx, &self.window), )), "customevent" => Ok(DomRoot::upcast(CustomEvent::new_uninitialized( cx, self.window.upcast(), ))), // FIXME(#25136): devicemotionevent, deviceorientationevent // FIXME(#7529): dragevent "events" | "event" | "htmlevents" | "svgevents" => { Ok(Event::new_uninitialized(cx, self.window.upcast())) }, "focusevent" => Ok(DomRoot::upcast(FocusEvent::new_uninitialized( cx, &self.window, ))), "hashchangeevent" => Ok(DomRoot::upcast(HashChangeEvent::new_uninitialized( cx, &self.window, ))), "keyboardevent" => Ok(DomRoot::upcast(KeyboardEvent::new_uninitialized( cx, &self.window, ))), "messageevent" => Ok(DomRoot::upcast(MessageEvent::new_uninitialized( cx, self.window.upcast(), ))), "mouseevent" | "mouseevents" => Ok(DomRoot::upcast(MouseEvent::new_uninitialized( cx, &self.window, ))), "storageevent" => Ok(DomRoot::upcast(StorageEvent::new_uninitialized( cx, &self.window, "".into(), ))), "touchevent" => { let touches = TouchList::new(cx, &self.window, &[]); let changed_touches = TouchList::new(cx, &self.window, &[]); let target_touches = TouchList::new(cx, &self.window, &[]); Ok(DomRoot::upcast(DomTouchEvent::new_uninitialized( cx, &self.window, &touches, &changed_touches, &target_touches, ))) }, "uievent" | "uievents" => Ok(DomRoot::upcast(UIEvent::new_uninitialized( cx, &self.window, ))), _ => Err(Error::NotSupported(None)), } } /// <https://html.spec.whatwg.org/multipage/#dom-document-lastmodified> fn LastModified(&self) -> DOMString { DOMString::from(self.last_modified.as_ref().cloned().unwrap_or_else(|| { // Ideally this would get the local time using `time`, but `time` always fails to get the local // timezone on Unix unless the application is single threaded unless the library is explicitly // set to "unsound" mode. Maybe that's fine, but it needs more investigation. see // https://nvd.nist.gov/vuln/detail/CVE-2020-26235 // When `time` supports a thread-safe way of getting the local time zone we could use it here. Local::now().format("%m/%d/%Y %H:%M:%S").to_string() })) } /// <https://dom.spec.whatwg.org/#dom-document-createrange> fn CreateRange(&self, cx: &mut JSContext) -> DomRoot<Range> { Range::new_with_doc(cx, self, None) } /// <https://dom.spec.whatwg.org/#dom-document-createnodeiteratorroot-whattoshow-filter> fn CreateNodeIterator( &self, cx: &mut js::context::JSContext, root: &Node, what_to_show: u32, filter: Option<Rc<NodeFilter>>, ) -> DomRoot<NodeIterator> { NodeIterator::new(cx, self, root, what_to_show, filter) } /// <https://dom.spec.whatwg.org/#dom-document-createtreewalker> fn CreateTreeWalker( &self, cx: &mut JSContext, root: &Node, what_to_show: u32, filter: Option<Rc<NodeFilter>>, ) -> DomRoot<TreeWalker> { TreeWalker::new(cx, self, root, what_to_show, filter) } /// <https://html.spec.whatwg.org/multipage/#document.title> fn Title(&self) -> DOMString { self.title().unwrap_or_else(|| DOMString::from("")) } /// <https://html.spec.whatwg.org/multipage/#document.title> fn SetTitle(&self, cx: &mut JSContext, title: DOMString) { let root = match self.GetDocumentElement() { Some(root) => root, None => return, }; let node = if root.namespace() == &ns!(svg) && root.local_name() == &local_name!("svg") { let elem = root .upcast::<Node>() .child_elements_unrooted(cx.no_gc()) .find(|node| { node.namespace() == &ns!(svg) && node.local_name() == &local_name!("title") }); match elem { Some(elem) => UnrootedDom::upcast::<Node>(elem).as_rooted(), None => { let name = QualName::new(None, ns!(svg), local_name!("title")); let elem = Element::create( cx, name, None, self, ElementCreator::ScriptCreated, CustomElementCreationMode::Synchronous, None, ); let parent = root.upcast::<Node>(); let child = elem.upcast::<Node>(); parent .InsertBefore(cx, child, parent.GetFirstChild().as_deref()) .unwrap() }, } } else if root.namespace() == &ns!(html) { let elem = root .upcast::<Node>() .traverse_preorder_non_rooting(cx.no_gc(), ShadowIncluding::No) .find(|node| node.is::<HTMLTitleElement>()); match elem { Some(elem) => elem.as_rooted(), None => match self.GetHead() { Some(head) => { let name = QualName::new(None, ns!(html), local_name!("title")); let elem = Element::create( cx, name, None, self, ElementCreator::ScriptCreated, CustomElementCreationMode::Synchronous, None, ); head.upcast::<Node>() .AppendChild(cx, elem.upcast()) .unwrap() }, None => return, }, } } else { return; }; node.set_text_content_for_element(cx, Some(title)); } /// <https://html.spec.whatwg.org/multipage/#dom-document-head> fn GetHead(&self) -> Option<DomRoot<HTMLHeadElement>> { self.get_html_element() .and_then(|root| root.upcast::<Node>().children().find_map(DomRoot::downcast)) } /// <https://html.spec.whatwg.org/multipage/#dom-document-currentscript> fn GetCurrentScript(&self) -> Option<DomRoot<HTMLScriptElement>> { self.current_script.get() } /// <https://html.spec.whatwg.org/multipage/#dom-document-body> fn GetBody(&self) -> Option<DomRoot<HTMLElement>> { // > The body element of a document is the first of the html element's children // > that is either a body element or a frameset element, or null if there is no such element. self.get_html_element().and_then(|root| { let node = root.upcast::<Node>(); node.children() .find(|child| { matches!( child.type_id(), NodeTypeId::Element(ElementTypeId::HTMLElement( HTMLElementTypeId::HTMLBodyElement, )) | NodeTypeId::Element(ElementTypeId::HTMLElement( HTMLElementTypeId::HTMLFrameSetElement, )) ) }) .map(|node| DomRoot::downcast(node).unwrap()) }) } /// <https://html.spec.whatwg.org/multipage/#dom-document-body> fn SetBody(&self, cx: &mut JSContext, new_body: Option<&HTMLElement>) -> ErrorResult { // Step 1. If the new value is not a body or frameset element, then throw a "HierarchyRequestError" DOMException. let new_body = match new_body { Some(new_body) => new_body, None => return Err(Error::HierarchyRequest(None)), }; let node = new_body.upcast::<Node>(); match node.type_id() { NodeTypeId::Element(ElementTypeId::HTMLElement(HTMLElementTypeId::HTMLBodyElement)) | NodeTypeId::Element(ElementTypeId::HTMLElement( HTMLElementTypeId::HTMLFrameSetElement, )) => {}, _ => return Err(Error::HierarchyRequest(None)), } // Step 2. Otherwise, if the new value is the same as the body element, return. let old_body = self.GetBody(); if old_body.as_deref() == Some(new_body) { return Ok(()); } match (self.GetDocumentElement(), &old_body) { // Step 3. Otherwise, if the body element is not null, // then replace the body element with the new value within the body element's parent and return. (Some(ref root), Some(child)) => { let root = root.upcast::<Node>(); root.ReplaceChild(cx, new_body.upcast(), child.upcast()) .map(|_| ()) }, // Step 4. Otherwise, if there is no document element, throw a "HierarchyRequestError" DOMException. (None, _) => Err(Error::HierarchyRequest(None)), // Step 5. Otherwise, the body element is null, but there's a document element. // Append the new value to the document element. (Some(ref root), &None) => { let root = root.upcast::<Node>(); root.AppendChild(cx, new_body.upcast()).map(|_| ()) }, } } /// <https://html.spec.whatwg.org/multipage/#dom-document-getelementsbyname> fn GetElementsByName(&self, cx: &mut JSContext, name: DOMString) -> DomRoot<NodeList> { NodeList::new_elements_by_name_list(cx, self.window(), self, name) } /// <https://html.spec.whatwg.org/multipage/#dom-document-images> fn Images(&self, cx: &mut JSContext) -> DomRoot<HTMLCollection> { self.images.or_init(|| { HTMLCollection::new_with_filter_fn(cx, &self.window, self.upcast(), |element, _| { element.is::<HTMLImageElement>() }) }) } /// <https://html.spec.whatwg.org/multipage/#dom-document-embeds> fn Embeds(&self, cx: &mut JSContext) -> DomRoot<HTMLCollection> { self.embeds.or_init(|| { HTMLCollection::new_with_filter_fn(cx, &self.window, self.upcast(), |element, _| { element.is::<HTMLEmbedElement>() }) }) } /// <https://html.spec.whatwg.org/multipage/#dom-document-plugins> fn Plugins(&self, cx: &mut JSContext) -> DomRoot<HTMLCollection> { self.Embeds(cx) } /// <https://html.spec.whatwg.org/multipage/#dom-document-links> fn Links(&self, cx: &mut JSContext) -> DomRoot<HTMLCollection> { self.links.or_init(|| { HTMLCollection::new_with_filter_fn(cx, &self.window, self.upcast(), |element, _| { (element.is::<HTMLAnchorElement>() || element.is::<HTMLAreaElement>()) && element.has_attribute(&local_name!("href")) }) }) } /// <https://html.spec.whatwg.org/multipage/#dom-document-forms> fn Forms(&self, cx: &mut JSContext) -> DomRoot<HTMLCollection> { self.forms.or_init(|| { HTMLCollection::new_with_filter_fn(cx, &self.window, self.upcast(), |element, _| { element.is::<HTMLFormElement>() }) }) } /// <https://html.spec.whatwg.org/multipage/#dom-document-scripts> fn Scripts(&self, cx: &mut JSContext) -> DomRoot<HTMLCollection> { self.scripts.or_init(|| { HTMLCollection::new_with_filter_fn(cx, &self.window, self.upcast(), |element, _| { element.is::<HTMLScriptElement>() }) }) } /// <https://html.spec.whatwg.org/multipage/#dom-document-anchors> fn Anchors(&self, cx: &mut JSContext) -> DomRoot<HTMLCollection> { self.anchors.or_init(|| { HTMLCollection::new_with_filter_fn(cx, &self.window, self.upcast(), |element, _| { element.is::<HTMLAnchorElement>() && element.has_attribute(&local_name!("href")) }) }) } /// <https://html.spec.whatwg.org/multipage/#dom-document-applets> fn Applets(&self, cx: &mut JSContext) -> DomRoot<HTMLCollection> { self.applets .or_init(|| HTMLCollection::always_empty(cx, &self.window, self.upcast())) } /// <https://html.spec.whatwg.org/multipage/#dom-document-location> fn GetLocation(&self, cx: &mut JSContext) -> Option<DomRoot<Location>> { if self.is_fully_active() { Some(self.window.Location(cx)) } else { None } } /// <https://dom.spec.whatwg.org/#dom-parentnode-children> fn Children(&self, cx: &mut JSContext) -> DomRoot<HTMLCollection> { HTMLCollection::children(cx, &self.window, self.upcast()) } /// <https://dom.spec.whatwg.org/#dom-parentnode-firstelementchild> fn GetFirstElementChild(&self) -> Option<DomRoot<Element>> { self.upcast::<Node>().child_elements().next() } /// <https://dom.spec.whatwg.org/#dom-parentnode-lastelementchild> fn GetLastElementChild(&self) -> Option<DomRoot<Element>> { self.upcast::<Node>() .rev_children() .find_map(DomRoot::downcast) } /// <https://dom.spec.whatwg.org/#dom-parentnode-childelementcount> fn ChildElementCount(&self) -> u32 { self.upcast::<Node>().child_elements().count() as u32 } /// <https://dom.spec.whatwg.org/#dom-parentnode-prepend> fn Prepend(&self, cx: &mut JSContext, nodes: Vec<NodeOrString>) -> ErrorResult { self.upcast::<Node>().prepend(cx, nodes) } /// <https://dom.spec.whatwg.org/#dom-parentnode-append> fn Append(&self, cx: &mut JSContext, nodes: Vec<NodeOrString>) -> ErrorResult { self.upcast::<Node>().append(cx, nodes) } /// <https://dom.spec.whatwg.org/#dom-parentnode-replacechildren> fn ReplaceChildren(&self, cx: &mut JSContext, nodes: Vec<NodeOrString>) -> ErrorResult { self.upcast::<Node>().replace_children(cx, nodes) } /// <https://dom.spec.whatwg.org/#dom-parentnode-movebefore> fn MoveBefore(&self, cx: &mut JSContext, node: &Node, child: Option<&Node>) -> ErrorResult { self.upcast::<Node>().move_before(cx, node, child) } /// <https://dom.spec.whatwg.org/#dom-parentnode-queryselector> fn QuerySelector( &self, cx: &mut JSContext, selectors: DOMString, ) -> Fallible<Option<DomRoot<Element>>> { self.upcast::<Node>().query_selector(cx.no_gc(), selectors) } /// <https://dom.spec.whatwg.org/#dom-parentnode-queryselectorall> fn QuerySelectorAll( &self, cx: &mut JSContext, selectors: DOMString, ) -> Fallible<DomRoot<NodeList>> { self.upcast::<Node>().query_selector_all(cx, selectors) } /// <https://html.spec.whatwg.org/multipage/#dom-document-readystate> fn ReadyState(&self) -> DocumentReadyState { self.ready_state.get() } /// <https://html.spec.whatwg.org/multipage/#dom-document-defaultview> fn GetDefaultView(&self) -> Option<DomRoot<Window>> { if self.has_browsing_context { Some(DomRoot::from_ref(&*self.window)) } else { None } } /// <https://html.spec.whatwg.org/multipage/#dom-document-cookie> fn GetCookie(&self) -> Fallible<DOMString> { if self.is_cookie_averse() { return Ok(DOMString::new()); } if !self.origin().is_tuple() { return Err(Error::Security(None)); } let url = self.url(); let (tx, rx) = profile_generic_channel::channel(self.global().time_profiler_chan().clone()).unwrap(); let _ = self .window .as_global_scope() .resource_threads() .send(GetCookieStringForUrl(url, tx, NonHTTP)); let cookies = rx.recv().unwrap(); Ok(cookies.map_or(DOMString::new(), DOMString::from)) } /// <https://html.spec.whatwg.org/multipage/#dom-document-cookie> fn SetCookie(&self, cookie: DOMString) -> ErrorResult { if self.is_cookie_averse() { return Ok(()); } if !self.origin().is_tuple() { return Err(Error::Security(None)); } if !cookie.is_valid_for_cookie() { return Ok(()); } let cookies = if let Some(cookie) = Cookie::parse(cookie.to_string()).ok().map(Serde) { vec![cookie] } else { vec![] }; let _ = self .window .as_global_scope() .resource_threads() .send(SetCookiesForUrl(self.url(), cookies, NonHTTP)); Ok(()) } /// <https://html.spec.whatwg.org/multipage/#dom-document-bgcolor> fn BgColor(&self) -> DOMString { self.get_body_attribute(&local_name!("bgcolor")) } /// <https://html.spec.whatwg.org/multipage/#dom-document-bgcolor> fn SetBgColor(&self, cx: &mut JSContext, value: DOMString) { self.set_body_attribute(cx, &local_name!("bgcolor"), value) } /// <https://html.spec.whatwg.org/multipage/#dom-document-fgcolor> fn FgColor(&self) -> DOMString { self.get_body_attribute(&local_name!("text")) } /// <https://html.spec.whatwg.org/multipage/#dom-document-fgcolor> fn SetFgColor(&self, cx: &mut JSContext, value: DOMString) { self.set_body_attribute(cx, &local_name!("text"), value) } /// <https://html.spec.whatwg.org/multipage/#dom-tree-accessors:dom-document-nameditem-filter> fn NamedGetter(&self, cx: &mut JSContext, name: DOMString) -> Option<NamedPropertyValue> { if name.is_empty() { return None; } let name = Atom::from(name); // Step 1. Let elements be the list of named elements with the name name that are in a document tree // with the Document as their root. let elements_with_name = self.get_elements_with_name(cx, &name); let name_iter = elements_with_name .iter() .filter(|elem| is_named_element_with_name_attribute(elem)); let elements_with_id = self.id_map.get_all(cx.no_gc(), self.upcast(), &name); let id_iter = elements_with_id .iter() .filter(|elem| is_named_element_with_id_attribute(elem)); let mut elements = name_iter.chain(id_iter); // Step 2. If elements has only one element, and that element is an iframe element, // and that iframe element's content navigable is not null, then return the active // WindowProxy of the element's content navigable. // NOTE: We have to check if all remaining elements are equal to the first, since // the same element may appear in both lists. let first = elements.next()?; if elements.all(|other| first == other) { if let Some(nested_window_proxy) = first .downcast::<HTMLIFrameElement>() .and_then(|iframe| iframe.GetContentWindow()) { return Some(NamedPropertyValue::WindowProxy(nested_window_proxy)); } // Step 3. Otherwise, if elements has only one element, return that element. return Some(NamedPropertyValue::Element(DomRoot::from_ref(first))); } // Step 4. Otherwise, return an HTMLCollection rooted at the Document node, // whose filter matches only named elements with the name name. #[derive(JSTraceable, MallocSizeOf)] struct DocumentNamedGetter { #[no_trace] name: Atom, } impl CollectionFilter for DocumentNamedGetter { fn filter(&self, elem: &Element, _root: &Node) -> bool { let type_ = match elem.upcast::<Node>().type_id() { NodeTypeId::Element(ElementTypeId::HTMLElement(type_)) => type_, _ => return false, }; match type_ { HTMLElementTypeId::HTMLFormElement | HTMLElementTypeId::HTMLIFrameElement => { elem.get_name().as_ref() == Some(&self.name) }, HTMLElementTypeId::HTMLImageElement => elem.get_name().is_some_and(|name| { name == *self.name || !name.is_empty() && elem.get_id().as_ref() == Some(&self.name) }), // TODO handle <embed> and <object>; these depend on whether the element is // “exposed”, a concept that doesn’t fully make sense until embed/object // behaviour is actually implemented _ => false, } } } let collection = HTMLCollection::create( cx, self.window(), self.upcast(), Box::new(DocumentNamedGetter { name }), ); Some(NamedPropertyValue::HTMLCollection(collection)) } /// <https://html.spec.whatwg.org/multipage/#dom-tree-accessors:supported-property-names> fn SupportedPropertyNames(&self, no_gc: &NoGC) -> Vec<DOMString> { let mut names_with_first_named_element_map = HashMap::new(); self.name_map .for_each(no_gc, self.upcast(), |name, elements| { if name.is_empty() { return; } let mut name_iter = elements .iter() .filter(|elem| is_named_element_with_name_attribute(elem)); if let Some(first) = name_iter.next() { names_with_first_named_element_map.insert(name.clone(), first.as_rooted()); } }); self.id_map.for_each(no_gc, self.upcast(), |id, elements| { if id.is_empty() { return; } let mut id_iter = elements .iter() .filter(|elem| is_named_element_with_id_attribute(elem)); if let Some(first) = id_iter.next() { match names_with_first_named_element_map.entry(id.clone()) { Vacant(entry) => drop(entry.insert(first.as_rooted())), Occupied(mut entry) => { if first.upcast::<Node>().is_before(entry.get().upcast()) { *entry.get_mut() = first.as_rooted(); } }, } } }); let mut names_with_first_named_element_vec: Vec<_> = names_with_first_named_element_map.into_iter().collect(); names_with_first_named_element_vec.sort_unstable_by(|a, b| { if a.1 == b.1 { // This can happen if an img has an id different from its name, // spec does not say which string to put first. a.0.cmp(&b.0) } else if a.1.upcast::<Node>().is_before(b.1.upcast::<Node>()) { Ordering::Less } else { Ordering::Greater } }); names_with_first_named_element_vec .into_iter() .map(|(k, _)| DOMString::from(&*k)) .collect() } /// <https://html.spec.whatwg.org/multipage/#dom-document-clear> fn Clear(&self) { // This method intentionally does nothing } /// <https://html.spec.whatwg.org/multipage/#dom-document-captureevents> fn CaptureEvents(&self) { // This method intentionally does nothing } /// <https://html.spec.whatwg.org/multipage/#dom-document-releaseevents> fn ReleaseEvents(&self) { // This method intentionally does nothing } // https://html.spec.whatwg.org/multipage/#globaleventhandlers global_event_handlers!(); // https://html.spec.whatwg.org/multipage/#handler-onreadystatechange event_handler!( readystatechange, GetOnreadystatechange, SetOnreadystatechange ); /// <https://drafts.csswg.org/cssom-view/#dom-document-elementfrompoint> fn ElementFromPoint(&self, x: Finite<f64>, y: Finite<f64>) -> Option<DomRoot<Element>> { self.document_or_shadow_root.element_from_point( self.upcast(), x, y, self.GetDocumentElement(), self.has_browsing_context, ) } /// <https://drafts.csswg.org/cssom-view/#dom-document-elementsfrompoint> fn ElementsFromPoint(&self, x: Finite<f64>, y: Finite<f64>) -> Vec<DomRoot<Element>> { self.document_or_shadow_root.elements_from_point( self.upcast(), x, y, self.GetDocumentElement(), self.has_browsing_context, ) } /// <https://drafts.csswg.org/cssom-view/#dom-document-scrollingelement> fn GetScrollingElement(&self) -> Option<DomRoot<Element>> { // Step 1. If the Document is in quirks mode, follow these steps: if self.quirks_mode() == QuirksMode::Quirks { // Step 1.1. If the body element exists, if let Some(ref body) = self.GetBody() { let e = body.upcast::<Element>(); // and it is not potentially scrollable, return the body element and abort these steps. // For this purpose, a value of overflow:clip on the body element’s parent element // must be treated as overflow:hidden. if !e.is_potentially_scrollable_body_for_scrolling_element() { return Some(DomRoot::from_ref(e)); } } // Step 1.2. Return null and abort these steps. return None; } // Step 2. If there is a root element, return the root element and abort these steps. // Step 3. Return null. self.GetDocumentElement() } /// <https://html.spec.whatwg.org/multipage/#dom-document-open> fn Open( &self, cx: &mut JSContext, _unused1: Option<DOMString>, _unused2: Option<DOMString>, ) -> Fallible<DomRoot<Document>> { // Step 1. If document is an XML document, then throw an "InvalidStateError" DOMException. if !self.is_html_document() { return Err(Error::InvalidState(None)); } // Step 2. If document's throw-on-dynamic-markup-insertion counter is greater than 0, // then throw an "InvalidStateError" DOMException. if self.throw_on_dynamic_markup_insertion_counter.get() > 0 { return Err(Error::InvalidState(None)); } // Step 3. Let entryDocument be the entry global object's associated Document. let entry_responsible_document = GlobalScope::entry().as_window().Document(); // Step 4. If document's origin is not same origin to entryDocument's origin, // then throw a "SecurityError" DOMException. if !self .origin() .same_origin(&entry_responsible_document.origin()) { return Err(Error::Security(None)); } // Step 5. If document has an active parser whose script nesting level is greater than 0, // then return document. if self .active_parser() .is_some_and(|parser| parser.script_nesting_level() > 0) { return Ok(DomRoot::from_ref(self)); } // Step 6. Similarly, if document's unload counter is greater than 0, then return document. if self.is_prompting_or_unloading() { return Ok(DomRoot::from_ref(self)); } // Step 7. If document's active parser was aborted is true, then return document. if self.active_parser_was_aborted.get() { return Ok(DomRoot::from_ref(self)); } // TODO: prompt to unload. // TODO: set unload_event_start and unload_event_end self.window().set_navigation_start(); // Step 8. If document's node navigable is non-null and document's node navigable's // ongoing navigation is a navigation ID, then stop loading document's node navigable. // TODO: https://github.com/servo/servo/issues/21937 if self.has_browsing_context() { // spec says "stop document loading", // which is a process that does more than just abort self.abort(cx); } // Step 9. For each shadow-including inclusive descendant node of document, // erase all event listeners and handlers given node. for node in self .upcast::<Node>() .traverse_preorder_non_rooting(cx.no_gc(), ShadowIncluding::Yes) { node.upcast::<EventTarget>().remove_all_listeners(); } // Step 10. If document is the associated Document of document's relevant global object, // then erase all event listeners and handlers given document's relevant global object. if self.window.Document() == DomRoot::from_ref(self) { self.window.upcast::<EventTarget>().remove_all_listeners(); } // Step 11. Replace all with null within document. Node::replace_all(cx, None, self.upcast::<Node>()); // Specs and tests are in a state of flux about whether // we want to clear the selection when we remove the contents; // WPT selection/Document-open.html wants us to not clear it // as of Feb 1 2020 // Step 12. If document is fully active, then: if self.is_fully_active() { // Step 12.1. Let newURL be a copy of entryDocument's URL. let mut new_url = entry_responsible_document.url(); // Step 12.2. If entryDocument is not document, then set newURL's fragment to null. if entry_responsible_document != DomRoot::from_ref(self) { new_url.set_fragment(None); } // Step 12.3. Run the URL and history update steps with document and newURL. // TODO: https://github.com/servo/servo/issues/21939 self.set_url(new_url); } // Step 13. Set document's is initial about:blank to false. self.is_initial_about_blank.set(false); // Step 14. If document's iframe load in progress flag is set, then set document's mute // iframe load flag. if self.iframe_load_in_progress.get() { self.mute_iframe_load.set(true); } // Step 15: Set document to no-quirks mode. self.set_quirks_mode(QuirksMode::NoQuirks); // Step 16. Create a new HTML parser and associate it with document. This is a // script-created parser (meaning that it can be closed by the document.open() and // document.close() methods, and that the tokenizer will wait for an explicit call to // document.close() before emitting an end-of-file token). The encoding confidence is // irrelevant. let resource_threads = self.window.as_global_scope().resource_threads().clone(); *self.loader.borrow_mut() = DocumentLoader::new_with_threads(resource_threads, Some(self.url())); ServoParser::parse_html_script_input(cx, self, self.url()); // Step 17. Set the insertion point to point at just before the end of the input stream // (which at this point will be empty). // Handled when creating the parser in step 16 // Step 18. Update the current document readiness of document to "loading". self.ready_state.set(DocumentReadyState::Loading); // Step 19. Return document. Ok(DomRoot::from_ref(self)) } /// <https://html.spec.whatwg.org/multipage/#dom-document-open-window> fn Open_( &self, cx: &mut JSContext, url: USVString, target: DOMString, features: DOMString, ) -> Fallible<Option<DomRoot<WindowProxy>>> { self.browsing_context() .ok_or(Error::InvalidAccess(None))? .open(cx, url, target, features) } /// <https://html.spec.whatwg.org/multipage/#dom-document-write> fn Write(&self, cx: &mut JSContext, text: Vec<TrustedHTMLOrString>) -> ErrorResult { // The document.write(...text) method steps are to run the document write steps // with this, text, false, and "Document write". self.write(cx, text, false, "Document", "write") } /// <https://html.spec.whatwg.org/multipage/#dom-document-writeln> fn Writeln(&self, cx: &mut JSContext, text: Vec<TrustedHTMLOrString>) -> ErrorResult { // The document.writeln(...text) method steps are to run the document write steps // with this, text, true, and "Document writeln". self.write(cx, text, true, "Document", "writeln") } /// <https://html.spec.whatwg.org/multipage/#dom-document-close> fn Close(&self, cx: &mut JSContext) -> ErrorResult { if !self.is_html_document() { // Step 1. If this is an XML document, then throw an "InvalidStateError" DOMException. return Err(Error::InvalidState(None)); } // Step 2. If this's throw-on-dynamic-markup-insertion counter is greater than zero, // then throw an "InvalidStateError" DOMException. if self.throw_on_dynamic_markup_insertion_counter.get() > 0 { return Err(Error::InvalidState(None)); } // Step 3. If there is no script-created parser associated with this, then return. let parser = match self.get_current_parser() { Some(ref parser) if parser.is_script_created() => DomRoot::from_ref(&**parser), _ => { return Ok(()); }, }; // parser.close implements the remainder of this algorithm parser.close(cx); Ok(()) } /// <https://w3c.github.io/editing/docs/execCommand/#execcommand()> fn ExecCommand( &self, cx: &mut JSContext, command_id: DOMString, _show_ui: bool, value: TrustedHTMLOrString, ) -> Fallible<bool> { let value = if command_id == "insertHTML" { TrustedHTML::get_trusted_type_compliant_string( cx, self.window.as_global_scope(), value, "Document execCommand", )? } else { match value { TrustedHTMLOrString::TrustedHTML(trusted_html) => trusted_html.data().clone(), TrustedHTMLOrString::String(value) => value, } }; Ok(self.exec_command_for_command_id(cx, command_id, value)) } /// <https://w3c.github.io/editing/docs/execCommand/#querycommandenabled()> fn QueryCommandEnabled(&self, cx: &mut JSContext, command_id: DOMString) -> bool { // Step 2. Return true if command is both supported and enabled, false otherwise. self.check_support_and_enabled(cx, &command_id).is_some() } /// <https://w3c.github.io/editing/docs/execCommand/#querycommandsupported()> fn QueryCommandSupported(&self, command_id: DOMString) -> bool { // > When the queryCommandSupported(command) method on the Document interface is invoked, // the user agent must return true if command is supported and available // within the current script on the current site, and false otherwise. self.is_command_supported(command_id) } /// <https://w3c.github.io/editing/docs/execCommand/#querycommandindeterm()> fn QueryCommandIndeterm(&self, cx: &mut JSContext, command_id: DOMString) -> bool { self.is_command_indeterminate(cx, command_id) } /// <https://w3c.github.io/editing/docs/execCommand/#querycommandstate()> fn QueryCommandState(&self, cx: &mut JSContext, command_id: DOMString) -> bool { self.command_state_for_command(cx, command_id) } /// <https://w3c.github.io/editing/docs/execCommand/#querycommandvalue()> fn QueryCommandValue(&self, cx: &mut JSContext, command_id: DOMString) -> DOMString { self.command_value_for_command(cx, command_id) } // https://fullscreen.spec.whatwg.org/#handler-document-onfullscreenerror event_handler!(fullscreenerror, GetOnfullscreenerror, SetOnfullscreenerror); // https://fullscreen.spec.whatwg.org/#handler-document-onfullscreenchange event_handler!( fullscreenchange, GetOnfullscreenchange, SetOnfullscreenchange ); /// <https://fullscreen.spec.whatwg.org/#dom-document-fullscreenenabled> fn FullscreenEnabled(&self) -> bool { self.get_allow_fullscreen() } /// <https://fullscreen.spec.whatwg.org/#dom-document-fullscreen> fn Fullscreen(&self) -> bool { self.fullscreen_element.get().is_some() } /// <https://fullscreen.spec.whatwg.org/#dom-document-fullscreenelement> fn GetFullscreenElement(&self) -> Option<DomRoot<Element>> { DocumentOrShadowRoot::get_fullscreen_element(&self.node, self.fullscreen_element.get()) } /// <https://fullscreen.spec.whatwg.org/#dom-document-exitfullscreen> fn ExitFullscreen(&self, cx: &mut CurrentRealm) -> Rc<Promise> { self.exit_fullscreen(cx) } // check-tidy: no specs after this line // Servo only API to get an instance of the controls of a specific // media element matching the given id. fn ServoGetMediaControls(&self, id: DOMString) -> Fallible<DomRoot<ShadowRoot>> { match self.media_controls.borrow().get(&*id.str()) { Some(m) => Ok(DomRoot::from_ref(m)), None => Err(Error::InvalidAccess(None)), } } /// <https://w3c.github.io/selection-api/#dom-document-getselection> fn GetSelection(&self, cx: &mut JSContext) -> Option<DomRoot<Selection>> { if self.has_browsing_context { Some(self.selection.or_init(|| Selection::new(cx, self))) } else { None } } /// <https://drafts.csswg.org/css-font-loading/#font-face-source> fn Fonts(&self, cx: &mut JSContext) -> DomRoot<FontFaceSet> { self.fonts .or_init(|| FontFaceSet::new(cx, &self.global(), None)) } /// <https://html.spec.whatwg.org/multipage/#dom-document-hidden> fn Hidden(&self) -> bool { self.visibility_state.get() == DocumentVisibilityState::Hidden } /// <https://html.spec.whatwg.org/multipage/#dom-document-visibilitystate> fn VisibilityState(&self) -> DocumentVisibilityState { self.visibility_state.get() } fn CreateExpression( &self, cx: &mut JSContext, expression: DOMString, resolver: Option<Rc<XPathNSResolver>>, ) -> Fallible<DomRoot<crate::dom::types::XPathExpression>> { let parsed_expression = parse_expression(cx, &expression.str(), resolver, self.is_html_document())?; Ok(XPathExpression::new( cx, &self.window, None, parsed_expression, )) } fn CreateNSResolver(&self, cx: &mut JSContext, node_resolver: &Node) -> DomRoot<Node> { let global = self.global(); let window = global.as_window(); let evaluator = XPathEvaluator::new(cx, window, None); XPathEvaluatorMethods::<crate::DomTypeHolder>::CreateNSResolver(&*evaluator, node_resolver) } fn Evaluate( &self, cx: &mut JSContext, expression: DOMString, context_node: &Node, resolver: Option<Rc<XPathNSResolver>>, result_type: u16, result: Option<&crate::dom::types::XPathResult>, ) -> Fallible<DomRoot<crate::dom::types::XPathResult>> { let parsed_expression = parse_expression(cx, &expression.str(), resolver, self.is_html_document())?; XPathExpression::new(cx, &self.window, None, parsed_expression).evaluate_internal( cx, context_node, result_type, result, ) } /// <https://drafts.csswg.org/cssom/#dom-documentorshadowroot-adoptedstylesheets> fn AdoptedStyleSheets(&self, cx: &mut JSContext, retval: MutableHandleValue) { self.adopted_stylesheets_frozen_types.get_or_init( cx, || { self.adopted_stylesheets .borrow() .clone() .iter() .map(|sheet| sheet.as_rooted()) .collect() }, retval, ); } /// <https://drafts.csswg.org/cssom/#dom-documentorshadowroot-adoptedstylesheets> fn SetAdoptedStyleSheets(&self, cx: &mut JSContext, val: HandleValue) -> ErrorResult { let result = DocumentOrShadowRoot::set_adopted_stylesheet_from_jsval( cx, &self.adopted_stylesheets, val, &StyleSheetListOwner::Document(Dom::from_ref(self)), ); if result.is_ok() { self.adopted_stylesheets_frozen_types.clear() } result } fn Timeline(&self) -> DomRoot<DocumentTimeline> { self.timeline.as_rooted() } } fn update_with_current_instant(marker: &Cell<Option<CrossProcessInstant>>) { if marker.get().is_none() { marker.set(Some(CrossProcessInstant::now())) } } #[derive(JSTraceable, MallocSizeOf)] pub(crate) enum AnimationFrameCallback { DevtoolsFramerateTick { actor_name: String, }, FrameRequestCallback { #[conditional_malloc_size_of] callback: Rc<FrameRequestCallback>, }, } impl AnimationFrameCallback { fn call(&self, cx: &mut JSContext, document: &Document, now: f64) { match *self { AnimationFrameCallback::DevtoolsFramerateTick { ref actor_name } => { let msg = ScriptToDevtoolsControlMsg::FramerateTick(actor_name.clone(), now); let devtools_sender = document.window().as_global_scope().devtools_chan().unwrap(); devtools_sender.send(msg).unwrap(); }, AnimationFrameCallback::FrameRequestCallback { ref callback } => { // TODO(jdm): The spec says that any exceptions should be suppressed: // https://github.com/servo/servo/issues/6928 let _ = callback.Call__(cx, Finite::wrap(now), ExceptionHandling::Report); }, } } } #[derive(Default, JSTraceable, MallocSizeOf)] #[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)] struct PendingInOrderScriptVec { scripts: DomRefCell<VecDeque<PendingScript>>, } impl PendingInOrderScriptVec { fn is_empty(&self) -> bool { self.scripts.borrow().is_empty() } fn push(&self, element: &HTMLScriptElement) { self.scripts .borrow_mut() .push_back(PendingScript::new(element)); } fn loaded(&self, element: &HTMLScriptElement, result: ScriptResult) { let mut scripts = self.scripts.borrow_mut(); let entry = scripts .iter_mut() .find(|entry| &*entry.element == element) .unwrap(); entry.loaded(result); } fn take_next_ready_to_be_executed(&self) -> Option<(DomRoot<HTMLScriptElement>, ScriptResult)> { let mut scripts = self.scripts.borrow_mut(); let pair = scripts.front_mut()?.take_result()?; scripts.pop_front(); Some(pair) } fn clear(&self) { *self.scripts.borrow_mut() = Default::default(); } } #[derive(JSTraceable, MallocSizeOf)] #[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)] struct PendingScript { element: Dom<HTMLScriptElement>, // TODO(sagudev): could this be all no_trace? load: Option<ScriptResult>, } impl PendingScript { fn new(element: &HTMLScriptElement) -> Self { Self { element: Dom::from_ref(element), load: None, } } fn new_with_load(element: &HTMLScriptElement, load: Option<ScriptResult>) -> Self { Self { element: Dom::from_ref(element), load, } } fn loaded(&mut self, result: ScriptResult) { assert!(self.load.is_none()); self.load = Some(result); } fn take_result(&mut self) -> Option<(DomRoot<HTMLScriptElement>, ScriptResult)> { self.load .take() .map(|result| (DomRoot::from_ref(&*self.element), result)) } } fn is_named_element_with_name_attribute(elem: &Element) -> bool { let type_ = match elem.upcast::<Node>().type_id() { NodeTypeId::Element(ElementTypeId::HTMLElement(type_)) => type_, _ => return false, }; match type_ { HTMLElementTypeId::HTMLFormElement | HTMLElementTypeId::HTMLIFrameElement | HTMLElementTypeId::HTMLImageElement => true, // TODO handle <embed> and <object>; these depend on whether the element is // “exposed”, a concept that doesn’t fully make sense until embed/object // behaviour is actually implemented _ => false, } } fn is_named_element_with_id_attribute(elem: &Element) -> bool { // TODO handle <embed> and <object>; these depend on whether the element is // “exposed”, a concept that doesn’t fully make sense until embed/object // behaviour is actually implemented elem.is::<HTMLImageElement>() && elem.get_name().is_some_and(|name| !name.is_empty()) } impl DocumentHelpers for Document { fn ensure_safe_to_run_script_or_layout(&self) { Document::ensure_safe_to_run_script_or_layout(self) } } /// Iterator for same origin ancestor navigables, returning the active documents of the navigables. /// <https://html.spec.whatwg.org/multipage/#ancestor-navigables> // TODO: Find a way for something equivalent for cross origin document. pub(crate) struct SameoriginAncestorNavigablesIterator { document: DomRoot<Document>, } impl SameoriginAncestorNavigablesIterator { pub(crate) fn new(document: DomRoot<Document>) -> Self { Self { document } } } impl Iterator for SameoriginAncestorNavigablesIterator { type Item = DomRoot<Document>; fn next(&mut self) -> Option<Self::Item> { let window_proxy = self.document.browsing_context()?; self.document = window_proxy.parent()?.document()?; Some(self.document.clone()) } } /// Iterator for same origin descendant navigables in a shadow-including tree order, returning the /// active documents of the navigables. /// <https://html.spec.whatwg.org/multipage/#descendant-navigables> // TODO: Find a way for something equivalent for cross origin document. pub(crate) struct SameOriginDescendantNavigablesIterator { stack: Vec<Box<dyn Iterator<Item = DomRoot<HTMLIFrameElement>>>>, } impl SameOriginDescendantNavigablesIterator { pub(crate) fn new(document: &Document) -> Self { let iframes: Vec<DomRoot<HTMLIFrameElement>> = document.iframes().iter().collect(); Self { stack: vec![Box::new(iframes.into_iter())], } } fn get_next_iframe(&mut self) -> Option<DomRoot<HTMLIFrameElement>> { let mut cur_iframe = self.stack.last_mut()?.next(); while cur_iframe.is_none() { self.stack.pop(); cur_iframe = self.stack.last_mut()?.next(); } cur_iframe } } impl Iterator for SameOriginDescendantNavigablesIterator { type Item = DomRoot<Document>; fn next(&mut self) -> Option<Self::Item> { while let Some(iframe) = self.get_next_iframe() { let Some(pipeline_id) = iframe.pipeline_id() else { continue; }; if let Some(document) = ScriptThread::find_document(pipeline_id) { let child_iframes: Vec<DomRoot<HTMLIFrameElement>> = document.iframes().iter().collect(); self.stack.push(Box::new(child_iframes.into_iter())); return Some(document); } else { continue; }; } None } }