servo/components/script/dom/document.rs
Josh Matthews c94ac5bccb
Move various reflector types and traits to script_bindings (#35279)
* script: Move Reflector to script_bindings.

Signed-off-by: Josh Matthews <josh@joshmatthews.net>

* script: Extract global() helper from DomObject into new trait. Move DomObject and related traits to script_bindings.

Signed-off-by: Josh Matthews <josh@joshmatthews.net>

---------

Signed-off-by: Josh Matthews <josh@joshmatthews.net>
2025-02-04 06:58:08 +00:00

6013 lines
223 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* 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::mem;
use std::rc::Rc;
use std::slice::from_ref;
use std::sync::{LazyLock, Mutex};
use std::time::{Duration, Instant};
use base::cross_process_instant::CrossProcessInstant;
use base::id::WebViewId;
use canvas_traits::webgl::{self, WebGLContextId, WebGLMsg};
use chrono::Local;
use content_security_policy::{self as csp, CspList};
use cookie::Cookie;
use cssparser::match_ignore_ascii_case;
use devtools_traits::ScriptToDevtoolsControlMsg;
use dom_struct::dom_struct;
use embedder_traits::{
ClipboardEventType, EmbedderMsg, LoadStatus, MouseButton, MouseEventType, TouchEventType,
TouchId, WheelDelta,
};
use encoding_rs::{Encoding, UTF_8};
use euclid::default::{Point2D, Rect, Size2D};
use html5ever::{local_name, namespace_url, ns, LocalName, Namespace, QualName};
use hyper_serde::Serde;
use ipc_channel::ipc;
use js::rust::{HandleObject, HandleValue};
use keyboard_types::{Code, Key, KeyState};
use metrics::{
InteractiveFlag, InteractiveMetrics, InteractiveWindow, ProfilerMetadataFactory,
ProgressiveWebMetric,
};
use mime::{self, Mime};
use net_traits::policy_container::PolicyContainer;
use net_traits::pub_domains::is_pub_domain;
use net_traits::request::RequestBuilder;
use net_traits::response::HttpsState;
use net_traits::CookieSource::NonHTTP;
use net_traits::CoreResourceMsg::{GetCookiesForUrl, SetCookiesForUrl};
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::{TimerMetadata, TimerMetadataFrameType, TimerMetadataReflowType};
use script_layout_interface::{PendingRestyle, TrustedNodeAddress};
use script_traits::{
AnimationState, AnimationTickType, CompositorEvent, DocumentActivity, ScriptMsg,
UntrustedNodeAddress,
};
use servo_arc::Arc;
use servo_atoms::Atom;
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 url::Host;
use uuid::Uuid;
#[cfg(feature = "webgpu")]
use webgpu::swapchain::WebGPUContextId;
use webrender_api::units::DeviceIntRect;
use super::bindings::codegen::Bindings::XPathEvaluatorBinding::XPathEvaluatorMethods;
use crate::animation_timeline::AnimationTimeline;
use crate::animations::Animations;
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::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::XPathNSResolverBinding::XPathNSResolver;
use crate::dom::bindings::codegen::UnionTypes::{NodeOrString, StringOrElementCreationOptions};
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::{reflect_dom_object_with_proto, DomGlobal};
use crate::dom::bindings::root::{Dom, DomRoot, DomSlice, LayoutDom, MutNullableDom};
use crate::dom::bindings::str::{DOMString, USVString};
use crate::dom::bindings::trace::{HashMapTracedValues, NoTrace};
use crate::dom::bindings::weakref::WeakRef;
use crate::dom::bindings::xmlname::XMLName::Invalid;
use crate::dom::bindings::xmlname::{
namespace_from_domstring, validate_and_extract, xml_name_type,
};
use crate::dom::cdatasection::CDATASection;
use crate::dom::clipboardevent::ClipboardEvent;
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::htmlbodyelement::HTMLBodyElement;
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::htmlmetaelement::RefreshRedirectDue;
use crate::dom::htmlscriptelement::{HTMLScriptElement, ScriptResult};
use crate::dom::htmltextareaelement::HTMLTextAreaElement;
use crate::dom::htmltitleelement::HTMLTitleElement;
use crate::dom::keyboardevent::KeyboardEvent;
use crate::dom::location::Location;
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::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;
use crate::dom::touchlist::TouchList;
use crate::dom::treewalker::TreeWalker;
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;
use crate::dom::window::Window;
use crate::dom::windowproxy::WindowProxy;
use crate::dom::xpathevaluator::XPathEvaluator;
use crate::drag_data_store::{DragDataStore, Kind, Mode, PlainString};
use crate::fetch::FetchCanceller;
use crate::iframe_collection::IFrameCollection;
use crate::messaging::{CommonScriptMsg, MainThreadScriptMsg};
use crate::network_listener::{NetworkListener, PreInvoke};
use crate::realms::{enter_realm, AlreadyInRealm, InRealm};
use crate::script_runtime::{CanGc, ScriptThreadEventCategory};
use crate::script_thread::{with_script_thread, ScriptThread};
use crate::stylesheet_set::StylesheetSetRef;
use crate::task::TaskBox;
use crate::task_source::TaskSourceName;
use crate::timers::OneshotTimerCallback;
/// The number of times we are allowed to see spurious `requestAnimationFrame()` calls before
/// falling back to fake ones.
///
/// A spurious `requestAnimationFrame()` call is defined as one that does not change the DOM.
const SPURIOUS_ANIMATION_FRAME_THRESHOLD: u8 = 5;
/// The amount of time between fake `requestAnimationFrame()`s.
const FAKE_REQUEST_ANIMATION_FRAME_DELAY: u64 = 16;
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(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)]
enum FocusTransaction {
/// No focus operation is in effect.
NotInTransaction,
/// A focus operation is in effect.
/// Contains the element that has most recently requested focus for itself.
InTransaction(Option<Dom<Element>>),
}
/// 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<FocusTransaction>,
/// The element that currently has the document focus context.
focused: MutNullableDom<Element>,
/// 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<Vec<(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<InteractiveMetrics>,
#[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 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>,
/// 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 composition events, to be handled at the next rendering opportunity.
#[no_trace]
#[ignore_malloc_size_of = "CompositorEvent contains data from outside crates"]
pending_compositor_events: DomRefCell<Vec<CompositorEvent>>,
/// The index of the last mouse move event in the pending compositor events queue.
mouse_move_event_index: DomRefCell<Option<usize>>,
/// Pending animation ticks, to be handled at the next rendering opportunity.
#[no_trace]
#[ignore_malloc_size_of = "AnimationTickType contains data from an outside crate"]
pending_animation_ticks: DomRefCell<AnimationTickType>,
/// <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>,
}
#[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.inclusive_ancestors(ShadowIncluding::Yes).nth(1) {
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(ShadowIncluding::Yes)
{
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(ShadowIncluding::Yes)
{
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(dirty_root.upcast(), ShadowIncluding::Yes)
.expect("Couldn't find common ancestor");
let mut has_dirty_descendants = true;
for ancestor in dirty_root
.upcast::<Node>()
.inclusive_ancestors(ShadowIncluding::Yes)
{
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.type_() == mime::APPLICATION &&
self.content_type.subtype().as_str() == "xhtml" &&
self.content_type.suffix() == Some(mime::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) {
// 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();
media.suspend(&client_context_id);
return;
}
self.title_changed();
self.dirty_all_nodes();
self.window().resume();
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::OtherNodeDamage);
}
/// 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) {
self.document_or_shadow_root
.unregister_named_element(&self.id_map, to_unregister, &id);
self.reset_form_owner_for_listeners(&id);
}
/// Associate an element present in this document with the provided id.
pub(crate) fn register_element_id(&self, element: &Element, id: 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.id_map,
element,
&id,
DomRoot::from_ref(root.upcast::<Node>()),
);
self.reset_form_owner_for_listeners(&id);
}
/// 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 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()
}
/// 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) {
*self.focus_transaction.borrow_mut() = FocusTransaction::InTransaction(Default::default());
}
/// <https://html.spec.whatwg.org/multipage/#focus-fixup-rule>
pub(crate) fn perform_focus_fixup_rule(&self, not_focusable: &Element, can_gc: CanGc) {
if Some(not_focusable) != self.focused.get().as_deref() {
return;
}
self.request_focus(
self.GetBody().as_ref().map(|e| e.upcast()),
FocusType::Element,
can_gc,
)
}
/// Request that the given element receive focus once the current transaction is complete.
/// If None is passed, then whatever element is currently focused will no longer be focused
/// once the transaction is complete.
pub(crate) fn request_focus(
&self,
elem: Option<&Element>,
focus_type: FocusType,
can_gc: CanGc,
) {
let implicit_transaction = matches!(
*self.focus_transaction.borrow(),
FocusTransaction::NotInTransaction
);
if implicit_transaction {
self.begin_focus_transaction();
}
if elem.map_or(true, |e| e.is_focusable_area()) {
*self.focus_transaction.borrow_mut() =
FocusTransaction::InTransaction(elem.map(Dom::from_ref));
}
if implicit_transaction {
self.commit_focus_transaction(focus_type, can_gc);
}
}
/// Reassign the focus context to the element that last requested focus during this
/// transaction, or none if no elements requested it.
fn commit_focus_transaction(&self, focus_type: FocusType, can_gc: CanGc) {
let possibly_focused = match *self.focus_transaction.borrow() {
FocusTransaction::NotInTransaction => unreachable!(),
FocusTransaction::InTransaction(ref elem) => {
elem.as_ref().map(|e| DomRoot::from_ref(&**e))
},
};
*self.focus_transaction.borrow_mut() = FocusTransaction::NotInTransaction;
if self.focused == possibly_focused.as_deref() {
return;
}
if let Some(ref elem) = self.focused.get() {
let node = elem.upcast::<Node>();
elem.set_focus_state(false);
// FIXME: pass appropriate relatedTarget
self.fire_focus_event(FocusEventType::Blur, node, 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()));
}
}
self.focused.set(possibly_focused.as_deref());
if let Some(ref elem) = self.focused.get() {
elem.set_focus_state(true);
let node = elem.upcast::<Node>();
// FIXME: pass appropriate relatedTarget
self.fire_focus_event(FocusEventType::Focus, node, None, can_gc);
// Update the focus state for all elements in the focus chain.
// https://html.spec.whatwg.org/multipage/#focus-chain
if focus_type == FocusType::Element {
self.window().send_to_constellation(ScriptMsg::Focus);
}
// 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()),
));
}
}
}
/// 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(ScriptMsg::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));
}
}
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::OtherNodeDamage)
}
}
#[allow(unsafe_code)]
#[allow(clippy::too_many_arguments)]
pub(crate) unsafe fn handle_mouse_button_event(
&self,
button: MouseButton,
client_point: Point2D<f32>,
mouse_event_type: MouseEventType,
node_address: Option<UntrustedNodeAddress>,
point_in_node: Option<Point2D<f32>>,
pressed_mouse_buttons: u16,
can_gc: CanGc,
) {
let mouse_event_type_string = match mouse_event_type {
MouseEventType::Click => "click".to_owned(),
MouseEventType::MouseUp => "mouseup".to_owned(),
MouseEventType::MouseDown => "mousedown".to_owned(),
};
debug!("{}: at {:?}", mouse_event_type_string, client_point);
let el = node_address.and_then(|address| {
let node = node::from_untrusted_node_address(address);
node.inclusive_ancestors(ShadowIncluding::No)
.filter_map(DomRoot::downcast::<Element>)
.next()
});
let el = match el {
Some(el) => el,
None => return,
};
let node = el.upcast::<Node>();
debug!("{} on {:?}", mouse_event_type_string, node.debug_str());
// Prevent click event if form control element is disabled.
if let MouseEventType::Click = mouse_event_type {
// The click event is filtered by the disabled state.
if el.is_actually_disabled() {
return;
}
self.begin_focus_transaction();
self.request_focus(Some(&*el), FocusType::Element, can_gc);
}
// https://w3c.github.io/uievents/#event-type-click
let client_x = client_point.x as i32;
let client_y = client_point.y as i32;
let click_count = 1;
let event = MouseEvent::new(
&self.window,
DOMString::from(mouse_event_type_string),
EventBubbles::Bubbles,
EventCancelable::Cancelable,
Some(&self.window),
click_count,
client_x,
client_y,
client_x,
client_y, // TODO: Get real screen coordinates?
false,
false,
false,
false,
match &button {
MouseButton::Left => 0i16,
MouseButton::Middle => 1i16,
MouseButton::Right => 2i16,
},
pressed_mouse_buttons,
None,
point_in_node,
can_gc,
);
let event = event.upcast::<Event>();
// https://w3c.github.io/uievents/#trusted-events
event.set_trusted(true);
// https://html.spec.whatwg.org/multipage/#run-authentic-click-activation-steps
let activatable = el.as_maybe_activatable();
match mouse_event_type {
MouseEventType::Click => {
el.set_click_in_progress(true);
event.fire(node.upcast(), can_gc);
el.set_click_in_progress(false);
},
MouseEventType::MouseDown => {
if let Some(a) = activatable {
a.enter_formal_activation_state();
}
let target = node.upcast();
event.fire(target, can_gc);
},
MouseEventType::MouseUp => {
if let Some(a) = activatable {
a.exit_formal_activation_state();
}
let target = node.upcast();
event.fire(target, can_gc);
},
}
if let MouseEventType::Click = mouse_event_type {
self.commit_focus_transaction(FocusType::Element, can_gc);
self.maybe_fire_dblclick(client_point, node, pressed_mouse_buttons, can_gc);
}
}
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);
let mouse_event = 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,
);
let event = mouse_event.upcast::<Event>();
event.fire(target, can_gc);
}
/// <https://www.w3.org/TR/clipboard-apis/#clipboard-actions>
pub(crate) 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(ref contents) => {
// 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 plain_string = PlainString::new(
DOMString::from_string(contents.to_string()),
DOMString::from("text/plain"),
);
let _ = drag_data_store.add(Kind::Text(plain_string));
// 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-datas files to match clipboard-event-datas items
// Step 7.1.4 Update clipboard-event-datas types to match clipboard-event-datas 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 events 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::ClearClipboardContents(self.webview_id()));
// Step 1.2
for item in drag_data_store.iter_item_list() {
match item {
Kind::Text(string) => {
// 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::SetClipboardContents(
self.webview_id(),
string.data(),
));
},
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::ClearClipboardContents(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,
client_point: Point2D<f32>,
prev_mouse_over_target: &MutNullableDom<Element>,
node_address: Option<UntrustedNodeAddress>,
pressed_mouse_buttons: u16,
can_gc: CanGc,
) {
let maybe_new_target = node_address.and_then(|address| {
let node = node::from_untrusted_node_address(address);
node.inclusive_ancestors(ShadowIncluding::No)
.filter_map(DomRoot::downcast::<Element>)
.next()
});
let new_target = match maybe_new_target {
Some(ref target) => target,
None => return,
};
let target_has_changed = prev_mouse_over_target
.get()
.as_ref()
.map_or(true, |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(
client_point,
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(
client_point,
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(
client_point,
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(
client_point,
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(
client_point,
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(maybe_new_target.as_deref());
}
}
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) unsafe fn handle_wheel_event(
&self,
delta: WheelDelta,
client_point: Point2D<f32>,
node_address: Option<UntrustedNodeAddress>,
can_gc: CanGc,
) {
let wheel_event_type_string = "wheel".to_owned();
debug!("{}: at {:?}", wheel_event_type_string, client_point);
let el = node_address.and_then(|address| {
let node = node::from_untrusted_node_address(address);
node.inclusive_ancestors(ShadowIncluding::No)
.filter_map(DomRoot::downcast::<Element>)
.next()
});
let el = match el {
Some(el) => el,
None => return,
};
let node = el.upcast::<Node>();
debug!("{}: on {:?}", wheel_event_type_string, node.debug_str());
// https://w3c.github.io/uievents/#event-wheelevents
let event = WheelEvent::new(
&self.window,
DOMString::from(wheel_event_type_string),
EventBubbles::Bubbles,
EventCancelable::Cancelable,
Some(&self.window),
0i32,
Finite::wrap(delta.x),
Finite::wrap(delta.y),
Finite::wrap(delta.z),
delta.mode as u32,
can_gc,
);
let event = event.upcast::<Event>();
event.set_trusted(true);
let target = node.upcast();
event.fire(target, can_gc);
}
#[allow(unsafe_code)]
pub(crate) unsafe fn handle_touch_event(
&self,
event_type: TouchEventType,
touch_id: TouchId,
point: Point2D<f32>,
node_address: Option<UntrustedNodeAddress>,
can_gc: CanGc,
) -> TouchEventResult {
let TouchId(identifier) = touch_id;
let event_name = match event_type {
TouchEventType::Down => "touchstart",
TouchEventType::Move => "touchmove",
TouchEventType::Up => "touchend",
TouchEventType::Cancel => "touchcancel",
};
let el = node_address.and_then(|address| {
let node = node::from_untrusted_node_address(address);
node.inclusive_ancestors(ShadowIncluding::No)
.filter_map(DomRoot::downcast::<Element>)
.next()
});
let el = match el {
Some(el) => el,
None => return TouchEventResult::Forwarded,
};
let target = DomRoot::upcast::<EventTarget>(el);
let window = &*self.window;
let client_x = Finite::wrap(point.x as f64);
let client_y = Finite::wrap(point.y as f64);
let page_x = Finite::wrap(point.x as f64 + window.PageXOffset() as f64);
let page_y = Finite::wrap(point.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,
);
match 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())
};
let event = TouchEvent::new(
window,
DOMString::from(event_name),
EventBubbles::Bubbles,
EventCancelable::Cancelable,
Some(window),
0i32,
&touches,
&TouchList::new(window, from_ref(&&*touch)),
&TouchList::new(window, target_touches.r()),
// FIXME: modifier keys
false,
false,
false,
false,
);
let event = event.upcast::<Event>();
let result = event.fire(&target, can_gc);
match result {
EventStatus::Canceled => TouchEventResult::Processed(false),
EventStatus::NotCanceled => TouchEventResult::Processed(true),
}
}
/// The entry point for all key processing for web content
pub(crate) fn dispatch_key_event(
&self,
keyboard_event: ::keyboard_types::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.state.to_string()),
true,
true,
Some(&self.window),
0,
keyboard_event.key.clone(),
DOMString::from(keyboard_event.code.to_string()),
keyboard_event.location as u32,
keyboard_event.repeat,
keyboard_event.is_composing,
keyboard_event.modifiers,
0,
keyboard_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
if keyboard_event.state == KeyState::Down &&
is_character_value_key(&(keyboard_event.key)) &&
!keyboard_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.key.clone(),
DOMString::from(keyboard_event.code.to_string()),
keyboard_event.location as u32,
keyboard_event.repeat,
keyboard_event.is_composing,
keyboard_event.modifiers,
keyboard_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.key == Key::Enter || keyboard_event.code == Code::Space) &&
keyboard_event.state == KeyState::Up
{
if let Some(elem) = target.downcast::<Element>() {
elem.upcast::<Node>()
.fire_synthetic_mouse_event_not_trusted(DOMString::from("click"), can_gc);
}
}
}
}
pub(crate) fn ime_dismissed(&self, can_gc: CanGc) {
self.request_focus(
self.GetBody().as_ref().map(|e| e.upcast()),
FocusType::Element,
can_gc,
)
}
pub(crate) fn dispatch_composition_event(
&self,
composition_event: ::keyboard_types::CompositionEvent,
can_gc: CanGc,
) {
// 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)?;
},
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).unwrap();
},
}
}
Ok(fragment)
}
}
pub(crate) fn get_body_attribute(&self, local_name: &LocalName) -> DOMString {
match self
.GetBody()
.and_then(DomRoot::downcast::<HTMLBodyElement>)
{
Some(ref body) => body.upcast::<Element>().get_string_attribute(local_name),
None => DOMString::new(),
}
}
pub(crate) fn set_body_attribute(
&self,
local_name: &LocalName,
value: DOMString,
can_gc: CanGc,
) {
if let Some(ref body) = self
.GetBody()
.and_then(DomRoot::downcast::<HTMLBodyElement>)
{
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::NodeStyleDamaged);
}
}
/// 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);
self.animation_frame_list
.borrow_mut()
.push((ident, Some(callback)));
// If we are running 'fake' animation frames, we unconditionally
// set up a one-shot timer for script to execute the rAF callbacks.
if self.is_faking_animation_frames() && !self.window().throttled() {
warn!("Scheduling fake animation frame. Animation frames tick too fast.");
let callback = FakeRequestAnimationFrameCallback {
document: Trusted::new(self),
};
self.global().schedule_callback(
OneshotTimerCallback::FakeRequestAnimationFrame(callback),
Duration::from_millis(FAKE_REQUEST_ANIMATION_FRAME_DELAY),
);
} else if !self.running_animation_callbacks.get() {
// 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.
let event =
ScriptMsg::ChangeRunningAnimationsState(AnimationState::AnimationCallbacksPresent);
self.window().send_to_constellation(event);
}
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) {
let _realm = enter_realm(self);
rooted_vec!(let mut animation_frame_list);
mem::swap(
&mut *animation_frame_list,
&mut *self.animation_frame_list.borrow_mut(),
);
self.pending_animation_ticks
.borrow_mut()
.remove(AnimationTickType::REQUEST_ANIMATION_FRAME);
self.running_animation_callbacks.set(true);
let was_faking_animation_frames = self.is_faking_animation_frames();
let timing = self.global().performance().Now();
for (_, callback) in animation_frame_list.drain(..) {
if let Some(callback) = callback {
callback.call(self, *timing);
}
}
self.running_animation_callbacks.set(false);
let callbacks_did_not_trigger_reflow = self.needs_reflow().is_none();
let is_empty = self.animation_frame_list.borrow().is_empty();
if !is_empty && callbacks_did_not_trigger_reflow && !was_faking_animation_frames {
// If the rAF callbacks did not mutate the DOM, then the impending
// reflow call as part of *update the rendering* will not do anything
// and therefore no new frame will be sent to the compositor.
// If this happens, the compositor will not tick the animation
// and the next rAF will never be called! When this happens
// for several frames, then the spurious rAF detection below
// will kick in and use a timer to tick the callbacks. However,
// for the interim frames where we are deciding whether this rAF
// is considered spurious, we need to ensure that the layout
// and compositor *do* tick the animation.
self.set_needs_paint(true);
}
// Only send the animation change state message after running any callbacks.
// This means that if the animation callback adds a new callback for
// the next frame (which is the common case), we won't send a NoAnimationCallbacksPresent
// message quickly followed by an AnimationCallbacksPresent message.
//
// If this frame was spurious and we've seen too many spurious frames in a row, tell the
// constellation to stop giving us video refresh callbacks, to save energy. (A spurious
// animation frame is one in which the callback did not mutate the DOM—that is, an
// animation frame that wasn't actually used for animation.)
if is_empty || (!was_faking_animation_frames && self.is_faking_animation_frames()) {
if is_empty {
// If the current animation frame list in the DOM instance is empty,
// we can reuse the original `Vec<T>` that we put on the stack to
// avoid allocating a new one next time an animation callback
// is queued.
mem::swap(
&mut *self.animation_frame_list.borrow_mut(),
&mut *animation_frame_list,
);
}
let event = ScriptMsg::ChangeRunningAnimationsState(
AnimationState::NoAnimationCallbacksPresent,
);
self.window().send_to_constellation(event);
}
// Update the counter of spurious animation frames.
if callbacks_did_not_trigger_reflow {
if self.spurious_animation_frames.get() < SPURIOUS_ANIMATION_FRAME_THRESHOLD {
self.spurious_animation_frames
.set(self.spurious_animation_frames.get() + 1)
}
} else {
self.spurious_animation_frames.set(0)
}
}
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,
request: RequestBuilder,
listener: Listener,
) {
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,
request: RequestBuilder,
listener: Listener,
) {
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();
},
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();
},
_ => {},
}
// 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,
);
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();
}
// 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 = ScriptMsg::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) {
{
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);
}
// 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,
) {
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);
}
}
// 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) {
self.deferred_scripts.loaded(element, result);
self.process_deferred_scripts();
}
/// <https://html.spec.whatwg.org/multipage/#the-end> step 3.
fn process_deferred_scripts(&self) {
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);
} 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(self, 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(ScriptMsg::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<InteractiveMetrics> {
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(
self,
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,
node: &Node,
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);
let target = node.upcast();
event.fire(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) {
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();
} 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();
}
#[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_of_webgpu_canvas());
}
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(&|s| encoding.encode(s).0))
.parse(url)
.map(ServoUrl::from)
}
}
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,
) -> 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 interactive_time =
InteractiveMetrics::new(window.time_profiler_chan().clone(), url.clone());
let content_type = content_type.unwrap_or_else(|| {
match is_html_document {
// https://dom.spec.whatwg.org/#dom-domimplementation-createhtmldocument
IsHTMLDocument::HTMLDocument => mime::TEXT_HTML,
// https://dom.spec.whatwg.org/#concept-document-content-type
IsHTMLDocument::NonHTMLDocument => "application/xml".parse().unwrap(),
}
});
let encoding = content_type
.get_param(mime::CHARSET)
.and_then(|charset| Encoding::for_label(charset.as_str().as_bytes()))
.unwrap_or(UTF_8);
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(FocusTransaction::NotInTransaction),
focused: Default::default(),
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(vec![]),
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_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()),
dirty_root: Default::default(),
declarative_refresh: Default::default(),
pending_animation_ticks: Default::default(),
pending_compositor_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),
}
}
/// Note a pending compositor event, to be processed at the next `update_the_rendering` task.
pub(crate) fn note_pending_compositor_event(&self, event: CompositorEvent) {
let mut pending_compositor_events = self.pending_compositor_events.borrow_mut();
if matches!(event, CompositorEvent::MouseMoveEvent { .. }) {
// 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_compositor_events(&self) -> Vec<CompositorEvent> {
// Reset the mouse event index.
*self.mouse_move_event_index.borrow_mut() = None;
mem::take(&mut *self.pending_compositor_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 element = csp::Element {
nonce: el
.get_attribute(&ns!(), &local_name!("nonce"))
.map(|attr| Cow::Owned(attr.value().to_string())),
};
// TODO: Instead of ignoring violations, report them.
self.get_csp_list()
.map(|c| {
c.should_elements_inline_type_behavior_be_blocked(&element, type_, source)
.0
})
.unwrap_or(csp::CheckResult::Allowed)
}
/// 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,
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,
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,
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,
)),
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,
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);
}
/// Whether we've seen so many spurious animation frames (i.e. animation frames that didn't
/// mutate the DOM) that we've decided to fall back to fake ones.
fn is_faking_animation_frames(&self) -> bool {
self.spurious_animation_frames.get() >= SPURIOUS_ANIMATION_FRAME_THRESHOLD
}
// 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();
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::SetFullscreenState(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();
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")));
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::SetFullscreenState(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) {
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();
}
}
}
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();
let cloned_stylesheet = sheet.clone();
let insertion_point2 = insertion_point.clone();
self.window.layout_mut().add_stylesheet(
cloned_stylesheet,
insertion_point2.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>) {
let cloned_stylesheet = stylesheet.clone();
self.window
.layout_mut()
.remove_stylesheet(cloned_stylesheet);
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()
}
/// Note a pending animation tick, to be processed at the next `update_the_rendering` task.
pub(crate) fn note_pending_animation_tick(&self, tick_type: AnimationTickType) {
self.pending_animation_ticks.borrow_mut().extend(tick_type);
}
/// Whether this document has received an animation tick for rafs.
pub(crate) fn has_received_raf_tick(&self) -> bool {
self.pending_animation_ticks
.borrow()
.contains(AnimationTickType::REQUEST_ANIMATION_FRAME)
}
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 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());
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()
}
}
impl ProfilerMetadataFactory for Document {
fn new_metadata(&self) -> Option<TimerMetadata> {
Some(TimerMetadata {
url: String::from(self.url().as_str()),
iframe: TimerMetadataFrameType::RootWindow,
incremental: TimerMetadataReflowType::Incremental,
})
}
}
#[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,
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) -> DomRoot<StyleSheetList> {
self.stylesheet_list.or_init(|| {
StyleSheetList::new(
&self.window,
StyleSheetListOwner::Document(Dom::from_ref(self)),
)
})
}
// https://dom.spec.whatwg.org/#dom-document-implementation
fn Implementation(&self) -> DomRoot<DOMImplementation> {
self.implementation.or_init(|| DOMImplementation::new(self))
}
// https://dom.spec.whatwg.org/#dom-document-url
fn URL(&self) -> USVString {
USVString(String::from(self.url().as_str()))
}
// https://html.spec.whatwg.org/multipage/#dom-document-activeelement
fn GetActiveElement(&self) -> Option<DomRoot<Element>> {
self.document_or_shadow_root.get_active_element(
self.get_focused_element(),
self.GetBody(),
self.GetDocumentElement(),
)
}
// https://html.spec.whatwg.org/multipage/#dom-document-hasfocus
fn HasFocus(&self) -> bool {
// Step 1-2.
if self.window().parent_info().is_none() && self.is_fully_active() {
return true;
}
// TODO Step 3.
false
}
// 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) -> 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());
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,
) -> 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());
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) -> 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());
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>> {
if xml_name_type(&local_name) == Invalid {
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>> {
let (namespace, prefix, local_name) = validate_and_extract(namespace, &qualified_name)?;
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))
},
};
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>> {
if xml_name_type(&local_name) == Invalid {
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 xml_name_type(&target) == Invalid {
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) -> 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);
// 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,
))),
"compositionevent" | "textevent" => Ok(DomRoot::upcast(
CompositionEvent::new_uninitialized(&self.window),
)),
"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(TouchEvent::new_uninitialized(
&self.window,
&TouchList::new(&self.window, &[]),
&TouchList::new(&self.window, &[]),
&TouchList::new(&self.window, &[]),
))),
"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>>,
) -> DomRoot<NodeIterator> {
NodeIterator::new(self, root, what_to_show, filter)
}
// 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())
.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()).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>) -> 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())
.unwrap();
},
// Step 4.
(None, _) => return Err(Error::HierarchyRequest),
// Step 5.
(Some(ref root), &None) => {
let root = root.upcast::<Node>();
root.AppendChild(new_body.upcast()).unwrap();
},
}
Ok(())
}
// https://html.spec.whatwg.org/multipage/#dom-document-getelementsbyname
fn GetElementsByName(&self, name: DOMString) -> DomRoot<NodeList> {
NodeList::new_elements_by_name_list(self.window(), self, name)
}
// https://html.spec.whatwg.org/multipage/#dom-document-images
fn Images(&self) -> DomRoot<HTMLCollection> {
self.images.or_init(|| {
HTMLCollection::new_with_filter_fn(&self.window, self.upcast(), |element, _| {
element.is::<HTMLImageElement>()
})
})
}
// https://html.spec.whatwg.org/multipage/#dom-document-embeds
fn Embeds(&self) -> DomRoot<HTMLCollection> {
self.embeds.or_init(|| {
HTMLCollection::new_with_filter_fn(&self.window, self.upcast(), |element, _| {
element.is::<HTMLEmbedElement>()
})
})
}
// https://html.spec.whatwg.org/multipage/#dom-document-plugins
fn Plugins(&self) -> DomRoot<HTMLCollection> {
self.Embeds()
}
// https://html.spec.whatwg.org/multipage/#dom-document-links
fn Links(&self) -> 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"))
})
})
}
// https://html.spec.whatwg.org/multipage/#dom-document-forms
fn Forms(&self) -> DomRoot<HTMLCollection> {
self.forms.or_init(|| {
HTMLCollection::new_with_filter_fn(&self.window, self.upcast(), |element, _| {
element.is::<HTMLFormElement>()
})
})
}
// https://html.spec.whatwg.org/multipage/#dom-document-scripts
fn Scripts(&self) -> DomRoot<HTMLCollection> {
self.scripts.or_init(|| {
HTMLCollection::new_with_filter_fn(&self.window, self.upcast(), |element, _| {
element.is::<HTMLScriptElement>()
})
})
}
// https://html.spec.whatwg.org/multipage/#dom-document-anchors
fn Anchors(&self) -> 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"))
})
})
}
// https://html.spec.whatwg.org/multipage/#dom-document-applets
fn Applets(&self) -> DomRoot<HTMLCollection> {
self.applets
.or_init(|| HTMLCollection::always_empty(&self.window, self.upcast()))
}
// 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) -> DomRoot<HTMLCollection> {
HTMLCollection::children(&self.window, self.upcast())
}
// https://dom.spec.whatwg.org/#dom-parentnode-firstelementchild
fn GetFirstElementChild(&self) -> Option<DomRoot<Element>> {
self.upcast::<Node>().child_elements().next()
}
// https://dom.spec.whatwg.org/#dom-parentnode-lastelementchild
fn GetLastElementChild(&self) -> Option<DomRoot<Element>> {
self.upcast::<Node>()
.rev_children()
.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)
}
#[allow(unsafe_code)]
/// <https://html.spec.whatwg.org/multipage/#dom-tree-accessors:dom-document-nameditem-filter>
fn NamedGetter(&self, name: DOMString) -> Option<NamedPropertyValue> {
if name.is_empty() {
return None;
}
let name = Atom::from(name);
// Step 1.
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);
let first = elements.next()?;
if elements.next().is_none() {
// Step 2.
if let Some(nested_window_proxy) = first
.downcast::<HTMLIFrameElement>()
.and_then(|iframe| iframe.GetContentWindow())
{
return Some(NamedPropertyValue::WindowProxy(nested_window_proxy));
}
// Step 3.
return Some(NamedPropertyValue::Element(DomRoot::from_ref(first)));
}
// Step 4.
#[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 doesnt fully make sense until embed/object
// behaviour is actually implemented
_ => false,
}
}
}
let collection = HTMLCollection::create(
self.window(),
self.upcast(),
Box::new(DocumentNamedGetter { name }),
);
Some(NamedPropertyValue::HTMLCollection(collection))
}
// https://html.spec.whatwg.org/multipage/#dom-tree-accessors:supported-property-names
fn SupportedPropertyNames(&self) -> 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://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
// TODO: https://github.com/servo/servo/issues/21936
Node::replace_all(None, self.upcast::<Node>());
// Specs and tests are in a state of flux about whether
// we want to clear the selection when we remove the contents;
// WPT selection/Document-open.html wants us to not clear it
// as of Feb 1 2020
// Step 12. If document is fully active, then:
if self.is_fully_active() {
// Step 12.1. Let newURL be a copy of entryDocument's URL.
let mut new_url = entry_responsible_document.url();
// Step 12.2. If entryDocument is not document, then set newURL's fragment to null.
if entry_responsible_document != DomRoot::from_ref(self) {
new_url.set_fragment(None);
}
// Step 12.3. Run the URL and history update steps with document and newURL.
// TODO: https://github.com/servo/servo/issues/21939
self.set_url(new_url);
}
// Step 13. Set document's is initial about:blank to false.
self.is_initial_about_blank.set(false);
// Step 14. If document's iframe load in progress flag is set, then set document's mute
// iframe load flag.
// 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<DOMString>, 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);
}
// Step 3 - what specifies the is_active() part here?
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),
_ => {
// Either there is no parser, which means the parsing ended;
// or script nesting level is 0, which means the method was
// called from outside a parser-executed script.
if self.is_prompting_or_unloading() ||
self.ignore_destructive_writes_counter.get() > 0
{
// Step 4.
return Ok(());
}
// Step 5.
self.Open(None, None, can_gc)?;
self.get_current_parser().unwrap()
},
};
// Step 7.
// TODO: handle reload override buffer.
// Steps 6-8.
parser.write(text, can_gc);
// Step 9.
Ok(())
}
// https://html.spec.whatwg.org/multipage/#dom-document-writeln
fn Writeln(&self, mut text: Vec<DOMString>, can_gc: CanGc) -> ErrorResult {
text.push("\n".into());
self.Write(text, 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://html.spec.whatwg.org/multipage/#documentandelementeventhandlers
document_and_element_event_handlers!();
// 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) -> Option<DomRoot<Selection>> {
if self.has_browsing_context {
Some(self.selection.or_init(|| Selection::new(self)))
} 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)
}
/// Focus events
pub(crate) enum FocusEventType {
Focus, // Element gained focus. Doesn't bubble.
Blur, // Element lost focus. Doesn't bubble.
}
/// A fake `requestAnimationFrame()` callback—"fake" because it is not triggered by the video
/// refresh but rather a simple timer.
///
/// If the page is observed to be using `requestAnimationFrame()` for non-animation purposes (i.e.
/// without mutating the DOM), then we fall back to simple timeouts to save energy over video
/// refresh.
#[derive(JSTraceable, MallocSizeOf)]
pub(crate) struct FakeRequestAnimationFrameCallback {
/// The document.
#[ignore_malloc_size_of = "non-owning"]
document: Trusted<Document>,
}
impl FakeRequestAnimationFrameCallback {
pub(crate) fn invoke(self, can_gc: CanGc) {
// TODO: Once there is a more generic mechanism to trigger `update_the_rendering` when
// not driven by the compositor, it should be used here.
self.document
.root()
.note_pending_animation_tick(AnimationTickType::REQUEST_ANIMATION_FRAME);
with_script_thread(|script_thread| script_thread.update_the_rendering(false, 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) {
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);
},
}
}
}
#[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 doesnt 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 doesnt fully make sense until embed/object
// behaviour is actually implemented
elem.is::<HTMLImageElement>() && elem.get_name().is_some_and(|name| !name.is_empty())
}