/* 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::borrow::Cow; 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::f64::consts::PI; use std::mem; use std::rc::Rc; use std::slice::from_ref; use std::str::FromStr; use std::sync::{LazyLock, Mutex}; use std::time::{Duration, Instant}; use base::cross_process_instant::CrossProcessInstant; use base::id::WebViewId; use canvas_traits::canvas::CanvasId; use canvas_traits::webgl::{self, WebGLContextId, WebGLMsg}; use chrono::Local; use constellation_traits::{NavigationHistoryBehavior, ScriptToConstellationMessage}; use content_security_policy::{self as csp, CspList, PolicyDisposition}; use cookie::Cookie; use cssparser::match_ignore_ascii_case; use data_url::mime::Mime; use devtools_traits::ScriptToDevtoolsControlMsg; use dom_struct::dom_struct; use embedder_traits::{ AllowOrDeny, AnimationState, CompositorHitTestResult, ContextMenuResult, EditingActionEvent, EmbedderMsg, FocusSequenceNumber, ImeEvent, InputEvent, LoadStatus, MouseButton, MouseButtonAction, MouseButtonEvent, ScrollEvent, TouchEvent, TouchEventType, TouchId, UntrustedNodeAddress, WheelEvent, }; use encoding_rs::{Encoding, UTF_8}; use euclid::default::{Point2D, Rect, Size2D}; use html5ever::{LocalName, Namespace, QualName, local_name, ns}; use hyper_serde::Serde; use ipc_channel::ipc; use js::rust::{HandleObject, HandleValue}; use keyboard_types::{Code, Key, KeyState, Modifiers}; use metrics::{InteractiveFlag, InteractiveWindow, ProgressiveWebMetrics}; use net_traits::CookieSource::NonHTTP; use net_traits::CoreResourceMsg::{GetCookiesForUrl, SetCookiesForUrl}; use net_traits::policy_container::PolicyContainer; use net_traits::pub_domains::is_pub_domain; use net_traits::request::{InsecureRequestsPolicy, RequestBuilder}; use net_traits::response::HttpsState; use net_traits::{FetchResponseListener, IpcSend, ReferrerPolicy}; use num_traits::ToPrimitive; use percent_encoding::percent_decode; use profile_traits::ipc as profile_ipc; use profile_traits::time::TimerMetadataFrameType; use regex::bytes::Regex; use script_bindings::interfaces::DocumentHelpers; use script_layout_interface::{PendingRestyle, TrustedNodeAddress, node_id_from_scroll_id}; use script_traits::{ConstellationInputEvent, DocumentActivity, ProgressiveWebMetricType}; use servo_arc::Arc; use servo_config::pref; use servo_media::{ClientContextId, ServoMedia}; use servo_url::{ImmutableOrigin, MutableOrigin, ServoUrl}; use style::attr::AttrValue; use style::context::QuirksMode; use style::invalidation::element::restyle_hints::RestyleHint; use style::selector_parser::Snapshot; use style::shared_lock::SharedRwLock as StyleSharedRwLock; use style::str::{split_html_space_chars, str_join}; use style::stylesheet_set::DocumentStylesheetSet; use style::stylesheets::{Origin, OriginSet, Stylesheet}; use stylo_atoms::Atom; use url::Host; use uuid::Uuid; #[cfg(feature = "webgpu")] use webgpu_traits::WebGPUContextId; use webrender_api::units::DeviceIntRect; use crate::animation_timeline::AnimationTimeline; use crate::animations::Animations; use crate::canvas_context::CanvasContext as _; use crate::document_loader::{DocumentLoader, LoadType}; use crate::dom::attr::Attr; use crate::dom::beforeunloadevent::BeforeUnloadEvent; use crate::dom::bindings::callback::ExceptionHandling; use crate::dom::bindings::cell::{DomRefCell, Ref, RefMut}; 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::EventBinding::Event_Binding::EventMethods; use crate::dom::bindings::codegen::Bindings::HTMLIFrameElementBinding::HTMLIFrameElement_Binding::HTMLIFrameElementMethods; use crate::dom::bindings::codegen::Bindings::HTMLInputElementBinding::HTMLInputElementMethods; use crate::dom::bindings::codegen::Bindings::HTMLTextAreaElementBinding::HTMLTextAreaElementMethods; 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::ShadowRootBinding::ShadowRootMethods; use crate::dom::bindings::codegen::Bindings::TouchBinding::TouchMethods; use crate::dom::bindings::codegen::Bindings::WindowBinding::{ FrameRequestCallback, ScrollBehavior, WindowMethods, }; use crate::dom::bindings::codegen::Bindings::XPathEvaluatorBinding::XPathEvaluatorMethods; use crate::dom::bindings::codegen::Bindings::XPathNSResolverBinding::XPathNSResolver; use crate::dom::bindings::codegen::UnionTypes::{ NodeOrString, StringOrElementCreationOptions, TrustedHTMLOrString, }; use crate::dom::bindings::error::{Error, ErrorInfo, ErrorResult, Fallible}; use crate::dom::bindings::inheritance::{Castable, ElementTypeId, HTMLElementTypeId, NodeTypeId}; use crate::dom::bindings::num::Finite; use crate::dom::bindings::refcounted::{Trusted, TrustedPromise}; use crate::dom::bindings::reflector::{DomGlobal, reflect_dom_object_with_proto}; use crate::dom::bindings::root::{Dom, DomRoot, DomSlice, LayoutDom, MutNullableDom, ToLayout}; use crate::dom::bindings::str::{DOMString, USVString}; use crate::dom::bindings::trace::{HashMapTracedValues, NoTrace}; #[cfg(feature = "webgpu")] use crate::dom::bindings::weakref::WeakRef; use crate::dom::bindings::xmlname::{ matches_name_production, namespace_from_domstring, validate_and_extract, }; use crate::dom::canvasrenderingcontext2d::CanvasRenderingContext2D; use crate::dom::cdatasection::CDATASection; use crate::dom::clipboardevent::{ClipboardEvent, ClipboardEventType}; use crate::dom::comment::Comment; use crate::dom::compositionevent::CompositionEvent; use crate::dom::cssstylesheet::CSSStyleSheet; use crate::dom::customelementregistry::CustomElementDefinition; use crate::dom::customevent::CustomEvent; use crate::dom::datatransfer::DataTransfer; use crate::dom::documentfragment::DocumentFragment; use crate::dom::documentorshadowroot::{DocumentOrShadowRoot, StyleSheetInDocument}; use crate::dom::documenttype::DocumentType; use crate::dom::domimplementation::DOMImplementation; use crate::dom::element::{ CustomElementCreationMode, Element, ElementCreator, ElementPerformFullscreenEnter, ElementPerformFullscreenExit, }; use crate::dom::event::{Event, EventBubbles, EventCancelable, EventDefault, EventStatus}; use crate::dom::eventtarget::EventTarget; use crate::dom::focusevent::FocusEvent; use crate::dom::fontfaceset::FontFaceSet; use crate::dom::globalscope::GlobalScope; use crate::dom::hashchangeevent::HashChangeEvent; use crate::dom::htmlanchorelement::HTMLAnchorElement; use crate::dom::htmlareaelement::HTMLAreaElement; use crate::dom::htmlbaseelement::HTMLBaseElement; use crate::dom::htmlcollection::{CollectionFilter, HTMLCollection}; use crate::dom::htmlelement::HTMLElement; use crate::dom::htmlembedelement::HTMLEmbedElement; use crate::dom::htmlformelement::{FormControl, FormControlElementHelpers, HTMLFormElement}; use crate::dom::htmlheadelement::HTMLHeadElement; use crate::dom::htmlhtmlelement::HTMLHtmlElement; use crate::dom::htmliframeelement::HTMLIFrameElement; use crate::dom::htmlimageelement::HTMLImageElement; use crate::dom::htmlinputelement::HTMLInputElement; use crate::dom::htmlscriptelement::{HTMLScriptElement, ScriptResult}; use crate::dom::htmltextareaelement::HTMLTextAreaElement; use crate::dom::htmltitleelement::HTMLTitleElement; use crate::dom::intersectionobserver::IntersectionObserver; use crate::dom::keyboardevent::KeyboardEvent; use crate::dom::location::{Location, NavigationType}; use crate::dom::messageevent::MessageEvent; use crate::dom::mouseevent::MouseEvent; use crate::dom::node::{ self, CloneChildrenFlag, Node, NodeDamage, NodeFlags, NodeTraits, ShadowIncluding, }; use crate::dom::nodeiterator::NodeIterator; use crate::dom::nodelist::NodeList; use crate::dom::pagetransitionevent::PageTransitionEvent; use crate::dom::performanceentry::PerformanceEntry; use crate::dom::performancepainttiming::PerformancePaintTiming; use crate::dom::pointerevent::{PointerEvent, PointerId}; use crate::dom::processinginstruction::ProcessingInstruction; use crate::dom::promise::Promise; use crate::dom::range::Range; use crate::dom::resizeobserver::{ResizeObservationDepth, ResizeObserver}; use crate::dom::selection::Selection; use crate::dom::servoparser::ServoParser; use crate::dom::shadowroot::ShadowRoot; use crate::dom::storageevent::StorageEvent; use crate::dom::stylesheetlist::{StyleSheetList, StyleSheetListOwner}; use crate::dom::text::Text; use crate::dom::touch::Touch; use crate::dom::touchevent::TouchEvent as DomTouchEvent; use crate::dom::touchlist::TouchList; use crate::dom::treewalker::TreeWalker; use crate::dom::trustedhtml::TrustedHTML; use crate::dom::types::VisibilityStateEntry; use crate::dom::uievent::UIEvent; use crate::dom::virtualmethods::vtable_for; use crate::dom::webglrenderingcontext::WebGLRenderingContext; #[cfg(feature = "webgpu")] use crate::dom::webgpu::gpucanvascontext::GPUCanvasContext; use crate::dom::wheelevent::WheelEvent as DomWheelEvent; use crate::dom::window::Window; use crate::dom::windowproxy::WindowProxy; use crate::dom::xpathevaluator::XPathEvaluator; use crate::drag_data_store::{DragDataStore, Kind, Mode}; use crate::fetch::FetchCanceller; use crate::iframe_collection::IFrameCollection; use crate::image_animation::ImageAnimationManager; use crate::messaging::{CommonScriptMsg, MainThreadScriptMsg}; use crate::mime::{APPLICATION, CHARSET, MimeExt}; use crate::network_listener::{NetworkListener, PreInvoke}; use crate::realms::{AlreadyInRealm, InRealm, enter_realm}; use crate::script_runtime::{CanGc, ScriptThreadEventCategory}; use crate::script_thread::{ScriptThread, with_script_thread}; use crate::stylesheet_set::StylesheetSetRef; use crate::task::TaskBox; use crate::task_source::TaskSourceName; use crate::timers::OneshotTimerCallback; pub(crate) enum TouchEventResult { Processed(bool), Forwarded, } #[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, #[ignore_malloc_size_of = "non-owning"] pub(crate) window: DomRoot<Window>, } impl RefreshRedirectDue { pub(crate) fn invoke(self, can_gc: CanGc) { self.window.Location().navigate( self.url.clone(), NavigationHistoryBehavior::Replace, NavigationType::DeclarativeRefresh, can_gc, ); } } #[derive(Clone, Copy, Debug, JSTraceable, MallocSizeOf, PartialEq)] pub(crate) enum IsHTMLDocument { HTMLDocument, NonHTMLDocument, } #[derive(JSTraceable, MallocSizeOf)] #[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)] struct FocusTransaction { /// The focused element of this document. element: Option<Dom<Element>>, /// See [`Document::has_focus`]. has_focus: bool, } /// Information about a declarative refresh #[derive(JSTraceable, MallocSizeOf)] pub(crate) enum DeclarativeRefresh { PendingLoad { #[no_trace] url: ServoUrl, time: u64, }, CreatedAfterLoad, } #[cfg(feature = "webgpu")] pub(crate) type WebGPUContextsMap = Rc<RefCell<HashMapTracedValues<WebGPUContextId, WeakRef<GPUCanvasContext>>>>; /// <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>, #[no_trace] url: DomRefCell<ServoUrl>, #[ignore_malloc_size_of = "defined in selectors"] #[no_trace] quirks_mode: Cell<QuirksMode>, /// Caches for the getElement methods id_map: DomRefCell<HashMapTracedValues<Atom, Vec<Dom<Element>>>>, name_map: DomRefCell<HashMapTracedValues<Atom, Vec<Dom<Element>>>>, tag_map: DomRefCell<HashMapTracedValues<LocalName, Dom<HTMLCollection>>>, tagns_map: DomRefCell<HashMapTracedValues<QualName, Dom<HTMLCollection>>>, 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>, /// Lock use for style attributes and author-origin stylesheet objects in this document. /// Can be acquired once for accessing many objects. #[no_trace] style_shared_lock: StyleSharedRwLock, /// List of stylesheets associated with nodes in this document. |None| if the list needs to be refreshed. #[custom_trace] stylesheets: DomRefCell<DocumentStylesheetSet<StyleSheetInDocument>>, stylesheet_list: MutNullableDom<StyleSheetList>, ready_state: Cell<DocumentReadyState>, /// Whether the DOMContentLoaded event has already been dispatched. domcontentloaded_dispatched: Cell<bool>, /// The state of this document's focus transaction. focus_transaction: DomRefCell<Option<FocusTransaction>>, /// The element that currently has the document focus context. focused: MutNullableDom<Element>, /// The last sequence number sent to the constellation. #[no_trace] focus_sequence: Cell<FocusSequenceNumber>, /// Indicates whether the container is included in the top-level browsing /// context's focus chain (not considering system focus). Permanently `true` /// for a top-level document. has_focus: Cell<bool>, /// The script element that is currently executing. current_script: MutNullableDom<HTMLScriptElement>, /// <https://html.spec.whatwg.org/multipage/#pending-parsing-blocking-script> pending_parsing_blocking_script: DomRefCell<Option<PendingScript>>, /// Number of stylesheets that block executing the next parser-inserted script script_blocking_stylesheets_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/#concept-n-noscript> /// True if scripting is enabled for all scripts in this document scripting_enabled: bool, /// <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 the compositor. 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>, /// 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<HashMap<Dom<Element>, NoTrace<PendingRestyle>>>, /// This flag will be true if the `Document` needs to be painted again /// during the next full layout attempt due to some external change such as /// the web view changing size, or because the previous layout was only for /// layout queries (which do not trigger display). needs_paint: Cell<bool>, /// <http://w3c.github.io/touch-events/#dfn-active-touch-point> active_touch_points: DomRefCell<Vec<Dom<Touch>>>, /// Navigation Timing properties: /// <https://w3c.github.io/navigation-timing/#sec-PerformanceNavigationTiming> #[no_trace] dom_interactive: Cell<Option<CrossProcessInstant>>, #[no_trace] dom_content_loaded_event_start: Cell<Option<CrossProcessInstant>>, #[no_trace] dom_content_loaded_event_end: Cell<Option<CrossProcessInstant>>, #[no_trace] dom_complete: Cell<Option<CrossProcessInstant>>, #[no_trace] top_level_dom_complete: Cell<Option<CrossProcessInstant>>, #[no_trace] load_event_start: Cell<Option<CrossProcessInstant>>, #[no_trace] load_event_end: Cell<Option<CrossProcessInstant>>, #[no_trace] unload_event_start: Cell<Option<CrossProcessInstant>>, #[no_trace] unload_event_end: Cell<Option<CrossProcessInstant>>, /// <https://html.spec.whatwg.org/multipage/#concept-document-https-state> #[no_trace] https_state: Cell<HttpsState>, /// The document's origin. #[no_trace] origin: 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://w3c.github.io/uievents/#event-type-dblclick> #[ignore_malloc_size_of = "Defined in std"] #[no_trace] last_click_info: DomRefCell<Option<(Instant, Point2D<f32>)>>, /// <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>, /// Track the total number of elements in this DOM's tree. /// This is sent to layout every time a reflow is done; /// layout uses this to determine if the gains from parallel layout will be worth the overhead. /// /// See also: <https://github.com/servo/servo/issues/10110> dom_count: Cell<u32>, /// 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> form_id_listener_map: DomRefCell<HashMapTracedValues<Atom, HashSet<Dom<Element>>>>, #[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>>>, /// Number of redirects for the document load redirect_count: Cell<u16>, /// 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 TaskBox>>>, /// <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>>>, /// List of all context 2d IDs that need flushing. dirty_2d_contexts: DomRefCell<HashMapTracedValues<CanvasId, Dom<CanvasRenderingContext2D>>>, /// List of all WebGL context IDs that need flushing. dirty_webgl_contexts: DomRefCell<HashMapTracedValues<WebGLContextId, Dom<WebGLRenderingContext>>>, /// List of all WebGPU contexts. #[cfg(feature = "webgpu")] #[ignore_malloc_size_of = "Rc are hard"] webgpu_contexts: WebGPUContextsMap, /// <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> animation_timeline: DomRefCell<AnimationTimeline>, /// Animations for this Document animations: DomRefCell<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>>, /// Pending input events, to be handled at the next rendering opportunity. #[no_trace] #[ignore_malloc_size_of = "CompositorEvent contains data from outside crates"] pending_input_events: DomRefCell<Vec<ConstellationInputEvent>>, /// The index of the last mouse move event in the pending compositor events queue. mouse_move_event_index: DomRefCell<Option<usize>>, /// <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 active keyboard modifiers for the WebView. This is updated when receiving any input event. #[no_trace] active_keyboard_modifiers: Cell<Modifiers>, /// The node that is currently highlighted by the devtools highlighted_dom_node: MutNullableDom<Node>, } #[allow(non_snake_case)] impl Document { pub(crate) fn note_node_with_dirty_descendants(&self, node: &Node) { debug_assert!(*node.owner_doc() == *self); if !node.is_connected() { return; } let parent = match node.parent_in_flat_tree() { Some(parent) => parent, None => { // There is no parent so this is the Document node, so we // behave as if we were called with the document element. let document_element = match self.GetDocumentElement() { Some(element) => element, None => return, }; if let Some(dirty_root) = self.dirty_root.get() { // There was an existing dirty root so we mark its // ancestors as dirty until the document element. for ancestor in dirty_root .upcast::<Node>() .inclusive_ancestors_in_flat_tree() { if ancestor.is::<Element>() { ancestor.set_flag(NodeFlags::HAS_DIRTY_DESCENDANTS, true); } } } self.dirty_root.set(Some(&document_element)); return; }, }; if parent.is::<Element>() { if !parent.is_styled() { return; } if parent.is_display_none() { return; } } let element_parent: DomRoot<Element>; let element = match node.downcast::<Element>() { Some(element) => element, None => { // Current node is not an element, it's probably a text node, // we try to get its element parent. match DomRoot::downcast::<Element>(parent) { Some(parent) => { element_parent = parent; &element_parent }, None => { // Parent is not an element so it must be a document, // and this is not an element either, so there is // nothing to do. return; }, } }, }; let dirty_root = match self.dirty_root.get() { None => { element .upcast::<Node>() .set_flag(NodeFlags::HAS_DIRTY_DESCENDANTS, true); self.dirty_root.set(Some(element)); return; }, Some(root) => root, }; for ancestor in element.upcast::<Node>().inclusive_ancestors_in_flat_tree() { if ancestor.get_flag(NodeFlags::HAS_DIRTY_DESCENDANTS) { return; } if ancestor.is::<Element>() { ancestor.set_flag(NodeFlags::HAS_DIRTY_DESCENDANTS, true); } } let new_dirty_root = element .upcast::<Node>() .common_ancestor_in_flat_tree(dirty_root.upcast()) .expect("Couldn't find common ancestor"); let mut has_dirty_descendants = true; for ancestor in dirty_root .upcast::<Node>() .inclusive_ancestors_in_flat_tree() { ancestor.set_flag(NodeFlags::HAS_DIRTY_DESCENDANTS, has_dirty_descendants); has_dirty_descendants &= *ancestor != *new_dirty_root; } let maybe_shadow_host = new_dirty_root .downcast::<ShadowRoot>() .map(ShadowRootMethods::Host); let new_dirty_root_element = new_dirty_root .downcast::<Element>() .or(maybe_shadow_host.as_deref()); self.dirty_root.set(new_dirty_root_element); } 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 set_https_state(&self, https_state: HttpsState) { self.https_state.set(https_state); } 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 } pub(crate) fn set_activity(&self, activity: DocumentActivity, can_gc: CanGc) { // 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(can_gc); media.suspend(&client_context_id); return; } self.title_changed(); self.dirty_all_nodes(); self.window().resume(can_gc); 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 || { 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(DocumentVisibilityState::Visible, CanGc::note()); // Step 4.6.4 Fire a page transition event named pageshow at document's relevant // global object with true. let event = PageTransitionEvent::new( window, atom!("pageshow"), false, // bubbles false, // cancelable true, // persisted CanGc::note(), ); let event = event.upcast::<Event>(); event.set_trusted(true); window.dispatch_event_with_target_override(event, CanGc::note()); })) } pub(crate) fn origin(&self) -> &MutableOrigin { &self.origin } /// <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; } /// <https://html.spec.whatwg.org/multipage/#fallback-base-url> pub(crate) fn fallback_base_url(&self) -> ServoUrl { let document_url = self.url(); if let Some(browsing_context) = self.browsing_context() { // Step 1: If document is an iframe srcdoc document, then return the // document base URL of document's browsing context's container document. let container_base_url = browsing_context .parent() .and_then(|parent| parent.document()) .map(|document| document.base_url()); if document_url.as_str() == "about:srcdoc" { if let Some(base_url) = container_base_url { return base_url; } } // Step 2: If document's URL is about:blank, and document's browsing // context's creator base URL is non-null, then return that creator base URL. if document_url.as_str() == "about:blank" && browsing_context.has_creator_base_url() { return browsing_context.creator_base_url().unwrap(); } } // 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 set_needs_paint(&self, value: bool) { self.needs_paint.set(value) } pub(crate) fn needs_reflow(&self) -> Option<ReflowTriggerCondition> { // FIXME: This should check the dirty bit on the document, // not the document element. Needs some layout changes to make // that workable. if self.stylesheets.borrow().has_changed() { return Some(ReflowTriggerCondition::StylesheetsChanged); } let root = self.GetDocumentElement()?; if root.upcast::<Node>().has_dirty_descendants() { return Some(ReflowTriggerCondition::DirtyDescendants); } if !self.pending_restyles.borrow().is_empty() { return Some(ReflowTriggerCondition::PendingRestyles); } if self.needs_paint.get() { return Some(ReflowTriggerCondition::PaintPostponed); } None } /// 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() } /// Refresh the cached first base element in the DOM. /// <https://github.com/w3c/web-platform-tests/issues/2122> pub(crate) fn refresh_base_element(&self) { let base = self .upcast::<Node>() .traverse_preorder(ShadowIncluding::No) .filter_map(DomRoot::downcast::<HTMLBaseElement>) .find(|element| { element .upcast::<Element>() .has_attribute(&local_name!("href")) }); self.base_element.set(base.as_deref()); } pub(crate) fn dom_count(&self) -> u32 { self.dom_count.get() } /// This is called by `bind_to_tree` when a node is added to the DOM. /// The internal count is used by layout to determine whether to be sequential or parallel. /// (it's sequential for small DOMs) pub(crate) fn increment_dom_count(&self) { self.dom_count.set(self.dom_count.get() + 1); } /// This is called by `unbind_from_tree` when a node is removed from the DOM. pub(crate) fn decrement_dom_count(&self) { self.dom_count.set(self.dom_count.get() - 1); } 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, node: &Node) { if node.is_connected() { node.note_dirty_descendants(); } // FIXME(emilio): This is very inefficient, ideally the flag above would // be enough and incremental layout could figure out from there. node.dirty(NodeDamage::Other); } /// Remove any existing association between the provided id and any elements in this document. pub(crate) fn unregister_element_id(&self, to_unregister: &Element, id: Atom, can_gc: CanGc) { self.document_or_shadow_root .unregister_named_element(&self.id_map, to_unregister, &id); self.reset_form_owner_for_listeners(&id, can_gc); } /// Associate an element present in this document with the provided id. pub(crate) fn register_element_id(&self, element: &Element, id: Atom, can_gc: CanGc) { let root = self.GetDocumentElement().expect( "The element is in the document, so there must be a document \ element.", ); self.document_or_shadow_root.register_named_element( &self.id_map, element, &id, DomRoot::from_ref(root.upcast::<Node>()), ); self.reset_form_owner_for_listeners(&id, can_gc); } /// Remove any existing association between the provided name and any elements in this document. pub(crate) fn unregister_element_name(&self, to_unregister: &Element, name: Atom) { self.document_or_shadow_root .unregister_named_element(&self.name_map, to_unregister, &name); } /// Associate an element present in this document with the provided name. pub(crate) fn register_element_name(&self, element: &Element, name: Atom) { let root = self.GetDocumentElement().expect( "The element is in the document, so there must be a document \ element.", ); self.document_or_shadow_root.register_named_element( &self.name_map, element, &name, DomRoot::from_ref(root.upcast::<Node>()), ); } 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(); } } } /// Attempt to find a named element in this page's document. /// <https://html.spec.whatwg.org/multipage/#the-indicated-part-of-the-document> pub(crate) fn find_fragment_node(&self, fragid: &str) -> Option<DomRoot<Element>> { // Step 1 is not handled here; the fragid is already obtained by the calling function // Step 2: Simply use None to indicate the top of the document. // Step 3 & 4 percent_decode(fragid.as_bytes()) .decode_utf8() .ok() // Step 5 .and_then(|decoded_fragid| self.get_element_by_id(&Atom::from(decoded_fragid))) // Step 6 .or_else(|| self.get_anchor_by_name(fragid)) // Step 7 & 8 } /// Scroll to the target element, and when we do not find a target /// and the fragment is empty or "top", scroll to the top. /// <https://html.spec.whatwg.org/multipage/#scroll-to-the-fragment-identifier> pub(crate) fn check_and_scroll_fragment(&self, fragment: &str, can_gc: CanGc) { let target = self.find_fragment_node(fragment); // Step 1 self.set_target_element(target.as_deref()); let point = target .as_ref() .map(|element| { // TODO: This strategy is completely wrong if the element we are scrolling to in // inside other scrollable containers. Ideally this should use an implementation of // `scrollIntoView` when that is available: // See https://github.com/servo/servo/issues/24059. let rect = element .upcast::<Node>() .bounding_content_box_or_zero(can_gc); // In order to align with element edges, we snap to unscaled pixel boundaries, since // the paint thread currently does the same for drawing elements. This is important // for pages that require pixel perfect scroll positioning for proper display // (like Acid2). let device_pixel_ratio = self.window.device_pixel_ratio().get(); ( rect.origin.x.to_nearest_pixel(device_pixel_ratio), rect.origin.y.to_nearest_pixel(device_pixel_ratio), ) }) .or_else(|| { if fragment.is_empty() || fragment.eq_ignore_ascii_case("top") { // FIXME(stshine): this should be the origin of the stacking context space, // which may differ under the influence of writing mode. Some((0.0, 0.0)) } else { None } }); if let Some((x, y)) = point { self.window .scroll(x as f64, y as f64, ScrollBehavior::Instant, can_gc) } } fn get_anchor_by_name(&self, name: &str) -> Option<DomRoot<Element>> { let name = Atom::from(name); self.name_map.borrow().get(&name).and_then(|elements| { elements .iter() .find(|e| e.is::<HTMLAnchorElement>()) .map(|e| DomRoot::from_ref(&**e)) }) } // https://html.spec.whatwg.org/multipage/#current-document-readiness pub(crate) fn set_ready_state(&self, state: DocumentReadyState, can_gc: CanGc) { 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)); } }, DocumentReadyState::Complete => { if self.window().is_top_level() { self.send_to_embedder(EmbedderMsg::NotifyLoadStatusChanged( self.webview_id(), LoadStatus::Complete, )); } update_with_current_instant(&self.dom_complete); }, DocumentReadyState::Interactive => update_with_current_instant(&self.dom_interactive), }; self.ready_state.set(state); self.upcast::<EventTarget>() .fire_event(atom!("readystatechange"), can_gc); } /// Return whether scripting is enabled or not pub(crate) fn is_scripting_enabled(&self) -> bool { self.scripting_enabled } /// Return whether scripting is enabled or not /// <https://html.spec.whatwg.org/multipage/#concept-n-noscript> pub(crate) fn scripting_enabled(&self) -> bool { self.has_browsing_context() } /// Return the element that currently has focus. // https://w3c.github.io/uievents/#events-focusevent-doc-focus pub(crate) fn get_focused_element(&self) -> Option<DomRoot<Element>> { self.focused.get() } /// Get the last sequence number sent to the constellation. /// /// Received focus-related messages with sequence numbers less than the one /// returned by this method must be discarded. pub fn get_focus_sequence(&self) -> FocusSequenceNumber { self.focus_sequence.get() } /// Generate the next sequence number for focus-related messages. fn increment_fetch_focus_sequence(&self) -> FocusSequenceNumber { self.focus_sequence.set(FocusSequenceNumber( self.focus_sequence .get() .0 .checked_add(1) .expect("too many focus messages have been sent"), )); self.focus_sequence.get() } /// Initiate a new round of checking for elements requesting focus. The last element to call /// `request_focus` before `commit_focus_transaction` is called will receive focus. fn begin_focus_transaction(&self) { // Initialize it with the current state *self.focus_transaction.borrow_mut() = Some(FocusTransaction { element: self.focused.get().as_deref().map(Dom::from_ref), has_focus: self.has_focus.get(), }); } /// <https://html.spec.whatwg.org/multipage/#focus-fixup-rule> pub(crate) fn perform_focus_fixup_rule(&self, not_focusable: &Element, can_gc: CanGc) { // Return if `not_focusable` is not the designated focused area of the // `Document`. if Some(not_focusable) != self.focused.get().as_deref() { return; } let implicit_transaction = self.focus_transaction.borrow().is_none(); if implicit_transaction { self.begin_focus_transaction(); } // Designate the viewport as the new focused area of the `Document`, but // do not run the focusing steps. { let mut focus_transaction = self.focus_transaction.borrow_mut(); focus_transaction.as_mut().unwrap().element = None; } if implicit_transaction { self.commit_focus_transaction(FocusInitiator::Local, can_gc); } } /// Request that the given element receive focus once the current /// transaction is complete. `None` specifies to focus the document. /// /// If there's no ongoing transaction, this method automatically starts and /// commits an implicit transaction. pub(crate) fn request_focus( &self, elem: Option<&Element>, focus_initiator: FocusInitiator, can_gc: CanGc, ) { // If an element is specified, and it's non-focusable, ignore the // request. if elem.is_some_and(|e| !e.is_focusable_area()) { return; } let implicit_transaction = self.focus_transaction.borrow().is_none(); if implicit_transaction { self.begin_focus_transaction(); } { let mut focus_transaction = self.focus_transaction.borrow_mut(); let focus_transaction = focus_transaction.as_mut().unwrap(); focus_transaction.element = elem.map(Dom::from_ref); focus_transaction.has_focus = true; } if implicit_transaction { self.commit_focus_transaction(focus_initiator, can_gc); } } /// Update the local focus state accordingly after being notified that the /// document's container is removed from the top-level browsing context's /// focus chain (not considering system focus). pub(crate) fn handle_container_unfocus(&self, can_gc: CanGc) { assert!( self.window().parent_info().is_some(), "top-level document cannot be unfocused", ); // Since this method is called from an event loop, there mustn't be // an in-progress focus transaction assert!( self.focus_transaction.borrow().is_none(), "there mustn't be an in-progress focus transaction at this point" ); // Start an implicit focus transaction self.begin_focus_transaction(); // Update the transaction { let mut focus_transaction = self.focus_transaction.borrow_mut(); focus_transaction.as_mut().unwrap().has_focus = false; } // Commit the implicit focus transaction self.commit_focus_transaction(FocusInitiator::Remote, can_gc); } /// Reassign the focus context to the element that last requested focus during this /// transaction, or the document if no elements requested it. fn commit_focus_transaction(&self, focus_initiator: FocusInitiator, can_gc: CanGc) { let (mut new_focused, new_focus_state) = { let focus_transaction = self.focus_transaction.borrow(); let focus_transaction = focus_transaction .as_ref() .expect("no focus transaction in progress"); ( focus_transaction .element .as_ref() .map(|e| DomRoot::from_ref(&**e)), focus_transaction.has_focus, ) }; *self.focus_transaction.borrow_mut() = None; if !new_focus_state { // In many browsers, a document forgets its focused area when the // document is removed from the top-level BC's focus chain if new_focused.take().is_some() { trace!( "Forgetting the document's focused area because the \ document's container was removed from the top-level BC's \ focus chain" ); } } let old_focused = self.focused.get(); let old_focus_state = self.has_focus.get(); debug!( "Committing focus transaction: {:?} → {:?}", (&old_focused, old_focus_state), (&new_focused, new_focus_state), ); // `*_focused_filtered` indicates the local element (if any) included in // the top-level BC's focus chain. let old_focused_filtered = old_focused.as_ref().filter(|_| old_focus_state); let new_focused_filtered = new_focused.as_ref().filter(|_| new_focus_state); let trace_focus_chain = |name, element, doc| { trace!( "{} local focus chain: {}", name, match (element, doc) { (Some(e), _) => format!("[{:?}, document]", e), (None, true) => "[document]".to_owned(), (None, false) => "[]".to_owned(), } ); }; trace_focus_chain("Old", old_focused_filtered, old_focus_state); trace_focus_chain("New", new_focused_filtered, new_focus_state); if old_focused_filtered != new_focused_filtered { if let Some(elem) = &old_focused_filtered { let node = elem.upcast::<Node>(); elem.set_focus_state(false); // FIXME: pass appropriate relatedTarget if node.is_connected() { self.fire_focus_event(FocusEventType::Blur, node.upcast(), None, can_gc); } // Notify the embedder to hide the input method. if elem.input_method_type().is_some() { self.send_to_embedder(EmbedderMsg::HideIME(self.webview_id())); } } } if old_focus_state != new_focus_state && !new_focus_state { self.fire_focus_event(FocusEventType::Blur, self.global().upcast(), None, can_gc); } self.focused.set(new_focused.as_deref()); self.has_focus.set(new_focus_state); if old_focus_state != new_focus_state && new_focus_state { self.fire_focus_event(FocusEventType::Focus, self.global().upcast(), None, can_gc); } if old_focused_filtered != new_focused_filtered { if let Some(elem) = &new_focused_filtered { elem.set_focus_state(true); let node = elem.upcast::<Node>(); // FIXME: pass appropriate relatedTarget self.fire_focus_event(FocusEventType::Focus, node.upcast(), None, can_gc); // Notify the embedder to display an input method. if let Some(kind) = elem.input_method_type() { let rect = elem.upcast::<Node>().bounding_content_box_or_zero(can_gc); let rect = Rect::new( Point2D::new(rect.origin.x.to_px(), rect.origin.y.to_px()), Size2D::new(rect.size.width.to_px(), rect.size.height.to_px()), ); let (text, multiline) = if let Some(input) = elem.downcast::<HTMLInputElement>() { ( Some(( (input.Value()).to_string(), input.GetSelectionEnd().unwrap_or(0) as i32, )), false, ) } else if let Some(textarea) = elem.downcast::<HTMLTextAreaElement>() { ( Some(( (textarea.Value()).to_string(), textarea.GetSelectionEnd().unwrap_or(0) as i32, )), true, ) } else { (None, false) }; self.send_to_embedder(EmbedderMsg::ShowIME( self.webview_id(), kind, text, multiline, DeviceIntRect::from_untyped(&rect.to_box2d()), )); } } } if focus_initiator != FocusInitiator::Local { return; } // We are the initiator of the focus operation, so we must broadcast // the change we intend to make. match (old_focus_state, new_focus_state) { (_, true) => { // Advertise the change in the focus chain. // <https://html.spec.whatwg.org/multipage/#focus-chain> // <https://html.spec.whatwg.org/multipage/#focusing-steps> // // If the top-level BC doesn't have system focus, this won't // have an immediate effect, but it will when we gain system // focus again. Therefore we still have to send `ScriptMsg:: // Focus`. // // When a container with a non-null nested browsing context is // focused, its active document becomes the focused area of the // top-level browsing context instead. Therefore we need to let // the constellation know if such a container is focused. // // > The focusing steps for an object `new focus target` [...] // > // > 3. If `new focus target` is a browsing context container // > with non-null nested browsing context, then set // > `new focus target` to the nested browsing context's // > active document. let child_browsing_context_id = new_focused .as_ref() .and_then(|elem| elem.downcast::<HTMLIFrameElement>()) .and_then(|iframe| iframe.browsing_context_id()); let sequence = self.increment_fetch_focus_sequence(); debug!( "Advertising the focus request to the constellation \ with sequence number {} and child BC ID {}", sequence, child_browsing_context_id .as_ref() .map(|id| id as &dyn std::fmt::Display) .unwrap_or(&"(none)"), ); self.window() .send_to_constellation(ScriptToConstellationMessage::Focus( child_browsing_context_id, sequence, )); }, (false, false) => { // Our `Document` doesn't have focus, and we intend to keep it // this way. }, (true, false) => { unreachable!( "Can't lose the document's focus without specifying \ another one to focus" ); }, } } /// 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(split_html_space_chars(&value), " ")) }) } /// 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) { let root = match self.GetDocumentElement() { Some(root) => root, None => return, }; for node in root .upcast::<Node>() .traverse_preorder(ShadowIncluding::Yes) { node.dirty(NodeDamage::Other) } } #[allow(unsafe_code)] pub(crate) fn handle_mouse_button_event( &self, event: MouseButtonEvent, hit_test_result: Option<CompositorHitTestResult>, pressed_mouse_buttons: u16, can_gc: CanGc, ) { // Ignore all incoming events without a hit test. let Some(hit_test_result) = hit_test_result else { return; }; debug!( "{:?}: at {:?}", event.action, hit_test_result.point_in_viewport ); let node = unsafe { node::from_untrusted_node_address(hit_test_result.node) }; let Some(el) = node .inclusive_ancestors(ShadowIncluding::Yes) .filter_map(DomRoot::downcast::<Element>) .next() else { return; }; let node = el.upcast::<Node>(); debug!("{:?} on {:?}", event.action, node.debug_str()); // Prevent click event if form control element is disabled. if let MouseButtonAction::Click = event.action { // The click event is filtered by the disabled state. if el.is_actually_disabled() { return; } self.begin_focus_transaction(); // Try to focus `el`. If it's not focusable, focus the document // instead. self.request_focus(None, FocusInitiator::Local, can_gc); self.request_focus(Some(&*el), FocusInitiator::Local, can_gc); } let dom_event = DomRoot::upcast::<Event>(MouseEvent::for_platform_mouse_event( event, pressed_mouse_buttons, &self.window, &hit_test_result, can_gc, )); // https://html.spec.whatwg.org/multipage/#run-authentic-click-activation-steps let activatable = el.as_maybe_activatable(); match event.action { MouseButtonAction::Click => { el.set_click_in_progress(true); dom_event.fire(node.upcast(), can_gc); el.set_click_in_progress(false); }, MouseButtonAction::Down => { if let Some(a) = activatable { a.enter_formal_activation_state(); } let target = node.upcast(); dom_event.fire(target, can_gc); }, MouseButtonAction::Up => { if let Some(a) = activatable { a.exit_formal_activation_state(); } let target = node.upcast(); dom_event.fire(target, can_gc); }, } if let MouseButtonAction::Click = event.action { if self.focus_transaction.borrow().is_some() { self.commit_focus_transaction(FocusInitiator::Local, can_gc); } self.maybe_fire_dblclick( hit_test_result.point_in_viewport, node, pressed_mouse_buttons, can_gc, ); } // When the contextmenu event is triggered by right mouse button // the contextmenu event MUST be dispatched after the mousedown event. if let (MouseButtonAction::Down, MouseButton::Right) = (event.action, event.button) { self.maybe_show_context_menu( node.upcast(), pressed_mouse_buttons, hit_test_result.point_in_viewport, can_gc, ); } } /// <https://www.w3.org/TR/uievents/#maybe-show-context-menu> fn maybe_show_context_menu( &self, target: &EventTarget, pressed_mouse_buttons: u16, client_point: Point2D<f32>, can_gc: CanGc, ) { let client_x = client_point.x.to_i32().unwrap_or(0); let client_y = client_point.y.to_i32().unwrap_or(0); // <https://w3c.github.io/uievents/#contextmenu> let menu_event = PointerEvent::new( &self.window, // window DOMString::from("contextmenu"), // type EventBubbles::Bubbles, // can_bubble EventCancelable::Cancelable, // cancelable Some(&self.window), // view 0, // detail client_x, // screen_x client_y, // screen_y client_x, // client_x client_y, // client_y false, // ctrl_key false, // alt_key false, // shift_key false, // meta_key 2i16, // button, right mouse button pressed_mouse_buttons, // buttons None, // related_target None, // point_in_target PointerId::Mouse as i32, // pointer_id 1, // width 1, // height 0.5, // pressure 0.0, // tangential_pressure 0, // tilt_x 0, // tilt_y 0, // twist PI / 2.0, // altitude_angle 0.0, // azimuth_angle DOMString::from("mouse"), // pointer_type true, // is_primary vec![], // coalesced_events vec![], // predicted_events can_gc, ); let event = menu_event.upcast::<Event>(); event.fire(target, can_gc); // if the event was not canceled, notify the embedder to show the context menu if event.status() == EventStatus::NotCanceled { let (sender, receiver) = ipc::channel::<ContextMenuResult>().expect("Failed to create IPC channel."); self.send_to_embedder(EmbedderMsg::ShowContextMenu( self.webview_id(), sender, None, vec![], )); let _ = receiver.recv().unwrap(); }; } fn maybe_fire_dblclick( &self, click_pos: Point2D<f32>, target: &Node, pressed_mouse_buttons: u16, can_gc: CanGc, ) { // https://w3c.github.io/uievents/#event-type-dblclick let now = Instant::now(); let opt = self.last_click_info.borrow_mut().take(); if let Some((last_time, last_pos)) = opt { let DBL_CLICK_TIMEOUT = Duration::from_millis(pref!(dom_document_dblclick_timeout) as u64); let DBL_CLICK_DIST_THRESHOLD = pref!(dom_document_dblclick_dist) as u64; // Calculate distance between this click and the previous click. let line = click_pos - last_pos; let dist = (line.dot(line) as f64).sqrt(); if now.duration_since(last_time) < DBL_CLICK_TIMEOUT && dist < DBL_CLICK_DIST_THRESHOLD as f64 { // A double click has occurred if this click is within a certain time and dist. of previous click. let click_count = 2; let client_x = click_pos.x as i32; let client_y = click_pos.y as i32; let event = MouseEvent::new( &self.window, DOMString::from("dblclick"), EventBubbles::Bubbles, EventCancelable::Cancelable, Some(&self.window), click_count, client_x, client_y, client_x, client_y, false, false, false, false, 0i16, pressed_mouse_buttons, None, None, can_gc, ); event.upcast::<Event>().fire(target.upcast(), can_gc); // When a double click occurs, self.last_click_info is left as None so that a // third sequential click will not cause another double click. return; } } // Update last_click_info with the time and position of the click. *self.last_click_info.borrow_mut() = Some((now, click_pos)); } #[allow(clippy::too_many_arguments)] pub(crate) fn fire_mouse_event( &self, client_point: Point2D<f32>, target: &EventTarget, event_name: FireMouseEventType, can_bubble: EventBubbles, cancelable: EventCancelable, pressed_mouse_buttons: u16, can_gc: CanGc, ) { let client_x = client_point.x.to_i32().unwrap_or(0); let client_y = client_point.y.to_i32().unwrap_or(0); MouseEvent::new( &self.window, DOMString::from(event_name.as_str()), can_bubble, cancelable, Some(&self.window), 0i32, client_x, client_y, client_x, client_y, false, false, false, false, 0i16, pressed_mouse_buttons, None, None, can_gc, ) .upcast::<Event>() .fire(target, can_gc); } pub(crate) fn handle_editing_action(&self, action: EditingActionEvent, can_gc: CanGc) -> bool { let clipboard_event = match action { EditingActionEvent::Copy => ClipboardEventType::Copy, EditingActionEvent::Cut => ClipboardEventType::Cut, EditingActionEvent::Paste => ClipboardEventType::Paste, }; self.handle_clipboard_action(clipboard_event, can_gc) } /// <https://www.w3.org/TR/clipboard-apis/#clipboard-actions> fn handle_clipboard_action(&self, action: ClipboardEventType, can_gc: CanGc) -> bool { // The script_triggered flag is set if the action runs because of a script, e.g. document.execCommand() let script_triggered = false; // The script_may_access_clipboard flag is set // if action is paste and the script thread is allowed to read from clipboard or // if action is copy or cut and the script thread is allowed to modify the clipboard let script_may_access_clipboard = false; // Step 1 If the script-triggered flag is set and the script-may-access-clipboard flag is unset if script_triggered && !script_may_access_clipboard { return false; } // Step 2 Fire a clipboard event let event = ClipboardEvent::new( &self.window, None, DOMString::from(action.as_str()), EventBubbles::Bubbles, EventCancelable::Cancelable, None, can_gc, ); self.fire_clipboard_event(&event, action, can_gc); // Step 3 If a script doesn't call preventDefault() // the event will be handled inside target's VirtualMethods::handle_event let e = event.upcast::<Event>(); if !e.IsTrusted() { return false; } // Step 4 If the event was canceled, then if e.DefaultPrevented() { match e.Type().str() { "copy" => { // Step 4.1 Call the write content to the clipboard algorithm, // passing on the DataTransferItemList items, a clear-was-called flag and a types-to-clear list. if let Some(clipboard_data) = event.get_clipboard_data() { let drag_data_store = clipboard_data.data_store().expect("This shouldn't fail"); self.write_content_to_the_clipboard(&drag_data_store); } }, "cut" => { // Step 4.1 Call the write content to the clipboard algorithm, // passing on the DataTransferItemList items, a clear-was-called flag and a types-to-clear list. if let Some(clipboard_data) = event.get_clipboard_data() { let drag_data_store = clipboard_data.data_store().expect("This shouldn't fail"); self.write_content_to_the_clipboard(&drag_data_store); } // Step 4.2 Fire a clipboard event named clipboardchange self.fire_clipboardchange_event(can_gc); }, "paste" => return false, _ => (), } } //Step 5 true } /// <https://www.w3.org/TR/clipboard-apis/#fire-a-clipboard-event> fn fire_clipboard_event( &self, event: &ClipboardEvent, action: ClipboardEventType, can_gc: CanGc, ) { // Step 1 Let clear_was_called be false // Step 2 Let types_to_clear an empty list let mut drag_data_store = DragDataStore::new(); // Step 4 let clipboard-entry be the sequence number of clipboard content, null if the OS doesn't support it. // Step 5 let trusted be true if the event is generated by the user agent, false otherwise let trusted = true; // Step 6 if the context is editable: let focused = self.get_focused_element(); let body = self.GetBody(); let target = match (&focused, &body) { (Some(focused), _) => focused.upcast(), (&None, Some(body)) => body.upcast(), (&None, &None) => self.window.upcast(), }; // Step 6.2 else TODO require Selection see https://github.com/w3c/clipboard-apis/issues/70 // Step 7 match action { ClipboardEventType::Copy | ClipboardEventType::Cut => { // Step 7.2.1 drag_data_store.set_mode(Mode::ReadWrite); }, ClipboardEventType::Paste => { let (sender, receiver) = ipc::channel().unwrap(); self.window .send_to_constellation(ScriptToConstellationMessage::ForwardToEmbedder( EmbedderMsg::GetClipboardText(self.window.webview_id(), sender), )); let text_contents = receiver .recv() .map(Result::unwrap_or_default) .unwrap_or_default(); // Step 7.1.1 drag_data_store.set_mode(Mode::ReadOnly); // Step 7.1.2 If trusted or the implementation gives script-generated events access to the clipboard if trusted { // Step 7.1.2.1 For each clipboard-part on the OS clipboard: // Step 7.1.2.1.1 If clipboard-part contains plain text, then let data = DOMString::from(text_contents.to_string()); let type_ = DOMString::from("text/plain"); let _ = drag_data_store.add(Kind::Text { data, type_ }); // Step 7.1.2.1.2 TODO If clipboard-part represents file references, then for each file reference // Step 7.1.2.1.3 TODO If clipboard-part contains HTML- or XHTML-formatted text then // Step 7.1.3 Update clipboard-event-data’s files to match clipboard-event-data’s items // Step 7.1.4 Update clipboard-event-data’s types to match clipboard-event-data’s items } }, ClipboardEventType::Change => (), } // Step 3 let clipboard_event_data = DataTransfer::new( &self.window, Rc::new(RefCell::new(Some(drag_data_store))), can_gc, ); // Step 8 event.set_clipboard_data(Some(&clipboard_event_data)); let event = event.upcast::<Event>(); // Step 9 event.set_trusted(trusted); // Step 10 Set event’s composed to true. event.set_composed(true); // Step 11 event.dispatch(target, false, can_gc); } pub(crate) fn fire_clipboardchange_event(&self, can_gc: CanGc) { let clipboardchange_event = ClipboardEvent::new( &self.window, None, DOMString::from("clipboardchange"), EventBubbles::Bubbles, EventCancelable::Cancelable, None, can_gc, ); self.fire_clipboard_event(&clipboardchange_event, ClipboardEventType::Change, can_gc); } /// <https://www.w3.org/TR/clipboard-apis/#write-content-to-the-clipboard> fn write_content_to_the_clipboard(&self, drag_data_store: &DragDataStore) { // Step 1 if drag_data_store.list_len() > 0 { // Step 1.1 Clear the clipboard. self.send_to_embedder(EmbedderMsg::ClearClipboard(self.webview_id())); // Step 1.2 for item in drag_data_store.iter_item_list() { match item { Kind::Text { data, .. } => { // Step 1.2.1.1 Ensure encoding is correct per OS and locale conventions // Step 1.2.1.2 Normalize line endings according to platform conventions // Step 1.2.1.3 self.send_to_embedder(EmbedderMsg::SetClipboardText( self.webview_id(), data.to_string(), )); }, Kind::File { .. } => { // Step 1.2.2 If data is of a type listed in the mandatory data types list, then // Step 1.2.2.1 Place part on clipboard with the appropriate OS clipboard format description // Step 1.2.3 Else this is left to the implementation }, } } } else { // Step 2.1 if drag_data_store.clear_was_called { // Step 2.1.1 If types-to-clear list is empty, clear the clipboard self.send_to_embedder(EmbedderMsg::ClearClipboard(self.webview_id())); // Step 2.1.2 Else remove the types in the list from the clipboard // As of now this can't be done with Arboard, and it's possible that will be removed from the spec } } } #[allow(unsafe_code)] pub(crate) unsafe fn handle_mouse_move_event( &self, hit_test_result: Option<CompositorHitTestResult>, pressed_mouse_buttons: u16, prev_mouse_over_target: &MutNullableDom<Element>, can_gc: CanGc, ) { // Ignore all incoming events without a hit test. let Some(hit_test_result) = hit_test_result else { return; }; let node = unsafe { node::from_untrusted_node_address(hit_test_result.node) }; let Some(new_target) = node .inclusive_ancestors(ShadowIncluding::No) .filter_map(DomRoot::downcast::<Element>) .next() else { return; }; let target_has_changed = prev_mouse_over_target .get() .as_ref() .is_none_or(|old_target| old_target != &new_target); // Here we know the target has changed, so we must update the state, // dispatch mouseout to the previous one, mouseover to the new one. if target_has_changed { // Dispatch mouseout and mouseleave to previous target. if let Some(old_target) = prev_mouse_over_target.get() { let old_target_is_ancestor_of_new_target = old_target .upcast::<Node>() .is_ancestor_of(new_target.upcast::<Node>()); // If the old target is an ancestor of the new target, this can be skipped // completely, since the node's hover state will be reset below. if !old_target_is_ancestor_of_new_target { for element in old_target .upcast::<Node>() .inclusive_ancestors(ShadowIncluding::No) .filter_map(DomRoot::downcast::<Element>) { element.set_hover_state(false); element.set_active_state(false); } } self.fire_mouse_event( hit_test_result.point_in_viewport, old_target.upcast(), FireMouseEventType::Out, EventBubbles::Bubbles, EventCancelable::Cancelable, pressed_mouse_buttons, can_gc, ); if !old_target_is_ancestor_of_new_target { let event_target = DomRoot::from_ref(old_target.upcast::<Node>()); let moving_into = Some(DomRoot::from_ref(new_target.upcast::<Node>())); self.handle_mouse_enter_leave_event( hit_test_result.point_in_viewport, FireMouseEventType::Leave, moving_into, event_target, pressed_mouse_buttons, can_gc, ); } } // Dispatch mouseover and mouseenter to new target. for element in new_target .upcast::<Node>() .inclusive_ancestors(ShadowIncluding::No) .filter_map(DomRoot::downcast::<Element>) { if element.hover_state() { break; } element.set_hover_state(true); } self.fire_mouse_event( hit_test_result.point_in_viewport, new_target.upcast(), FireMouseEventType::Over, EventBubbles::Bubbles, EventCancelable::Cancelable, pressed_mouse_buttons, can_gc, ); let moving_from = prev_mouse_over_target .get() .map(|old_target| DomRoot::from_ref(old_target.upcast::<Node>())); let event_target = DomRoot::from_ref(new_target.upcast::<Node>()); self.handle_mouse_enter_leave_event( hit_test_result.point_in_viewport, FireMouseEventType::Enter, moving_from, event_target, pressed_mouse_buttons, can_gc, ); } // Send mousemove event to topmost target, unless it's an iframe, in which case the // compositor should have also sent an event to the inner document. self.fire_mouse_event( hit_test_result.point_in_viewport, new_target.upcast(), FireMouseEventType::Move, EventBubbles::Bubbles, EventCancelable::Cancelable, pressed_mouse_buttons, can_gc, ); // If the target has changed then store the current mouse over target for next frame. if target_has_changed { prev_mouse_over_target.set(Some(&new_target)); } } #[allow(unsafe_code)] pub(crate) fn handle_mouse_leave_event( &self, hit_test_result: Option<CompositorHitTestResult>, pressed_mouse_buttons: u16, can_gc: CanGc, ) { // Ignore all incoming events without a hit test. let Some(hit_test_result) = hit_test_result else { return; }; self.window() .send_to_embedder(EmbedderMsg::Status(self.webview_id(), None)); let node = unsafe { node::from_untrusted_node_address(hit_test_result.node) }; for element in node .inclusive_ancestors(ShadowIncluding::No) .filter_map(DomRoot::downcast::<Element>) { element.set_hover_state(false); element.set_active_state(false); } self.fire_mouse_event( hit_test_result.point_in_viewport, node.upcast(), FireMouseEventType::Out, EventBubbles::Bubbles, EventCancelable::Cancelable, pressed_mouse_buttons, can_gc, ); self.handle_mouse_enter_leave_event( hit_test_result.point_in_viewport, FireMouseEventType::Leave, None, node, pressed_mouse_buttons, can_gc, ); } fn handle_mouse_enter_leave_event( &self, client_point: Point2D<f32>, event_type: FireMouseEventType, related_target: Option<DomRoot<Node>>, event_target: DomRoot<Node>, pressed_mouse_buttons: u16, can_gc: CanGc, ) { assert!(matches!( event_type, FireMouseEventType::Enter | FireMouseEventType::Leave )); let common_ancestor = match related_target.as_ref() { Some(related_target) => event_target .common_ancestor(related_target, ShadowIncluding::No) .unwrap_or_else(|| DomRoot::from_ref(&*event_target)), None => DomRoot::from_ref(&*event_target), }; // We need to create a target chain in case the event target shares // its boundaries with its ancestors. let mut targets = vec![]; let mut current = Some(event_target); while let Some(node) = current { if node == common_ancestor { break; } current = node.GetParentNode(); targets.push(node); } // The order for dispatching mouseenter events starts from the topmost // common ancestor of the event target and the related target. if event_type == FireMouseEventType::Enter { targets = targets.into_iter().rev().collect(); } for target in targets { self.fire_mouse_event( client_point, target.upcast(), event_type, EventBubbles::DoesNotBubble, EventCancelable::NotCancelable, pressed_mouse_buttons, can_gc, ); } } #[allow(unsafe_code)] pub(crate) fn handle_wheel_event( &self, event: WheelEvent, hit_test_result: Option<CompositorHitTestResult>, can_gc: CanGc, ) { // Ignore all incoming events without a hit test. let Some(hit_test_result) = hit_test_result else { return; }; let node = unsafe { node::from_untrusted_node_address(hit_test_result.node) }; let Some(el) = node .inclusive_ancestors(ShadowIncluding::No) .filter_map(DomRoot::downcast::<Element>) .next() else { return; }; let node = el.upcast::<Node>(); let wheel_event_type_string = "wheel".to_owned(); debug!( "{}: on {:?} at {:?}", wheel_event_type_string, node.debug_str(), hit_test_result.point_in_viewport ); // https://w3c.github.io/uievents/#event-wheelevents let dom_event = DomWheelEvent::new( &self.window, DOMString::from(wheel_event_type_string), EventBubbles::Bubbles, EventCancelable::Cancelable, Some(&self.window), 0i32, // winit defines positive wheel delta values as revealing more content left/up. // https://docs.rs/winit-gtk/latest/winit/event/enum.MouseScrollDelta.html // This is the opposite of wheel delta in uievents // https://w3c.github.io/uievents/#dom-wheeleventinit-deltaz Finite::wrap(-event.delta.x), Finite::wrap(-event.delta.y), Finite::wrap(-event.delta.z), event.delta.mode as u32, can_gc, ); let dom_event = dom_event.upcast::<Event>(); dom_event.set_trusted(true); let target = node.upcast(); dom_event.fire(target, can_gc); } #[allow(unsafe_code)] pub(crate) fn handle_touch_event( &self, event: TouchEvent, hit_test_result: Option<CompositorHitTestResult>, can_gc: CanGc, ) -> TouchEventResult { // Ignore all incoming events without a hit test. let Some(hit_test_result) = hit_test_result else { return TouchEventResult::Forwarded; }; let TouchId(identifier) = event.id; let event_name = match event.event_type { TouchEventType::Down => "touchstart", TouchEventType::Move => "touchmove", TouchEventType::Up => "touchend", TouchEventType::Cancel => "touchcancel", }; let node = unsafe { node::from_untrusted_node_address(hit_test_result.node) }; let Some(el) = node .inclusive_ancestors(ShadowIncluding::No) .filter_map(DomRoot::downcast::<Element>) .next() else { return TouchEventResult::Forwarded; }; let target = DomRoot::upcast::<EventTarget>(el); let window = &*self.window; let client_x = Finite::wrap(hit_test_result.point_in_viewport.x as f64); let client_y = Finite::wrap(hit_test_result.point_in_viewport.y as f64); let page_x = Finite::wrap(hit_test_result.point_in_viewport.x as f64 + window.PageXOffset() as f64); let page_y = Finite::wrap(hit_test_result.point_in_viewport.y as f64 + window.PageYOffset() as f64); let touch = Touch::new( window, identifier, &target, client_x, client_y, // TODO: Get real screen coordinates? client_x, client_y, page_x, page_y, can_gc, ); match event.event_type { TouchEventType::Down => { // Add a new touch point self.active_touch_points .borrow_mut() .push(Dom::from_ref(&*touch)); }, TouchEventType::Move => { // Replace an existing touch point let mut active_touch_points = self.active_touch_points.borrow_mut(); match active_touch_points .iter_mut() .find(|t| t.Identifier() == identifier) { Some(t) => *t = Dom::from_ref(&*touch), None => warn!("Got a touchmove event for a non-active touch point"), } }, TouchEventType::Up | TouchEventType::Cancel => { // Remove an existing touch point let mut active_touch_points = self.active_touch_points.borrow_mut(); match active_touch_points .iter() .position(|t| t.Identifier() == identifier) { Some(i) => { active_touch_points.swap_remove(i); }, None => warn!("Got a touchend event for a non-active touch point"), } }, } rooted_vec!(let mut target_touches); let touches = { let touches = self.active_touch_points.borrow(); target_touches.extend(touches.iter().filter(|t| t.Target() == target).cloned()); TouchList::new(window, touches.r(), can_gc) }; let event = DomTouchEvent::new( window, DOMString::from(event_name), EventBubbles::Bubbles, EventCancelable::from(event.is_cancelable()), Some(window), 0i32, &touches, &TouchList::new(window, from_ref(&&*touch), can_gc), &TouchList::new(window, target_touches.r(), can_gc), // FIXME: modifier keys false, false, false, false, can_gc, ); let event = event.upcast::<Event>(); let result = event.fire(&target, can_gc); match result { EventStatus::Canceled => TouchEventResult::Processed(false), EventStatus::NotCanceled => TouchEventResult::Processed(true), } } #[allow(unsafe_code)] pub(crate) fn handle_scroll_event(&self, event: ScrollEvent, can_gc: CanGc) { // <https://drafts.csswg.org/cssom-view/#scrolling-events> // If target is a Document, fire an event named scroll that bubbles at target. if event.external_id.is_root() { let Some(document) = self .node .inclusive_ancestors(ShadowIncluding::No) .filter_map(DomRoot::downcast::<Document>) .next() else { return; }; DomRoot::upcast::<EventTarget>(document) .fire_bubbling_event(Atom::from("scroll"), can_gc); } else { // Otherwise, fire an event named scroll at target. let Some(node_id) = node_id_from_scroll_id(event.external_id.0 as usize) else { return; }; let node = unsafe { node::from_untrusted_node_address(UntrustedNodeAddress::from_id(node_id)) }; let Some(element) = node .inclusive_ancestors(ShadowIncluding::No) .filter_map(DomRoot::downcast::<Element>) .next() else { return; }; DomRoot::upcast::<EventTarget>(element).fire_event(Atom::from("scroll"), can_gc); } } /// The entry point for all key processing for web content pub(crate) fn dispatch_key_event( &self, keyboard_event: ::embedder_traits::KeyboardEvent, can_gc: CanGc, ) { let focused = self.get_focused_element(); let body = self.GetBody(); let target = match (&focused, &body) { (Some(focused), _) => focused.upcast(), (&None, Some(body)) => body.upcast(), (&None, &None) => self.window.upcast(), }; let keyevent = KeyboardEvent::new( &self.window, DOMString::from(keyboard_event.event.state.to_string()), true, true, Some(&self.window), 0, keyboard_event.event.key.clone(), DOMString::from(keyboard_event.event.code.to_string()), keyboard_event.event.location as u32, keyboard_event.event.repeat, keyboard_event.event.is_composing, keyboard_event.event.modifiers, 0, keyboard_event.event.key.legacy_keycode(), can_gc, ); let event = keyevent.upcast::<Event>(); event.fire(target, can_gc); let mut cancel_state = event.get_cancel_state(); // https://w3c.github.io/uievents/#keys-cancelable-keys // it MUST prevent the respective beforeinput and input // (and keypress if supported) events from being generated // TODO: keypress should be deprecated and superceded by beforeinput if keyboard_event.event.state == KeyState::Down && is_character_value_key(&(keyboard_event.event.key)) && !keyboard_event.event.is_composing && cancel_state != EventDefault::Prevented { // https://w3c.github.io/uievents/#keypress-event-order let event = KeyboardEvent::new( &self.window, DOMString::from("keypress"), true, true, Some(&self.window), 0, keyboard_event.event.key.clone(), DOMString::from(keyboard_event.event.code.to_string()), keyboard_event.event.location as u32, keyboard_event.event.repeat, keyboard_event.event.is_composing, keyboard_event.event.modifiers, keyboard_event.event.key.legacy_charcode(), 0, can_gc, ); let ev = event.upcast::<Event>(); ev.fire(target, can_gc); cancel_state = ev.get_cancel_state(); } if cancel_state == EventDefault::Allowed { let msg = EmbedderMsg::Keyboard(self.webview_id(), keyboard_event.clone()); self.send_to_embedder(msg); // This behavior is unspecced // We are supposed to dispatch synthetic click activation for Space and/or Return, // however *when* we do it is up to us. // Here, we're dispatching it after the key event so the script has a chance to cancel it // https://www.w3.org/Bugs/Public/show_bug.cgi?id=27337 if (keyboard_event.event.key == Key::Enter || keyboard_event.event.code == Code::Space) && keyboard_event.event.state == KeyState::Up { if let Some(elem) = target.downcast::<Element>() { elem.upcast::<Node>() .fire_synthetic_pointer_event_not_trusted(DOMString::from("click"), can_gc); } } } } pub(crate) fn dispatch_ime_event(&self, event: ImeEvent, can_gc: CanGc) { let composition_event = match event { ImeEvent::Dismissed => { self.request_focus( self.GetBody().as_ref().map(|e| e.upcast()), FocusInitiator::Local, can_gc, ); return; }, ImeEvent::Composition(composition_event) => composition_event, }; // spec: https://w3c.github.io/uievents/#compositionstart // spec: https://w3c.github.io/uievents/#compositionupdate // spec: https://w3c.github.io/uievents/#compositionend // > Event.target : focused element processing the composition let focused = self.get_focused_element(); let target = if let Some(elem) = &focused { elem.upcast() } else { // Event is only dispatched if there is a focused element. return; }; let cancelable = composition_event.state == keyboard_types::CompositionState::Start; let compositionevent = CompositionEvent::new( &self.window, DOMString::from(composition_event.state.to_string()), true, cancelable, Some(&self.window), 0, DOMString::from(composition_event.data), can_gc, ); let event = compositionevent.upcast::<Event>(); event.fire(target, can_gc); } // https://dom.spec.whatwg.org/#converting-nodes-into-a-node pub(crate) fn node_from_nodes_and_strings( &self, mut nodes: Vec<NodeOrString>, can_gc: CanGc, ) -> Fallible<DomRoot<Node>> { if nodes.len() == 1 { Ok(match nodes.pop().unwrap() { NodeOrString::Node(node) => node, NodeOrString::String(string) => { DomRoot::upcast(self.CreateTextNode(string, can_gc)) }, }) } else { let fragment = DomRoot::upcast::<Node>(self.CreateDocumentFragment(can_gc)); for node in nodes { match node { NodeOrString::Node(node) => { fragment.AppendChild(&node, can_gc)?; }, NodeOrString::String(string) => { let node = DomRoot::upcast::<Node>(self.CreateTextNode(string, can_gc)); // No try!() here because appending a text node // should not fail. fragment.AppendChild(&node, can_gc).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, local_name: &LocalName, value: DOMString, can_gc: CanGc, ) { 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(local_name, value, can_gc); } } pub(crate) fn set_current_script(&self, script: Option<&HTMLScriptElement>) { self.current_script.set(script); } pub(crate) fn get_script_blocking_stylesheets_count(&self) -> u32 { self.script_blocking_stylesheets_count.get() } pub(crate) fn increment_script_blocking_stylesheet_count(&self) { let count_cell = &self.script_blocking_stylesheets_count; count_cell.set(count_cell.get() + 1); } pub(crate) fn decrement_script_blocking_stylesheet_count(&self) { let count_cell = &self.script_blocking_stylesheets_count; assert!(count_cell.get() > 0); count_cell.set(count_cell.get() - 1); } pub(crate) fn invalidate_stylesheets(&self) { 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(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, can_gc: CanGc) { let _realm = enter_realm(self); self.running_animation_callbacks.set(true); let timing = self.global().performance().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(self, *timing, can_gc); } } self.running_animation_callbacks.set(false); if self.animation_frame_list.borrow().is_empty() { self.window().send_to_constellation( ScriptToConstellationMessage::ChangeRunningAnimationsState( AnimationState::NoAnimationCallbacksPresent, ), ); } } pub(crate) fn policy_container(&self) -> Ref<PolicyContainer> { self.policy_container.borrow() } /// Add the policy container and HTTPS state to a given request. /// /// TODO: Can this hapen for all requests that go through the document? pub(crate) fn prepare_request(&self, request: RequestBuilder) -> RequestBuilder { request .policy_container(self.policy_container().to_owned()) .https_state(self.https_state.get()) } pub(crate) fn fetch<Listener: FetchResponseListener + PreInvoke + Send + 'static>( &self, load: LoadType, mut request: RequestBuilder, listener: Listener, ) { request = request .insecure_requests_policy(self.insecure_requests_policy()) .has_trustworthy_ancestor_origin(self.has_trustworthy_ancestor_or_current_origin()); let callback = NetworkListener { context: std::sync::Arc::new(Mutex::new(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 + PreInvoke + Send + 'static>( &self, mut request: RequestBuilder, listener: Listener, ) { request = request .insecure_requests_policy(self.insecure_requests_policy()) .has_trustworthy_ancestor_origin(self.has_trustworthy_ancestor_or_current_origin()); let callback = NetworkListener { context: std::sync::Arc::new(Mutex::new(listener)), task_source: self .owner_global() .task_manager() .networking_task_source() .into(), } .into_callback(); self.loader_mut().fetch_async_background(request, callback); } // 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, can_gc: CanGc) { // 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(can_gc); // Step 3. self.process_deferred_scripts(can_gc); }, 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(can_gc); } // Deferred scripts have to wait for page to finish loading, // this is the first opportunity to process them. // Step 3. self.process_deferred_scripts(can_gc); }, _ => {}, } // 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.top_level_dom_complete.get().is_none() && loader.is_only_blocked_by_iframes() { update_with_current_instant(&self.top_level_dom_complete); } if loader.is_blocked() || loader.events_inhibited() { // Step 6. return; } ScriptThread::mark_document_with_no_blocked_loads(self); } // https://html.spec.whatwg.org/multipage/#prompt-to-unload-a-document pub(crate) fn prompt_to_unload(&self, recursive_flag: bool, can_gc: CanGc) -> 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( &self.window, atom!("beforeunload"), EventBubbles::Bubbles, EventCancelable::Cancelable, can_gc, ); 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(event, can_gc); // 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) = ipc::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 { // `prompt_to_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(); can_unload = document.prompt_to_unload(true, can_gc); 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, recursive_flag: bool, can_gc: CanGc) { // 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( &self.window, atom!("pagehide"), false, // bubbles false, // cancelable self.salvageable.get(), // persisted can_gc, ); let event = event.upcast::<Event>(); event.set_trusted(true); let _ = self .window .dispatch_event_with_target_override(event, can_gc); // Step 6 Update the visibility state of oldDocument to "hidden". self.update_visibility_state(DocumentVisibilityState::Hidden, can_gc); } // Step 7 if !self.fired_unload.get() { let event = Event::new( self.window.upcast(), atom!("unload"), EventBubbles::Bubbles, EventCancelable::Cancelable, can_gc, ); event.set_trusted(true); let event_target = self.window.upcast::<EventTarget>(); let has_listeners = event_target.has_listeners_for(&atom!("unload")); let _ = self .window .dispatch_event_with_target_override(&event, can_gc); 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(true, can_gc); if !document.salvageable() { self.salvageable.set(false); } } } let global_scope = self.window.as_global_scope(); // Step 10, 14 // https://html.spec.whatwg.org/multipage/#unloading-document-cleanup-steps if !self.salvageable.get() { // Step 1 of clean-up steps. global_scope.close_event_sources(); let msg = ScriptToConstellationMessage::DiscardDocument; let _ = global_scope.script_to_constellation_chan().send(msg); } // https://w3c.github.io/FileAPI/#lifeTime global_scope.clean_up_all_file_resources(); // Step 15, End self.decr_ignore_opens_during_unload_counter(); } // https://html.spec.whatwg.org/multipage/#the-end pub(crate) fn maybe_queue_document_completion(&self) { // 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; if not_ready_for_load { // Step 6. return; } assert!(!self.loader.borrow().events_inhibited()); self.loader.borrow_mut().inhibit_events(); // The rest will ever run only once per document. // Step 7. 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 || { let document = document.root(); let window = document.window(); if !window.is_alive() { return; } // Step 7.1. document.set_ready_state(DocumentReadyState::Complete, CanGc::note()); // Step 7.2. if document.browsing_context().is_none() { return; } let event = Event::new( window.upcast(), atom!("load"), EventBubbles::DoesNotBubble, EventCancelable::NotCancelable, CanGc::note(), ); event.set_trusted(true); // http://w3c.github.io/navigation-timing/#widl-PerformanceNavigationTiming-loadEventStart update_with_current_instant(&document.load_event_start); debug!("About to dispatch load for {:?}", document.url()); window.dispatch_event_with_target_override(&event, CanGc::note()); // http://w3c.github.io/navigation-timing/#widl-PerformanceNavigationTiming-loadEventEnd update_with_current_instant(&document.load_event_end); if let Some(fragment) = document.url().fragment() { document.check_and_scroll_fragment(fragment, CanGc::note()); } })); // Step 8. let document = Trusted::new(self); if document.root().browsing_context().is_some() { self.owner_global() .task_manager() .dom_manipulation_task_source() .queue(task!(fire_pageshow_event: move || { let document = document.root(); let window = document.window(); if document.page_showing.get() || !window.is_alive() { return; } document.page_showing.set(true); let event = PageTransitionEvent::new( window, atom!("pageshow"), false, // bubbles false, // cancelable false, // persisted CanGc::note(), ); let event = event.upcast::<Event>(); event.set_trusted(true); window.dispatch_event_with_target_override(event, CanGc::note()); })); } // 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().Xr().dispatch_sessionavailable(); } // Step 12: completely loaded. // https://html.spec.whatwg.org/multipage/#completely-loaded // TODO: fully implement "completely loaded". let document = Trusted::new(self); if document.root().browsing_context().is_some() { self.owner_global() .task_manager() .dom_manipulation_task_source() .queue(task!(completely_loaded: move || { let document = document.root(); document.completely_loaded.set(true); if let Some(DeclarativeRefresh::PendingLoad { url, time }) = &*document.declarative_refresh.borrow() { // https://html.spec.whatwg.org/multipage/#shared-declarative-refresh-steps document.window.as_global_scope().schedule_callback( OneshotTimerCallback::RefreshRedirectDue(RefreshRedirectDue { window: DomRoot::from_ref(document.window()), url: url.clone(), }), Duration::from_secs(*time), ); } // Note: this will, among others, result in the "iframe-load-event-steps" being run. // https://html.spec.whatwg.org/multipage/#iframe-load-event-steps document.notify_constellation_load(); })); } } pub(crate) fn completely_loaded(&self) -> bool { self.completely_loaded.get() } // 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, can_gc: CanGc, ) { { 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(can_gc); } fn process_pending_parsing_blocking_script(&self, can_gc: CanGc) { if self.script_blocking_stylesheets_count.get() > 0 { 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(&element, result, can_gc); } } // 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, element: &HTMLScriptElement, result: ScriptResult, can_gc: CanGc, ) { { let mut scripts = self.asap_scripts_set.borrow_mut(); let idx = scripts .iter() .position(|entry| &**entry == element) .unwrap(); scripts.swap_remove(idx); } element.execute(result, can_gc); } // 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, element: &HTMLScriptElement, result: ScriptResult, can_gc: CanGc, ) { 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(result, can_gc); } } // 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, element: &HTMLScriptElement, result: ScriptResult, can_gc: CanGc, ) { self.deferred_scripts.loaded(element, result); self.process_deferred_scripts(can_gc); } /// <https://html.spec.whatwg.org/multipage/#the-end> step 3. fn process_deferred_scripts(&self, can_gc: CanGc) { if self.ready_state.get() != DocumentReadyState::Interactive { return; } // Part of substep 1. loop { if self.script_blocking_stylesheets_count.get() > 0 { return; } if let Some((element, result)) = self.deferred_scripts.take_next_ready_to_be_executed() { element.execute(result, can_gc); } else { break; } } if self.deferred_scripts.is_empty() { // https://html.spec.whatwg.org/multipage/#the-end step 4. self.maybe_dispatch_dom_content_loaded(); } } // https://html.spec.whatwg.org/multipage/#the-end step 4. pub(crate) fn maybe_dispatch_dom_content_loaded(&self) { if self.domcontentloaded_dispatched.get() { return; } self.domcontentloaded_dispatched.set(true); assert_ne!( self.ReadyState(), DocumentReadyState::Complete, "Complete before DOMContentLoaded?" ); update_with_current_instant(&self.dom_content_loaded_event_start); // Step 4.1. let document = Trusted::new(self); self.owner_global() .task_manager() .dom_manipulation_task_source() .queue( task!(fire_dom_content_loaded_event: move || { let document = document.root(); document.upcast::<EventTarget>().fire_bubbling_event(atom!("DOMContentLoaded"), CanGc::note()); update_with_current_instant(&document.dom_content_loaded_event_end); }) ); // html parsing has finished - set dom content loaded self.interactive_time .borrow() .maybe_set_tti(InteractiveFlag::DOMContentLoaded); // Step 4.2. // TODO: client message queue. } // https://html.spec.whatwg.org/multipage/#abort-a-document pub(crate) fn abort(&self, can_gc: CanGc) { // We need to inhibit the loader before anything else. self.loader.borrow_mut().inhibit_events(); // Step 1. for iframe in self.iframes().iter() { if let Some(document) = iframe.GetContentDocument() { // TODO: abort the active documents of every child browsing context. document.abort(can_gc); // TODO: salvageable flag. } } // Step 2. self.script_blocking_stylesheets_count.set(0); *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.loader.borrow_mut().cancel_all_loads(); 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 let Some(parser) = self.get_current_parser() { self.active_parser_was_aborted.set(true); parser.abort(can_gc); self.salvageable.set(false); } } 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() } /// 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_mut().validate(self); 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().validate(self); self.iframes.borrow_mut() } pub(crate) fn get_dom_interactive(&self) -> Option<CrossProcessInstant> { self.dom_interactive.get() } 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 get_dom_content_loaded_event_start(&self) -> Option<CrossProcessInstant> { self.dom_content_loaded_event_start.get() } pub(crate) fn get_dom_content_loaded_event_end(&self) -> Option<CrossProcessInstant> { self.dom_content_loaded_event_end.get() } pub(crate) fn get_dom_complete(&self) -> Option<CrossProcessInstant> { self.dom_complete.get() } pub(crate) fn get_top_level_dom_complete(&self) -> Option<CrossProcessInstant> { self.top_level_dom_complete.get() } pub(crate) fn get_load_event_start(&self) -> Option<CrossProcessInstant> { self.load_event_start.get() } pub(crate) fn get_load_event_end(&self) -> Option<CrossProcessInstant> { self.load_event_end.get() } pub(crate) fn get_unload_event_start(&self) -> Option<CrossProcessInstant> { self.unload_event_start.get() } pub(crate) fn get_unload_event_end(&self) -> Option<CrossProcessInstant> { self.unload_event_end.get() } 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/#fire-a-focus-event fn fire_focus_event( &self, focus_event_type: FocusEventType, event_target: &EventTarget, related_target: Option<&EventTarget>, can_gc: CanGc, ) { let (event_name, does_bubble) = match focus_event_type { FocusEventType::Focus => (DOMString::from("focus"), EventBubbles::DoesNotBubble), FocusEventType::Blur => (DOMString::from("blur"), EventBubbles::DoesNotBubble), }; let event = FocusEvent::new( &self.window, event_name, does_bubble, EventCancelable::NotCancelable, Some(&self.window), 0i32, related_target, can_gc, ); let event = event.upcast::<Event>(); event.set_trusted(true); event.fire(event_target, can_gc); } /// <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()) } /// <https://html.spec.whatwg.org/multipage/#look-up-a-custom-element-definition> pub(crate) fn lookup_custom_element_definition( &self, namespace: &Namespace, local_name: &LocalName, is: Option<&LocalName>, ) -> Option<Rc<CustomElementDefinition>> { if !pref!(dom_customelements_enabled) { return None; } // Step 1 if *namespace != ns!(html) { return None; } // Step 2 if !self.has_browsing_context { return None; } // Step 3 let registry = self.window.CustomElements(); registry.lookup_definition(local_name, is) } 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) { for image in self.responsive_images.borrow().iter() { image.react_to_environment_changes(); } } 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, controls: &ShadowRoot) -> String { let id = Uuid::new_v4().to_string(); self.media_controls .borrow_mut() .insert(id.clone(), Dom::from_ref(controls)); id } pub(crate) fn unregister_media_controls(&self, id: &str, can_gc: CanGc) { if let Some(ref media_controls) = self.media_controls.borrow_mut().remove(id) { let media_controls = DomRoot::from_ref(&**media_controls); media_controls.Host().detach_shadow(can_gc); } else { debug_assert!(false, "Trying to unregister unknown media controls"); } } pub(crate) fn add_dirty_webgl_canvas(&self, context: &WebGLRenderingContext) { self.dirty_webgl_contexts .borrow_mut() .entry(context.context_id()) .or_insert_with(|| Dom::from_ref(context)); } pub(crate) fn flush_dirty_webgl_canvases(&self) { let dirty_context_ids: Vec<_> = self .dirty_webgl_contexts .borrow_mut() .drain() .filter(|(_, context)| context.onscreen()) .map(|(id, _)| id) .collect(); if dirty_context_ids.is_empty() { return; } #[allow(unused)] let mut time = 0; let (sender, receiver) = webgl::webgl_channel().unwrap(); self.window .webgl_chan() .expect("Where's the WebGL channel?") .send(WebGLMsg::SwapBuffers(dirty_context_ids, sender, time)) .unwrap(); receiver.recv().unwrap(); } pub(crate) fn add_dirty_2d_canvas(&self, context: &CanvasRenderingContext2D) { self.dirty_2d_contexts .borrow_mut() .entry(context.context_id()) .or_insert_with(|| Dom::from_ref(context)); } pub(crate) fn flush_dirty_2d_canvases(&self) { self.dirty_2d_contexts .borrow_mut() .drain() .filter(|(_, context)| context.onscreen()) .for_each(|(_, context)| context.update_rendering()); } #[cfg(feature = "webgpu")] pub(crate) fn webgpu_contexts(&self) -> WebGPUContextsMap { self.webgpu_contexts.clone() } #[cfg_attr(crown, allow(crown::unrooted_must_root))] #[cfg(feature = "webgpu")] pub(crate) fn update_rendering_of_webgpu_canvases(&self) { self.webgpu_contexts .borrow_mut() .iter() .filter_map(|(_, context)| context.root()) .filter(|context| context.onscreen()) .for_each(|context| context.update_rendering()); } pub(crate) fn id_map(&self) -> Ref<HashMapTracedValues<Atom, Vec<Dom<Element>>>> { self.id_map.borrow() } pub(crate) fn name_map(&self) -> Ref<HashMapTracedValues<Atom, Vec<Dom<Element>>>> { self.name_map.borrow() } /// <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, can_gc: CanGc, ) -> 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, can_gc, ); } has_active_resize_observations } /// <https://drafts.csswg.org/resize-observer/#broadcast-active-resize-observations> pub(crate) fn broadcast_active_resize_observations( &self, can_gc: CanGc, ) -> 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(&mut shallowest, can_gc); } 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, can_gc: CanGc) { 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(error_info, HandleValue::null(), can_gc); } 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, time: CrossProcessInstant, can_gc: CanGc, ) { // Step 1-2 for intersection_observer in &*self.intersection_observers.borrow() { self.update_single_intersection_observer_steps(intersection_observer, time, can_gc); } } /// Step 2.1-2.2 of <https://w3c.github.io/IntersectionObserver/#update-intersection-observations-algo> fn update_single_intersection_observer_steps( &self, intersection_observer: &IntersectionObserver, time: CrossProcessInstant, can_gc: CanGc, ) { // Step 1 // > Let rootBounds be observer’s root intersection rectangle. let root_bounds = intersection_observer.root_intersection_rectangle(self); // 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( self, time, root_bounds, can_gc, ); } /// <https://w3c.github.io/IntersectionObserver/#notify-intersection-observers-algo> pub(crate) fn notify_intersection_observers(&self, can_gc: CanGc) { // 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(can_gc); } } /// <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 || { document.root().notify_intersection_observers(CanGc::note()); })); } pub(crate) fn handle_paint_metric( &self, metric_type: ProgressiveWebMetricType, metric_value: CrossProcessInstant, first_reflow: bool, can_gc: CanGc, ) { let metrics = self.interactive_time.borrow(); match metric_type { ProgressiveWebMetricType::FirstPaint => { metrics.set_first_paint(metric_value, first_reflow) }, ProgressiveWebMetricType::FirstContentfulPaint => { metrics.set_first_contentful_paint(metric_value, first_reflow) }, ProgressiveWebMetricType::TimeToInteractive => { unreachable!("Unexpected non-paint metric.") }, } let entry = PerformancePaintTiming::new( self.window.as_global_scope(), metric_type, metric_value, can_gc, ); self.window .Performance() .queue_entry(entry.upcast::<PerformanceEntry>(), can_gc); } /// <https://html.spec.whatwg.org/multipage/#document-write-steps> fn write( &self, text: Vec<TrustedHTMLOrString>, line_feed: bool, containing_class: &str, field: &str, can_gc: CanGc, ) -> 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().to_owned()); }, 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_script_compliant_string( &self.global(), TrustedHTMLOrString::String(string.into()), containing_class, field, can_gc, )? .as_ref() .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); } // 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); } // Step 8: If document's active parser was aborted is true, then return. if !self.is_active() || 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(None, None, can_gc)?; self.get_current_parser().unwrap() }, }; // Steps 10-11. parser.write(string.into(), can_gc); Ok(()) } } fn is_character_value_key(key: &Key) -> bool { matches!(key, Key::Character(_) | Key::Enter) } #[derive(MallocSizeOf, PartialEq)] pub(crate) enum DocumentSource { FromParser, NotFromParser, } #[allow(unsafe_code)] pub(crate) trait LayoutDocumentHelpers<'dom> { fn is_html_document_for_layout(&self) -> bool; fn quirks_mode(self) -> QuirksMode; fn style_shared_lock(self) -> &'dom StyleSharedRwLock; fn shadow_roots(self) -> Vec<LayoutDom<'dom, ShadowRoot>>; fn shadow_roots_styles_changed(self) -> bool; fn flush_shadow_roots_stylesheets(self); } #[allow(unsafe_code)] impl<'dom> LayoutDocumentHelpers<'dom> for LayoutDom<'dom, Document> { #[inline] fn is_html_document_for_layout(&self) -> bool { self.unsafe_get().is_html_document } #[inline] fn quirks_mode(self) -> QuirksMode { self.unsafe_get().quirks_mode.get() } #[inline] fn style_shared_lock(self) -> &'dom StyleSharedRwLock { self.unsafe_get().style_shared_lock() } #[inline] fn shadow_roots(self) -> Vec<LayoutDom<'dom, ShadowRoot>> { // FIXME(nox): We should just return a // &'dom HashSet<LayoutDom<'dom, ShadowRoot>> here but not until // I rework the ToLayout trait as mentioned in // LayoutDom::to_layout_slice. unsafe { self.unsafe_get() .shadow_roots .borrow_for_layout() .iter() .map(|sr| sr.to_layout()) .collect() } } #[inline] fn shadow_roots_styles_changed(self) -> bool { self.unsafe_get().shadow_roots_styles_changed.get() } #[inline] fn flush_shadow_roots_stylesheets(self) { (*self.unsafe_get()).flush_shadow_roots_stylesheets() } } // 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. 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>, 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, ) -> 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; 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), // https://dom.spec.whatwg.org/#concept-document-quirks quirks_mode: Cell::new(QuirksMode::NoQuirks), id_map: DomRefCell::new(HashMapTracedValues::new()), name_map: DomRefCell::new(HashMapTracedValues::new()), // 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()), tagns_map: DomRefCell::new(HashMapTracedValues::new()), 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: Default::default(), style_shared_lock: { /// Per-process shared lock for author-origin stylesheets /// /// FIXME: make it per-document or per-pipeline instead: /// <https://github.com/servo/servo/issues/16027> /// (Need to figure out what to do with the style attribute /// of elements adopted into another document.) static PER_PROCESS_AUTHOR_SHARED_LOCK: LazyLock<StyleSharedRwLock> = LazyLock::new(StyleSharedRwLock::new); PER_PROCESS_AUTHOR_SHARED_LOCK.clone() //StyleSharedRwLock::new() }, stylesheets: DomRefCell::new(DocumentStylesheetSet::new()), stylesheet_list: MutNullableDom::new(None), ready_state: Cell::new(ready_state), domcontentloaded_dispatched: Cell::new(domcontentloaded_dispatched), focus_transaction: DomRefCell::new(None), focused: Default::default(), focus_sequence: Cell::new(FocusSequenceNumber::default()), has_focus: Cell::new(has_focus), current_script: Default::default(), pending_parsing_blocking_script: Default::default(), script_blocking_stylesheets_count: Cell::new(0u32), deferred_scripts: Default::default(), asap_in_order_scripts_list: Default::default(), asap_scripts_set: Default::default(), scripting_enabled: has_browsing_context, 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(), appropriate_template_contents_owner_document: Default::default(), pending_restyles: DomRefCell::new(HashMap::new()), needs_paint: Cell::new(false), active_touch_points: DomRefCell::new(Vec::new()), dom_interactive: Cell::new(Default::default()), dom_content_loaded_event_start: Cell::new(Default::default()), dom_content_loaded_event_end: Cell::new(Default::default()), dom_complete: Cell::new(Default::default()), top_level_dom_complete: Cell::new(Default::default()), load_event_start: Cell::new(Default::default()), load_event_end: Cell::new(Default::default()), unload_event_start: Cell::new(Default::default()), unload_event_end: Cell::new(Default::default()), https_state: Cell::new(HttpsState::None), origin, referrer, target_element: MutNullableDom::new(None), policy_container: DomRefCell::new(PolicyContainer::default()), last_click_info: DomRefCell::new(None), ignore_destructive_writes_counter: Default::default(), ignore_opens_during_unload_counter: Default::default(), spurious_animation_frames: Cell::new(0), dom_count: Cell::new(1), 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(), redirect_count: Cell::new(0), 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_2d_contexts: DomRefCell::new(HashMapTracedValues::new()), dirty_webgl_contexts: DomRefCell::new(HashMapTracedValues::new()), #[cfg(feature = "webgpu")] webgpu_contexts: Rc::new(RefCell::new(HashMapTracedValues::new())), selection: MutNullableDom::new(None), animation_timeline: if pref!(layout_animations_test_enabled) { DomRefCell::new(AnimationTimeline::new_for_testing()) } else { DomRefCell::new(AnimationTimeline::new()) }, animations: DomRefCell::new(Animations::new()), image_animation_manager: DomRefCell::new(ImageAnimationManager::new()), dirty_root: Default::default(), declarative_refresh: Default::default(), pending_input_events: Default::default(), mouse_move_event_index: 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(), active_keyboard_modifiers: Cell::new(Modifiers::empty()), highlighted_dom_node: 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() { 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) } /// Update the active keyboard modifiers for this [`Document`] while handling events. pub(crate) fn update_active_keyboard_modifiers(&self, modifiers: Modifiers) { self.active_keyboard_modifiers.set(modifiers); } pub(crate) fn alternate_action_keyboard_modifier_active(&self) -> bool { #[cfg(target_os = "macos")] { self.active_keyboard_modifiers .get() .contains(Modifiers::META) } #[cfg(not(target_os = "macos"))] { self.active_keyboard_modifiers .get() .contains(Modifiers::CONTROL) } } /// Note a pending compositor event, to be processed at the next `update_the_rendering` task. pub(crate) fn note_pending_input_event(&self, event: ConstellationInputEvent) { let mut pending_compositor_events = self.pending_input_events.borrow_mut(); if matches!(event.event, InputEvent::MouseMove(..)) { // First try to replace any existing mouse move event. if let Some(mouse_move_event) = self .mouse_move_event_index .borrow() .and_then(|index| pending_compositor_events.get_mut(index)) { *mouse_move_event = event; return; } *self.mouse_move_event_index.borrow_mut() = Some(pending_compositor_events.len()); } pending_compositor_events.push(event); } /// Get pending compositor events, for processing within an `update_the_rendering` task. pub(crate) fn take_pending_input_events(&self) -> Vec<ConstellationInputEvent> { // Reset the mouse event index. *self.mouse_move_event_index.borrow_mut() = None; mem::take(&mut *self.pending_input_events.borrow_mut()) } pub(crate) fn set_csp_list(&self, csp_list: Option<CspList>) { self.policy_container.borrow_mut().set_csp_list(csp_list); } pub(crate) fn get_csp_list(&self) -> Option<CspList> { self.policy_container.borrow().csp_list.clone() } /// <https://www.w3.org/TR/CSP/#should-block-inline> pub(crate) fn should_elements_inline_type_behavior_be_blocked( &self, el: &Element, type_: csp::InlineCheckType, source: &str, ) -> csp::CheckResult { let (result, violations) = match self.get_csp_list() { None => { return csp::CheckResult::Allowed; }, Some(csp_list) => { let element = csp::Element { nonce: el.nonce_value_if_nonceable().map(Cow::Owned), }; csp_list.should_elements_inline_type_behavior_be_blocked(&element, type_, source) }, }; self.global().report_csp_violations(violations, Some(el)); result } /// 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) { 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(); } } /// Enqueue a task to run as soon as any JS and layout blockers are removed. pub(crate) fn add_delayed_task<T: 'static + TaskBox>(&self, task: T) { self.delayed_tasks.borrow_mut().push(Box::new(task)); } /// 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_eq!( self.script_and_layout_blockers.get(), 0, "Attempt to use script or layout while DOM not in a stable state" ); } #[allow(clippy::too_many_arguments)] pub(crate) fn new( window: &Window, has_browsing_context: HasBrowsingContext, 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, can_gc: CanGc, ) -> DomRoot<Document> { Self::new_with_proto( window, None, has_browsing_context, 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, can_gc, ) } #[allow(clippy::too_many_arguments)] fn new_with_proto( window: &Window, proto: Option<HandleObject>, has_browsing_context: HasBrowsingContext, 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, can_gc: CanGc, ) -> DomRoot<Document> { let document = reflect_dom_object_with_proto( Box::new(Document::new_inherited( window, has_browsing_context, 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, )), window, proto, can_gc, ); { let node = document.upcast::<Node>(); node.set_owner_doc(&document); } document } pub(crate) fn get_redirect_count(&self) -> u16 { self.redirect_count.get() } pub(crate) fn set_redirect_count(&self, count: u16) { self.redirect_count.set(count) } 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( &self, index: u32, name: &DOMString, ) -> Option<DomRoot<Node>> { if name.is_empty() { return None; } self.nth_in_node_list(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<F: Fn(&Node) -> bool>( &self, index: u32, callback: F, ) -> Option<DomRoot<Node>> { 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)) .nth(index as usize) .map(|n| DomRoot::from_ref(&*n)) } fn get_html_element(&self) -> Option<DomRoot<HTMLHtmlElement>> { self.GetDocumentElement().and_then(DomRoot::downcast) } /// Return a reference to the per-document shared lock used in stylesheets. pub(crate) fn style_shared_lock(&self) -> &StyleSharedRwLock { &self.style_shared_lock } /// 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/#appropriate-template-contents-owner-document> pub(crate) fn appropriate_template_contents_owner_document( &self, can_gc: CanGc, ) -> 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( self.window(), HasBrowsingContext::No, 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(), can_gc, ); new_doc .appropriate_template_contents_owner_document .set(Some(&new_doc)); new_doc }) } pub(crate) fn get_element_by_id(&self, id: &Atom) -> Option<DomRoot<Element>> { self.id_map .borrow() .get(id) .map(|elements| DomRoot::from_ref(&*elements[0])) } 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_state_will_change(&self, el: &Element) { let mut entry = self.ensure_pending_restyle(el); if entry.snapshot.is_none() { entry.snapshot = Some(Snapshot::new()); } let snapshot = entry.snapshot.as_mut().unwrap(); if snapshot.state.is_none() { snapshot.state = Some(el.state()); } } pub(crate) fn element_attr_will_change(&self, el: &Element, attr: &Attr) { // 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) { 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() .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); } // https://fullscreen.spec.whatwg.org/#dom-element-requestfullscreen pub(crate) fn enter_fullscreen(&self, pending: &Element, can_gc: CanGc) -> Rc<Promise> { // Step 1 let in_realm_proof = AlreadyInRealm::assert::<crate::DomTypeHolder>(); let promise = Promise::new_in_current_realm(InRealm::Already(&in_realm_proof), can_gc); let mut error = false; // Step 4 // check namespace match *pending.namespace() { ns!(mathml) => { if pending.local_name().as_ref() != "math" { error = true; } }, ns!(svg) => { if pending.local_name().as_ref() != "svg" { error = true; } }, ns!(html) => (), _ => error = true, } // fullscreen element ready check if !pending.fullscreen_element_ready_check() { error = true; } if pref!(dom_fullscreen_test) { // For reftests we just take over the current window, // and don't try to really enter fullscreen. info!("Tests don't really enter fullscreen."); } else { // TODO fullscreen is supported // TODO This algorithm is allowed to request fullscreen. warn!("Fullscreen not supported yet"); } // Step 5 Parallel start let window = self.window(); // Step 6 if !error { let event = EmbedderMsg::NotifyFullscreenStateChanged(self.webview_id(), true); self.send_to_embedder(event); } let pipeline_id = self.window().pipeline_id(); // Step 7 let trusted_pending = Trusted::new(pending); let trusted_promise = TrustedPromise::new(promise.clone()); let handler = ElementPerformFullscreenEnter::new(trusted_pending, trusted_promise, error); // NOTE: This steps should be running in parallel // https://fullscreen.spec.whatwg.org/#dom-element-requestfullscreen let script_msg = CommonScriptMsg::Task( ScriptThreadEventCategory::EnterFullscreen, handler, Some(pipeline_id), TaskSourceName::DOMManipulation, ); let msg = MainThreadScriptMsg::Common(script_msg); window.main_thread_script_chan().send(msg).unwrap(); promise } // https://fullscreen.spec.whatwg.org/#exit-fullscreen pub(crate) fn exit_fullscreen(&self, can_gc: CanGc) -> Rc<Promise> { let global = self.global(); // Step 1 let in_realm_proof = AlreadyInRealm::assert::<crate::DomTypeHolder>(); let promise = Promise::new_in_current_realm(InRealm::Already(&in_realm_proof), can_gc); // Step 2 if self.fullscreen_element.get().is_none() { promise.reject_error(Error::Type(String::from("fullscreen is null")), can_gc); return promise; } // TODO Step 3-6 let element = self.fullscreen_element.get().unwrap(); // Step 7 Parallel start let window = self.window(); // Step 8 let event = EmbedderMsg::NotifyFullscreenStateChanged(self.webview_id(), false); self.send_to_embedder(event); // Step 9 let trusted_element = Trusted::new(&*element); let trusted_promise = TrustedPromise::new(promise.clone()); let handler = ElementPerformFullscreenExit::new(trusted_element, trusted_promise); let pipeline_id = Some(global.pipeline_id()); // NOTE: This steps should be running in parallel // https://fullscreen.spec.whatwg.org/#exit-fullscreen let script_msg = CommonScriptMsg::Task( ScriptThreadEventCategory::ExitFullscreen, handler, pipeline_id, TaskSourceName::DOMManipulation, ); let msg = MainThreadScriptMsg::Common(script_msg); window.main_thread_script_chan().send(msg).unwrap(); promise } pub(crate) fn set_fullscreen_element(&self, element: Option<&Element>) { self.fullscreen_element.set(element); } pub(crate) fn get_allow_fullscreen(&self) -> bool { // https://html.spec.whatwg.org/multipage/#allowed-to-use match self.browsing_context() { // Step 1 None => false, Some(_) => { // Step 2 let window = self.window(); if window.is_top_level() { true } else { // Step 3 window .GetFrameElement() .is_some_and(|el| el.has_attribute(&local_name!("allowfullscreen"))) } }, } } fn reset_form_owner_for_listeners(&self, id: &Atom, can_gc: CanGc) { 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(can_gc); } } } 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 shadow_roots_styles_changed(&self) -> bool { self.shadow_roots_styles_changed.get() } pub(crate) fn flush_shadow_roots_stylesheets(&self) { if !self.shadow_roots_styles_changed.get() { return; } self.shadow_roots_styles_changed.set(false); } pub(crate) fn stylesheet_count(&self) -> usize { self.stylesheets.borrow().len() } pub(crate) fn stylesheet_at(&self, index: usize) -> Option<DomRoot<CSSStyleSheet>> { let stylesheets = self.stylesheets.borrow(); stylesheets .get(Origin::Author, index) .and_then(|s| s.owner.upcast::<Node>().get_cssom_stylesheet()) } /// Add a stylesheet owned by `owner` to the list of document sheets, in the /// correct tree position. #[cfg_attr(crown, allow(crown::unrooted_must_root))] // Owner needs to be rooted already necessarily. pub(crate) fn add_stylesheet(&self, owner: &Element, sheet: Arc<Stylesheet>) { let stylesheets = &mut *self.stylesheets.borrow_mut(); let insertion_point = stylesheets .iter() .map(|(sheet, _origin)| sheet) .find(|sheet_in_doc| { owner .upcast::<Node>() .is_before(sheet_in_doc.owner.upcast()) }) .cloned(); if self.has_browsing_context() { self.window.layout_mut().add_stylesheet( sheet.clone(), insertion_point.as_ref().map(|s| s.sheet.clone()), ); } DocumentOrShadowRoot::add_stylesheet( owner, StylesheetSetRef::Document(stylesheets), sheet, insertion_point, self.style_shared_lock(), ); } /// Given a stylesheet, load all web fonts from it in Layout. pub(crate) fn load_web_fonts_from_stylesheet(&self, stylesheet: Arc<Stylesheet>) { self.window .layout() .load_web_fonts_from_stylesheet(stylesheet); } /// Remove a stylesheet owned by `owner` from the list of document sheets. #[cfg_attr(crown, allow(crown::unrooted_must_root))] // Owner needs to be rooted already necessarily. pub(crate) fn remove_stylesheet(&self, owner: &Element, 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, id: &Atom) -> Ref<[Dom<Element>]> { Ref::map(self.id_map.borrow(), |map| { map.get(id).map(|vec| &**vec).unwrap_or_default() }) } pub(crate) fn get_elements_with_name(&self, name: &Atom) -> Ref<[Dom<Element>]> { Ref::map(self.name_map.borrow(), |map| { map.get(name).map(|vec| &**vec).unwrap_or_default() }) } #[cfg_attr(crown, allow(crown::unrooted_must_root))] pub(crate) fn drain_pending_restyles(&self) -> Vec<(TrustedNodeAddress, PendingRestyle)> { self.pending_restyles .borrow_mut() .drain() .filter_map(|(elem, restyle)| { let node = elem.upcast::<Node>(); if !node.get_flag(NodeFlags::IS_CONNECTED) { return None; } node.note_dirty_descendants(); Some((node.to_trusted_node_address(), restyle.0)) }) .collect() } pub(crate) fn advance_animation_timeline_for_testing(&self, delta: f64) { self.animation_timeline.borrow_mut().advance_specific(delta); let current_timeline_value = self.current_animation_timeline_value(); self.animations .borrow() .update_for_new_timeline_value(&self.window, current_timeline_value); } pub(crate) fn maybe_mark_animating_nodes_as_dirty(&self) { let current_timeline_value = self.current_animation_timeline_value(); self.animations .borrow() .mark_animating_nodes_as_dirty(current_timeline_value); } pub(crate) fn current_animation_timeline_value(&self) -> f64 { self.animation_timeline.borrow().current_value() } pub(crate) fn animations(&self) -> Ref<Animations> { self.animations.borrow() } pub(crate) fn update_animations_post_reflow(&self) { self.animations .borrow() .do_post_reflow_update(&self.window, self.current_animation_timeline_value()); } pub(crate) fn cancel_animations_for_node(&self, node: &Node) { self.animations.borrow().cancel_animations_for_node(node); } /// An implementation of <https://drafts.csswg.org/web-animations-1/#update-animations-and-send-events>. pub(crate) fn update_animations_and_send_events(&self, can_gc: CanGc) { // Only update the time if it isn't being managed by a test. if !pref!(layout_animations_test_enabled) { self.animation_timeline.borrow_mut().update(); } // > 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 .borrow() .update_for_new_timeline_value(&self.window, current_timeline_value); self.maybe_mark_animating_nodes_as_dirty(); // > 3. Perform a microtask checkpoint. self.window() .as_global_scope() .perform_a_microtask_checkpoint(can_gc); // Steps 4 through 7 occur inside `send_pending_events().` let _realm = enter_realm(self); self.animations().send_pending_events(self.window(), can_gc); } pub(crate) fn image_animation_manager(&self) -> Ref<ImageAnimationManager> { self.image_animation_manager.borrow() } pub(crate) fn image_animation_manager_mut(&self) -> RefMut<ImageAnimationManager> { self.image_animation_manager.borrow_mut() } pub(crate) fn update_animating_images(&self) { let mut image_animation_manager = self.image_animation_manager.borrow_mut(); if !image_animation_manager.image_animations_present() { return; } image_animation_manager .update_active_frames(&self.window, self.current_animation_timeline_value()); if !self.animations().animations_present() { let next_scheduled_time = image_animation_manager.next_schedule_time(self.current_animation_timeline_value()); // TODO: Once we have refresh signal from the compositor, // we should get rid of timer for animated image update. if let Some(next_scheduled_time) = next_scheduled_time { self.schedule_image_animation_update(next_scheduled_time); } } } fn schedule_image_animation_update(&self, next_scheduled_time: f64) { let callback = ImageAnimationUpdateCallback { document: Trusted::new(self), }; self.global().schedule_callback( OneshotTimerCallback::ImageAnimationUpdate(callback), Duration::from_secs_f64(next_scheduled_time), ); } /// <https://html.spec.whatwg.org/multipage/#shared-declarative-refresh-steps> pub(crate) fn shared_declarative_refresh_steps(&self, content: &[u8]) { // 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; } } // 12. Set document's will declaratively refresh to true. if self.completely_loaded() { // TODO: handle active sandboxing flag self.window.as_global_scope().schedule_callback( OneshotTimerCallback::RefreshRedirectDue(RefreshRedirectDue { window: DomRoot::from_ref(self.window()), url: url_record, }), Duration::from_secs(time), ); self.set_declarative_refresh(DeclarativeRefresh::CreatedAfterLoad); } else { self.set_declarative_refresh(DeclarativeRefresh::PendingLoad { url: url_record, time, }); } } 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, visibility_state: DocumentVisibilityState, can_gc: CanGc) { // 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( &self.global(), visibility_state, CrossProcessInstant::now(), can_gc, ); self.window .Performance() .queue_entry(entry.upcast::<PerformanceEntry>(), can_gc); // 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> if visibility_state == DocumentVisibilityState::Hidden { self.window .Navigator() .GetGamepads() .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(atom!("visibilitychange"), can_gc); } /// <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 fn allow_declarative_shadow_roots(&self) -> bool { self.allow_declarative_shadow_roots.get() } pub fn set_allow_declarative_shadow_roots(&self, value: bool) { self.allow_declarative_shadow_roots.set(value) } pub fn has_trustworthy_ancestor_origin(&self) -> bool { self.has_trustworthy_ancestor_origin.get() } pub 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.set_needs_paint(true); } pub(crate) fn highlighted_dom_node(&self) -> Option<DomRoot<Node>> { self.highlighted_dom_node.get() } } #[allow(non_snake_case)] impl DocumentMethods<crate::DomTypeHolder> for Document { // https://dom.spec.whatwg.org/#dom-document-document fn Constructor( window: &Window, proto: Option<HandleObject>, can_gc: CanGc, ) -> Fallible<DomRoot<Document>> { let doc = window.Document(); let docloader = DocumentLoader::new(&doc.loader()); Ok(Document::new_with_proto( window, proto, HasBrowsingContext::No, 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(), can_gc, )) } // https://w3c.github.io/editing/ActiveDocuments/execCommand.html#querycommandsupported() fn QueryCommandSupported(&self, _command: DOMString) -> bool { false } // https://drafts.csswg.org/cssom/#dom-document-stylesheets fn StyleSheets(&self, can_gc: CanGc) -> DomRoot<StyleSheetList> { self.stylesheet_list.or_init(|| { StyleSheetList::new( &self.window, StyleSheetListOwner::Document(Dom::from_ref(self)), can_gc, ) }) } // https://dom.spec.whatwg.org/#dom-document-implementation fn Implementation(&self, can_gc: CanGc) -> DomRoot<DOMImplementation> { self.implementation .or_init(|| DOMImplementation::new(self, can_gc)) } // 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.get_active_element( self.get_focused_element(), self.GetBody(), self.GetDocumentElement(), ) } // 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.has_focus.get() } } // https://html.spec.whatwg.org/multipage/#dom-document-domain fn Domain(&self) -> DOMString { // Step 1. if !self.has_browsing_context { return DOMString::new(); } // Step 2. match self.origin.effective_domain() { // Step 3. None => DOMString::new(), // Step 4. 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 !self.has_browsing_context { return Err(Error::Security); } // TODO: 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." // Steps 3-4. let effective_domain = match self.origin.effective_domain() { Some(effective_domain) => effective_domain, None => return Err(Error::Security), }; // Step 5 let host = match get_registrable_domain_suffix_of_or_is_equal_to(&value, effective_domain) { None => return Err(Error::Security), Some(host) => host, }; // Step 6 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() .filter_map(DomRoot::downcast) .next() } // 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, qualified_name: DOMString, can_gc: CanGc, ) -> 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( &self.window, self.upcast(), qualified_name.clone(), can_gc, ); self.tag_map .borrow_mut() .insert(qualified_name, Dom::from_ref(&*result)); result } // https://dom.spec.whatwg.org/#dom-document-getelementsbytagnamens fn GetElementsByTagNameNS( &self, maybe_ns: Option<DOMString>, tag_name: DOMString, can_gc: CanGc, ) -> 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(&self.window, self.upcast(), qname.clone(), can_gc); self.tagns_map .borrow_mut() .insert(qname, Dom::from_ref(&*result)); result } // https://dom.spec.whatwg.org/#dom-document-getelementsbyclassname fn GetElementsByClassName(&self, classes: DOMString, can_gc: CanGc) -> DomRoot<HTMLCollection> { let class_atoms: Vec<Atom> = split_html_space_chars(&classes).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( &self.window, self.upcast(), class_atoms.clone(), can_gc, ); self.classes_map .borrow_mut() .insert(class_atoms, Dom::from_ref(&*result)); result } // https://dom.spec.whatwg.org/#dom-nonelementparentnode-getelementbyid fn GetElementById(&self, id: DOMString) -> Option<DomRoot<Element>> { self.get_element_by_id(&Atom::from(id)) } /// <https://dom.spec.whatwg.org/#dom-document-createelement> fn CreateElement( &self, mut local_name: DOMString, options: StringOrElementCreationOptions, can_gc: CanGc, ) -> Fallible<DomRoot<Element>> { // Step 1. If localName does not match the Name production, then throw an "InvalidCharacterError" DOMException. if !matches_name_production(&local_name) { debug!("Not a valid element name"); return Err(Error::InvalidCharacter); } 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(|is| LocalName::from(&**is)) }, }; Ok(Element::create( name, is, self, ElementCreator::ScriptCreated, CustomElementCreationMode::Synchronous, None, can_gc, )) } /// <https://dom.spec.whatwg.org/#dom-document-createelementns> fn CreateElementNS( &self, namespace: Option<DOMString>, qualified_name: DOMString, options: StringOrElementCreationOptions, can_gc: CanGc, ) -> Fallible<DomRoot<Element>> { // Step 1. Let namespace, prefix, and localName be the result of passing namespace and qualifiedName // to validate and extract. let (namespace, prefix, local_name) = validate_and_extract(namespace, &qualified_name)?; // 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(|is| LocalName::from(&**is)) }, }; // Step 4. Return the result of creating an element given document, localName, namespace, prefix, is, and true. Ok(Element::create( name, is, self, ElementCreator::ScriptCreated, CustomElementCreationMode::Synchronous, None, can_gc, )) } /// <https://dom.spec.whatwg.org/#dom-document-createattribute> fn CreateAttribute(&self, mut local_name: DOMString, can_gc: CanGc) -> Fallible<DomRoot<Attr>> { // Step 1. If localName does not match the Name production in XML, // then throw an "InvalidCharacterError" DOMException. if !matches_name_production(&local_name) { debug!("Not a valid element name"); return Err(Error::InvalidCharacter); } 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( self, name.clone(), value, name, ns!(), None, None, can_gc, )) } // https://dom.spec.whatwg.org/#dom-document-createattributens fn CreateAttributeNS( &self, namespace: Option<DOMString>, qualified_name: DOMString, can_gc: CanGc, ) -> Fallible<DomRoot<Attr>> { let (namespace, prefix, local_name) = validate_and_extract(namespace, &qualified_name)?; let value = AttrValue::String("".to_owned()); let qualified_name = LocalName::from(qualified_name); Ok(Attr::new( self, local_name, value, qualified_name, namespace, prefix, None, can_gc, )) } // https://dom.spec.whatwg.org/#dom-document-createdocumentfragment fn CreateDocumentFragment(&self, can_gc: CanGc) -> DomRoot<DocumentFragment> { DocumentFragment::new(self, can_gc) } // https://dom.spec.whatwg.org/#dom-document-createtextnode fn CreateTextNode(&self, data: DOMString, can_gc: CanGc) -> DomRoot<Text> { Text::new(data, self, can_gc) } // https://dom.spec.whatwg.org/#dom-document-createcdatasection fn CreateCDATASection( &self, data: DOMString, can_gc: CanGc, ) -> Fallible<DomRoot<CDATASection>> { // Step 1 if self.is_html_document { return Err(Error::NotSupported); } // Step 2 if data.contains("]]>") { return Err(Error::InvalidCharacter); } // Step 3 Ok(CDATASection::new(data, self, can_gc)) } // https://dom.spec.whatwg.org/#dom-document-createcomment fn CreateComment(&self, data: DOMString, can_gc: CanGc) -> DomRoot<Comment> { Comment::new(data, self, None, can_gc) } // https://dom.spec.whatwg.org/#dom-document-createprocessinginstruction fn CreateProcessingInstruction( &self, target: DOMString, data: DOMString, can_gc: CanGc, ) -> Fallible<DomRoot<ProcessingInstruction>> { // Step 1. If target does not match the Name production, then throw an "InvalidCharacterError" DOMException. if !matches_name_production(&target) { return Err(Error::InvalidCharacter); } // Step 2. if data.contains("?>") { return Err(Error::InvalidCharacter); } // Step 3. Ok(ProcessingInstruction::new(target, data, self, can_gc)) } // https://dom.spec.whatwg.org/#dom-document-importnode fn ImportNode(&self, node: &Node, deep: bool, can_gc: CanGc) -> Fallible<DomRoot<Node>> { // Step 1. if node.is::<Document>() || node.is::<ShadowRoot>() { return Err(Error::NotSupported); } // Step 2. let clone_children = if deep { CloneChildrenFlag::CloneChildren } else { CloneChildrenFlag::DoNotCloneChildren }; Ok(Node::clone(node, Some(self), clone_children, can_gc)) } // https://dom.spec.whatwg.org/#dom-document-adoptnode fn AdoptNode(&self, node: &Node, can_gc: CanGc) -> Fallible<DomRoot<Node>> { // Step 1. if node.is::<Document>() { return Err(Error::NotSupported); } // Step 2. if node.is::<ShadowRoot>() { return Err(Error::HierarchyRequest); } // Step 3. Node::adopt(node, self, can_gc); // Step 4. Ok(DomRoot::from_ref(node)) } // https://dom.spec.whatwg.org/#dom-document-createevent fn CreateEvent(&self, mut interface: DOMString, can_gc: CanGc) -> Fallible<DomRoot<Event>> { interface.make_ascii_lowercase(); match &*interface { "beforeunloadevent" => Ok(DomRoot::upcast(BeforeUnloadEvent::new_uninitialized( &self.window, can_gc, ))), "compositionevent" | "textevent" => Ok(DomRoot::upcast( CompositionEvent::new_uninitialized(&self.window, can_gc), )), "customevent" => Ok(DomRoot::upcast(CustomEvent::new_uninitialized( self.window.upcast(), can_gc, ))), // FIXME(#25136): devicemotionevent, deviceorientationevent // FIXME(#7529): dragevent "events" | "event" | "htmlevents" | "svgevents" => { Ok(Event::new_uninitialized(self.window.upcast(), can_gc)) }, "focusevent" => Ok(DomRoot::upcast(FocusEvent::new_uninitialized( &self.window, can_gc, ))), "hashchangeevent" => Ok(DomRoot::upcast(HashChangeEvent::new_uninitialized( &self.window, can_gc, ))), "keyboardevent" => Ok(DomRoot::upcast(KeyboardEvent::new_uninitialized( &self.window, can_gc, ))), "messageevent" => Ok(DomRoot::upcast(MessageEvent::new_uninitialized( self.window.upcast(), can_gc, ))), "mouseevent" | "mouseevents" => Ok(DomRoot::upcast(MouseEvent::new_uninitialized( &self.window, can_gc, ))), "storageevent" => Ok(DomRoot::upcast(StorageEvent::new_uninitialized( &self.window, "".into(), can_gc, ))), "touchevent" => Ok(DomRoot::upcast(DomTouchEvent::new_uninitialized( &self.window, &TouchList::new(&self.window, &[], can_gc), &TouchList::new(&self.window, &[], can_gc), &TouchList::new(&self.window, &[], can_gc), can_gc, ))), "uievent" | "uievents" => Ok(DomRoot::upcast(UIEvent::new_uninitialized( &self.window, can_gc, ))), _ => Err(Error::NotSupported), } } // 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, can_gc: CanGc) -> DomRoot<Range> { Range::new_with_doc(self, None, can_gc) } // https://dom.spec.whatwg.org/#dom-document-createnodeiteratorroot-whattoshow-filter fn CreateNodeIterator( &self, root: &Node, what_to_show: u32, filter: Option<Rc<NodeFilter>>, can_gc: CanGc, ) -> DomRoot<NodeIterator> { NodeIterator::new(self, root, what_to_show, filter, can_gc) } // https://dom.spec.whatwg.org/#dom-document-createtreewalker fn CreateTreeWalker( &self, root: &Node, what_to_show: u32, filter: Option<Rc<NodeFilter>>, ) -> DomRoot<TreeWalker> { TreeWalker::new(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, title: DOMString, can_gc: CanGc) { let root = match self.GetDocumentElement() { Some(root) => root, None => return, }; let elem = if root.namespace() == &ns!(svg) && root.local_name() == &local_name!("svg") { let elem = root.upcast::<Node>().child_elements().find(|node| { node.namespace() == &ns!(svg) && node.local_name() == &local_name!("title") }); match elem { Some(elem) => DomRoot::upcast::<Node>(elem), None => { let name = QualName::new(None, ns!(svg), local_name!("title")); let elem = Element::create( name, None, self, ElementCreator::ScriptCreated, CustomElementCreationMode::Synchronous, None, can_gc, ); let parent = root.upcast::<Node>(); let child = elem.upcast::<Node>(); parent .InsertBefore(child, parent.GetFirstChild().as_deref(), can_gc) .unwrap() }, } } else if root.namespace() == &ns!(html) { let elem = root .upcast::<Node>() .traverse_preorder(ShadowIncluding::No) .find(|node| node.is::<HTMLTitleElement>()); match elem { Some(elem) => elem, None => match self.GetHead() { Some(head) => { let name = QualName::new(None, ns!(html), local_name!("title")); let elem = Element::create( name, None, self, ElementCreator::ScriptCreated, CustomElementCreationMode::Synchronous, None, can_gc, ); head.upcast::<Node>() .AppendChild(elem.upcast(), can_gc) .unwrap() }, None => return, }, } } else { return; }; elem.SetTextContent(Some(title), can_gc); } // 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() .filter_map(DomRoot::downcast) .next() }) } // 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>> { 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, new_body: Option<&HTMLElement>, can_gc: CanGc) -> ErrorResult { // Step 1. let new_body = match new_body { Some(new_body) => new_body, None => return Err(Error::HierarchyRequest), }; 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), } // Step 2. let old_body = self.GetBody(); if old_body.as_deref() == Some(new_body) { return Ok(()); } match (self.GetDocumentElement(), &old_body) { // Step 3. (Some(ref root), Some(child)) => { let root = root.upcast::<Node>(); root.ReplaceChild(new_body.upcast(), child.upcast(), can_gc) .unwrap(); }, // Step 4. (None, _) => return Err(Error::HierarchyRequest), // Step 5. (Some(ref root), &None) => { let root = root.upcast::<Node>(); root.AppendChild(new_body.upcast(), can_gc).unwrap(); }, } Ok(()) } // https://html.spec.whatwg.org/multipage/#dom-document-getelementsbyname fn GetElementsByName(&self, name: DOMString, can_gc: CanGc) -> DomRoot<NodeList> { NodeList::new_elements_by_name_list(self.window(), self, name, can_gc) } // https://html.spec.whatwg.org/multipage/#dom-document-images fn Images(&self, can_gc: CanGc) -> DomRoot<HTMLCollection> { self.images.or_init(|| { HTMLCollection::new_with_filter_fn( &self.window, self.upcast(), |element, _| element.is::<HTMLImageElement>(), can_gc, ) }) } // https://html.spec.whatwg.org/multipage/#dom-document-embeds fn Embeds(&self, can_gc: CanGc) -> DomRoot<HTMLCollection> { self.embeds.or_init(|| { HTMLCollection::new_with_filter_fn( &self.window, self.upcast(), |element, _| element.is::<HTMLEmbedElement>(), can_gc, ) }) } // https://html.spec.whatwg.org/multipage/#dom-document-plugins fn Plugins(&self, can_gc: CanGc) -> DomRoot<HTMLCollection> { self.Embeds(can_gc) } // https://html.spec.whatwg.org/multipage/#dom-document-links fn Links(&self, can_gc: CanGc) -> DomRoot<HTMLCollection> { self.links.or_init(|| { HTMLCollection::new_with_filter_fn( &self.window, self.upcast(), |element, _| { (element.is::<HTMLAnchorElement>() || element.is::<HTMLAreaElement>()) && element.has_attribute(&local_name!("href")) }, can_gc, ) }) } // https://html.spec.whatwg.org/multipage/#dom-document-forms fn Forms(&self, can_gc: CanGc) -> DomRoot<HTMLCollection> { self.forms.or_init(|| { HTMLCollection::new_with_filter_fn( &self.window, self.upcast(), |element, _| element.is::<HTMLFormElement>(), can_gc, ) }) } // https://html.spec.whatwg.org/multipage/#dom-document-scripts fn Scripts(&self, can_gc: CanGc) -> DomRoot<HTMLCollection> { self.scripts.or_init(|| { HTMLCollection::new_with_filter_fn( &self.window, self.upcast(), |element, _| element.is::<HTMLScriptElement>(), can_gc, ) }) } // https://html.spec.whatwg.org/multipage/#dom-document-anchors fn Anchors(&self, can_gc: CanGc) -> DomRoot<HTMLCollection> { self.anchors.or_init(|| { HTMLCollection::new_with_filter_fn( &self.window, self.upcast(), |element, _| { element.is::<HTMLAnchorElement>() && element.has_attribute(&local_name!("href")) }, can_gc, ) }) } // https://html.spec.whatwg.org/multipage/#dom-document-applets fn Applets(&self, can_gc: CanGc) -> DomRoot<HTMLCollection> { self.applets .or_init(|| HTMLCollection::always_empty(&self.window, self.upcast(), can_gc)) } // https://html.spec.whatwg.org/multipage/#dom-document-location fn GetLocation(&self) -> Option<DomRoot<Location>> { if self.is_fully_active() { Some(self.window.Location()) } else { None } } // https://dom.spec.whatwg.org/#dom-parentnode-children fn Children(&self, can_gc: CanGc) -> DomRoot<HTMLCollection> { HTMLCollection::children(&self.window, self.upcast(), can_gc) } // 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() .filter_map(DomRoot::downcast) .next() } // 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, nodes: Vec<NodeOrString>, can_gc: CanGc) -> ErrorResult { self.upcast::<Node>().prepend(nodes, can_gc) } // https://dom.spec.whatwg.org/#dom-parentnode-append fn Append(&self, nodes: Vec<NodeOrString>, can_gc: CanGc) -> ErrorResult { self.upcast::<Node>().append(nodes, can_gc) } // https://dom.spec.whatwg.org/#dom-parentnode-replacechildren fn ReplaceChildren(&self, nodes: Vec<NodeOrString>, can_gc: CanGc) -> ErrorResult { self.upcast::<Node>().replace_children(nodes, can_gc) } // https://dom.spec.whatwg.org/#dom-parentnode-queryselector fn QuerySelector(&self, selectors: DOMString) -> Fallible<Option<DomRoot<Element>>> { let root = self.upcast::<Node>(); root.query_selector(selectors) } // https://dom.spec.whatwg.org/#dom-parentnode-queryselectorall fn QuerySelectorAll(&self, selectors: DOMString) -> Fallible<DomRoot<NodeList>> { let root = self.upcast::<Node>(); root.query_selector_all(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); } let url = self.url(); let (tx, rx) = profile_ipc::channel(self.global().time_profiler_chan().clone()).unwrap(); let _ = self .window .as_global_scope() .resource_threads() .send(GetCookiesForUrl(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); } 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, value: DOMString, can_gc: CanGc) { self.set_body_attribute(&local_name!("bgcolor"), value, can_gc) } // 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, value: DOMString, can_gc: CanGc) { self.set_body_attribute(&local_name!("text"), value, can_gc) } /// <https://html.spec.whatwg.org/multipage/#dom-tree-accessors:dom-document-nameditem-filter> fn NamedGetter(&self, name: DOMString, can_gc: CanGc) -> 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(&name); let name_iter = elements_with_name .iter() .filter(|elem| is_named_element_with_name_attribute(elem)); let elements_with_id = self.get_elements_with_id(&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( self.window(), self.upcast(), Box::new(DocumentNamedGetter { name }), can_gc, ); Some(NamedPropertyValue::HTMLCollection(collection)) } // https://html.spec.whatwg.org/multipage/#dom-tree-accessors:supported-property-names fn SupportedPropertyNames(&self) -> Vec<DOMString> { let mut names_with_first_named_element_map: HashMap<&Atom, &Element> = HashMap::new(); let name_map = self.name_map.borrow(); for (name, elements) in &(name_map).0 { if name.is_empty() { continue; } 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, first); } } let id_map = self.id_map.borrow(); for (id, elements) in &(id_map).0 { if id.is_empty() { continue; } 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) { Vacant(entry) => drop(entry.insert(first)), Occupied(mut entry) => { if first.upcast::<Node>().is_before(entry.get().upcast()) { *entry.get_mut() = first; } }, } } } let mut names_with_first_named_element_vec: Vec<(&Atom, &Element)> = names_with_first_named_element_map .iter() .map(|(k, v)| (*k, *v)) .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 .iter() .map(|(k, _v)| 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>, can_gc: CanGc, ) -> Option<DomRoot<Element>> { self.document_or_shadow_root.element_from_point( x, y, self.GetDocumentElement(), self.has_browsing_context, can_gc, ) } // https://drafts.csswg.org/cssom-view/#dom-document-elementsfrompoint fn ElementsFromPoint( &self, x: Finite<f64>, y: Finite<f64>, can_gc: CanGc, ) -> Vec<DomRoot<Element>> { self.document_or_shadow_root.elements_from_point( x, y, self.GetDocumentElement(), self.has_browsing_context, can_gc, ) } /// <https://drafts.csswg.org/cssom-view/#dom-document-scrollingelement> fn GetScrollingElement(&self, can_gc: CanGc) -> 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 the body element’s parent element // must be treated as overflow:hidden. if !e.is_potentially_scrollable_body_for_scrolling_element(can_gc) { 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, _unused1: Option<DOMString>, _unused2: Option<DOMString>, can_gc: CanGc, ) -> Fallible<DomRoot<Document>> { // Step 1 if !self.is_html_document() { return Err(Error::InvalidState); } // Step 2 if self.throw_on_dynamic_markup_insertion_counter.get() > 0 { return Err(Error::InvalidState); } // Step 3 let entry_responsible_document = GlobalScope::entry().as_window().Document(); // Step 4 if !self.origin.same_origin(&entry_responsible_document.origin) { return Err(Error::Security); } // Step 5 if self .get_current_parser() .is_some_and(|parser| parser.is_active()) { return Ok(DomRoot::from_ref(self)); } // Step 6 if self.is_prompting_or_unloading() { return Ok(DomRoot::from_ref(self)); } // Step 7 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 // 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(can_gc); } // Step 9 for node in self .upcast::<Node>() .traverse_preorder(ShadowIncluding::Yes) { node.upcast::<EventTarget>().remove_all_listeners(); } // Step 10 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(None, self.upcast::<Node>(), can_gc); // 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. // TODO: https://github.com/servo/servo/issues/21938 // 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(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, url: USVString, target: DOMString, features: DOMString, can_gc: CanGc, ) -> Fallible<Option<DomRoot<WindowProxy>>> { self.browsing_context() .ok_or(Error::InvalidAccess)? .open(url, target, features, can_gc) } // https://html.spec.whatwg.org/multipage/#dom-document-write fn Write(&self, text: Vec<TrustedHTMLOrString>, can_gc: CanGc) -> ErrorResult { // The document.write(...text) method steps are to run the document write steps // with this, text, false, and "Document write". self.write(text, false, "Document", "write", can_gc) } // https://html.spec.whatwg.org/multipage/#dom-document-writeln fn Writeln(&self, text: Vec<TrustedHTMLOrString>, can_gc: CanGc) -> ErrorResult { // The document.writeln(...text) method steps are to run the document write steps // with this, text, true, and "Document writeln". self.write(text, true, "Document", "writeln", can_gc) } // https://html.spec.whatwg.org/multipage/#dom-document-close fn Close(&self, can_gc: CanGc) -> ErrorResult { if !self.is_html_document() { // Step 1. return Err(Error::InvalidState); } // Step 2. if self.throw_on_dynamic_markup_insertion_counter.get() > 0 { return Err(Error::InvalidState); } let parser = match self.get_current_parser() { Some(ref parser) if parser.is_script_created() => DomRoot::from_ref(&**parser), _ => { // Step 3. return Ok(()); }, }; // Step 4-6. parser.close(can_gc); Ok(()) } // 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>> { // TODO ShadowRoot self.fullscreen_element.get() } // https://fullscreen.spec.whatwg.org/#dom-document-exitfullscreen fn ExitFullscreen(&self, can_gc: CanGc) -> Rc<Promise> { self.exit_fullscreen(can_gc) } // 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) { Some(m) => Ok(DomRoot::from_ref(m)), None => Err(Error::InvalidAccess), } } // https://w3c.github.io/selection-api/#dom-document-getselection fn GetSelection(&self, can_gc: CanGc) -> Option<DomRoot<Selection>> { if self.has_browsing_context { Some(self.selection.or_init(|| Selection::new(self, can_gc))) } else { None } } // https://drafts.csswg.org/css-font-loading/#font-face-source fn Fonts(&self, can_gc: CanGc) -> DomRoot<FontFaceSet> { self.fonts .or_init(|| FontFaceSet::new(&self.global(), None, can_gc)) } /// <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, expression: DOMString, resolver: Option<Rc<XPathNSResolver>>, can_gc: CanGc, ) -> Fallible<DomRoot<super::types::XPathExpression>> { let global = self.global(); let window = global.as_window(); let evaluator = XPathEvaluator::new(window, None, can_gc); XPathEvaluatorMethods::<crate::DomTypeHolder>::CreateExpression( &*evaluator, expression, resolver, can_gc, ) } fn CreateNSResolver(&self, node_resolver: &Node, can_gc: CanGc) -> DomRoot<Node> { let global = self.global(); let window = global.as_window(); let evaluator = XPathEvaluator::new(window, None, can_gc); XPathEvaluatorMethods::<crate::DomTypeHolder>::CreateNSResolver(&*evaluator, node_resolver) } fn Evaluate( &self, expression: DOMString, context_node: &Node, resolver: Option<Rc<XPathNSResolver>>, type_: u16, result: Option<&super::types::XPathResult>, can_gc: CanGc, ) -> Fallible<DomRoot<super::types::XPathResult>> { let global = self.global(); let window = global.as_window(); let evaluator = XPathEvaluator::new(window, None, can_gc); XPathEvaluatorMethods::<crate::DomTypeHolder>::Evaluate( &*evaluator, expression, context_node, resolver, type_, result, can_gc, ) } } fn update_with_current_instant(marker: &Cell<Option<CrossProcessInstant>>) { if marker.get().is_none() { marker.set(Some(CrossProcessInstant::now())) } } /// <https://w3c.github.io/webappsec-referrer-policy/#determine-policy-for-token> pub(crate) fn determine_policy_for_token(token: &str) -> ReferrerPolicy { match_ignore_ascii_case! { token, "never" | "no-referrer" => ReferrerPolicy::NoReferrer, "no-referrer-when-downgrade" => ReferrerPolicy::NoReferrerWhenDowngrade, "origin" => ReferrerPolicy::Origin, "same-origin" => ReferrerPolicy::SameOrigin, "strict-origin" => ReferrerPolicy::StrictOrigin, "default" | "strict-origin-when-cross-origin" => ReferrerPolicy::StrictOriginWhenCrossOrigin, "origin-when-cross-origin" => ReferrerPolicy::OriginWhenCrossOrigin, "always" | "unsafe-url" => ReferrerPolicy::UnsafeUrl, _ => ReferrerPolicy::EmptyString, } } /// Specifies the type of focus event that is sent to a pipeline #[derive(Clone, Copy, PartialEq)] pub(crate) enum FocusType { Element, // The first focus message - focus the element itself Parent, // Focusing a parent element (an iframe) } /// Specifies the initiator of a focus operation. #[derive(Clone, Copy, PartialEq)] pub enum FocusInitiator { /// The operation is initiated by this document and to be broadcasted /// through the constellation. Local, /// The operation is initiated somewhere else, and we are updating our /// internal state accordingly. Remote, } /// Focus events pub(crate) enum FocusEventType { Focus, // Element gained focus. Doesn't bubble. Blur, // Element lost focus. Doesn't bubble. } /// This is a temporary workaround to update animated images, /// we should get rid of this after we have refresh driver #3406 #[derive(JSTraceable, MallocSizeOf)] pub(crate) struct ImageAnimationUpdateCallback { /// The document. #[ignore_malloc_size_of = "non-owning"] document: Trusted<Document>, } impl ImageAnimationUpdateCallback { pub(crate) fn invoke(self, can_gc: CanGc) { with_script_thread(|script_thread| script_thread.update_the_rendering(true, can_gc)) } } #[derive(JSTraceable, MallocSizeOf)] pub(crate) enum AnimationFrameCallback { DevtoolsFramerateTick { actor_name: String, }, FrameRequestCallback { #[ignore_malloc_size_of = "Rc is hard"] callback: Rc<FrameRequestCallback>, }, } impl AnimationFrameCallback { fn call(&self, document: &Document, now: f64, can_gc: CanGc) { 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__(Finite::wrap(now), ExceptionHandling::Report, can_gc); }, } } } #[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)) } } #[derive(Clone, Copy, Debug, PartialEq)] pub(crate) enum ReflowTriggerCondition { StylesheetsChanged, DirtyDescendants, PendingRestyles, PaintPostponed, } 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) } }