mirror of
https://github.com/servo/servo.git
synced 2025-09-27 15:20:09 +01:00
Stylesheets loaded via the `<link>` element should block the rendering of the page according to the HTML specification [1]. This change makes it so that they do this and, in addition, we do not take reftest screenshots until all no element is blocking the rendering. This change does not add support for the `blocking` attribute of `<link>`, but that can be added in a follow change. In addition to fixing a few tests, this change likely makes other tests no longer intermittent. We will need to watch CI runs after this lands in order to verify that though. Testing: This change fixes at least two WPT tests. Fixes: #26424. Signed-off-by: Martin Robinson <mrobinson@igalia.com>
6154 lines
234 KiB
Rust
6154 lines
234 KiB
Rust
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||
|
||
use std::cell::{Cell, RefCell};
|
||
use std::cmp::Ordering;
|
||
use std::collections::hash_map::Entry::{Occupied, Vacant};
|
||
use std::collections::{HashMap, HashSet, VecDeque};
|
||
use std::default::Default;
|
||
use std::rc::Rc;
|
||
use std::str::FromStr;
|
||
use std::sync::{LazyLock, Mutex};
|
||
use std::time::Duration;
|
||
|
||
use base::cross_process_instant::CrossProcessInstant;
|
||
use base::id::WebViewId;
|
||
use base::{Epoch, IpcSend, generic_channel};
|
||
use canvas_traits::canvas::CanvasId;
|
||
use canvas_traits::webgl::{WebGLContextId, WebGLMsg};
|
||
use chrono::Local;
|
||
use constellation_traits::{NavigationHistoryBehavior, ScriptToConstellationMessage};
|
||
use content_security_policy::sandboxing_directive::SandboxingFlagSet;
|
||
use content_security_policy::{CspList, PolicyDisposition};
|
||
use cookie::Cookie;
|
||
use cssparser::match_ignore_ascii_case;
|
||
use data_url::mime::Mime;
|
||
use devtools_traits::ScriptToDevtoolsControlMsg;
|
||
use dom_struct::dom_struct;
|
||
use embedder_traits::{AllowOrDeny, AnimationState, EmbedderMsg, FocusSequenceNumber, LoadStatus};
|
||
use encoding_rs::{Encoding, UTF_8};
|
||
use euclid::Point2D;
|
||
use euclid::default::{Rect, Size2D};
|
||
use html5ever::{LocalName, Namespace, QualName, local_name, ns};
|
||
use hyper_serde::Serde;
|
||
use js::rust::{HandleObject, HandleValue, MutableHandleValue};
|
||
use layout_api::{
|
||
PendingRestyle, ReflowGoal, ReflowPhasesRun, RestyleReason, ScrollContainerQueryFlags,
|
||
TrustedNodeAddress,
|
||
};
|
||
use metrics::{InteractiveFlag, InteractiveWindow, ProgressiveWebMetrics};
|
||
use net_traits::CookieSource::NonHTTP;
|
||
use net_traits::CoreResourceMsg::{GetCookiesForUrl, SetCookiesForUrl};
|
||
use net_traits::policy_container::PolicyContainer;
|
||
use net_traits::pub_domains::is_pub_domain;
|
||
use net_traits::request::{InsecureRequestsPolicy, RequestBuilder};
|
||
use net_traits::response::HttpsState;
|
||
use net_traits::{FetchResponseListener, ReferrerPolicy};
|
||
use percent_encoding::percent_decode;
|
||
use profile_traits::ipc as profile_ipc;
|
||
use profile_traits::time::TimerMetadataFrameType;
|
||
use regex::bytes::Regex;
|
||
use rustc_hash::{FxBuildHasher, FxHashMap};
|
||
use script_bindings::codegen::GenericBindings::ElementBinding::ElementMethods;
|
||
use script_bindings::interfaces::DocumentHelpers;
|
||
use script_bindings::script_runtime::JSContext;
|
||
use script_traits::{DocumentActivity, ProgressiveWebMetricType};
|
||
use servo_arc::Arc;
|
||
use servo_config::pref;
|
||
use servo_media::{ClientContextId, ServoMedia};
|
||
use servo_url::{ImmutableOrigin, MutableOrigin, ServoUrl};
|
||
use style::attr::AttrValue;
|
||
use style::context::QuirksMode;
|
||
use style::invalidation::element::restyle_hints::RestyleHint;
|
||
use style::selector_parser::Snapshot;
|
||
use style::shared_lock::SharedRwLock as StyleSharedRwLock;
|
||
use style::str::{split_html_space_chars, str_join};
|
||
use style::stylesheet_set::DocumentStylesheetSet;
|
||
use style::stylesheets::{Origin, OriginSet, Stylesheet};
|
||
use stylo_atoms::Atom;
|
||
use url::Host;
|
||
use uuid::Uuid;
|
||
#[cfg(feature = "webgpu")]
|
||
use webgpu_traits::WebGPUContextId;
|
||
use webrender_api::units::DeviceIntRect;
|
||
|
||
use crate::animation_timeline::AnimationTimeline;
|
||
use crate::animations::Animations;
|
||
use crate::canvas_context::CanvasContext as _;
|
||
use crate::document_loader::{DocumentLoader, LoadType};
|
||
use crate::dom::attr::Attr;
|
||
use crate::dom::beforeunloadevent::BeforeUnloadEvent;
|
||
use crate::dom::bindings::callback::ExceptionHandling;
|
||
use crate::dom::bindings::cell::{DomRefCell, Ref, RefMut};
|
||
use crate::dom::bindings::codegen::Bindings::BeforeUnloadEventBinding::BeforeUnloadEvent_Binding::BeforeUnloadEventMethods;
|
||
use crate::dom::bindings::codegen::Bindings::DocumentBinding::{
|
||
DocumentMethods, DocumentReadyState, DocumentVisibilityState, NamedPropertyValue,
|
||
};
|
||
use crate::dom::bindings::codegen::Bindings::ElementBinding::{
|
||
ScrollIntoViewContainer, ScrollIntoViewOptions, ScrollLogicalPosition,
|
||
};
|
||
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::HTMLOrSVGElementBinding::FocusOptions;
|
||
use crate::dom::bindings::codegen::Bindings::HTMLTextAreaElementBinding::HTMLTextAreaElementMethods;
|
||
use crate::dom::bindings::codegen::Bindings::NavigatorBinding::Navigator_Binding::NavigatorMethods;
|
||
use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods;
|
||
use crate::dom::bindings::codegen::Bindings::NodeFilterBinding::NodeFilter;
|
||
use crate::dom::bindings::codegen::Bindings::PerformanceBinding::PerformanceMethods;
|
||
use crate::dom::bindings::codegen::Bindings::PermissionStatusBinding::PermissionName;
|
||
use crate::dom::bindings::codegen::Bindings::WindowBinding::{
|
||
FrameRequestCallback, ScrollBehavior, ScrollOptions, WindowMethods,
|
||
};
|
||
use crate::dom::bindings::codegen::Bindings::XPathEvaluatorBinding::XPathEvaluatorMethods;
|
||
use crate::dom::bindings::codegen::Bindings::XPathNSResolverBinding::XPathNSResolver;
|
||
use crate::dom::bindings::codegen::UnionTypes::{
|
||
BooleanOrScrollIntoViewOptions, NodeOrString, StringOrElementCreationOptions,
|
||
TrustedHTMLOrString,
|
||
};
|
||
use crate::dom::bindings::domname::{
|
||
self, is_valid_attribute_local_name, is_valid_element_local_name, namespace_from_domstring,
|
||
};
|
||
use crate::dom::bindings::error::{Error, ErrorInfo, ErrorResult, Fallible};
|
||
use crate::dom::bindings::frozenarray::CachedFrozenArray;
|
||
use crate::dom::bindings::inheritance::{Castable, ElementTypeId, HTMLElementTypeId, NodeTypeId};
|
||
use crate::dom::bindings::num::Finite;
|
||
use crate::dom::bindings::refcounted::{Trusted, TrustedPromise};
|
||
use crate::dom::bindings::reflector::{DomGlobal, reflect_dom_object_with_proto};
|
||
use crate::dom::bindings::root::{Dom, DomRoot, LayoutDom, MutNullableDom, ToLayout};
|
||
use crate::dom::bindings::str::{DOMString, USVString};
|
||
use crate::dom::bindings::trace::{HashMapTracedValues, NoTrace};
|
||
use crate::dom::bindings::xmlname::matches_name_production;
|
||
use crate::dom::canvasrenderingcontext2d::CanvasRenderingContext2D;
|
||
use crate::dom::cdatasection::CDATASection;
|
||
use crate::dom::comment::Comment;
|
||
use crate::dom::compositionevent::CompositionEvent;
|
||
use crate::dom::cssstylesheet::CSSStyleSheet;
|
||
use crate::dom::customelementregistry::{CustomElementDefinition, CustomElementReactionStack};
|
||
use crate::dom::customevent::CustomEvent;
|
||
use crate::dom::document_event_handler::DocumentEventHandler;
|
||
use crate::dom::documentfragment::DocumentFragment;
|
||
use crate::dom::documentorshadowroot::{
|
||
DocumentOrShadowRoot, ServoStylesheetInDocument, StylesheetSource,
|
||
};
|
||
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};
|
||
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::html::htmlanchorelement::HTMLAnchorElement;
|
||
use crate::dom::html::htmlareaelement::HTMLAreaElement;
|
||
use crate::dom::html::htmlbaseelement::HTMLBaseElement;
|
||
use crate::dom::html::htmlcollection::{CollectionFilter, HTMLCollection};
|
||
use crate::dom::html::htmlelement::HTMLElement;
|
||
use crate::dom::html::htmlembedelement::HTMLEmbedElement;
|
||
use crate::dom::html::htmlformelement::{FormControl, FormControlElementHelpers, HTMLFormElement};
|
||
use crate::dom::html::htmlheadelement::HTMLHeadElement;
|
||
use crate::dom::html::htmlhtmlelement::HTMLHtmlElement;
|
||
use crate::dom::html::htmliframeelement::HTMLIFrameElement;
|
||
use crate::dom::html::htmlimageelement::HTMLImageElement;
|
||
use crate::dom::html::htmlinputelement::HTMLInputElement;
|
||
use crate::dom::html::htmlscriptelement::{HTMLScriptElement, ScriptResult};
|
||
use crate::dom::html::htmltextareaelement::HTMLTextAreaElement;
|
||
use crate::dom::html::htmltitleelement::HTMLTitleElement;
|
||
use crate::dom::intersectionobserver::IntersectionObserver;
|
||
use crate::dom::keyboardevent::KeyboardEvent;
|
||
use crate::dom::location::{Location, NavigationType};
|
||
use crate::dom::messageevent::MessageEvent;
|
||
use crate::dom::mouseevent::MouseEvent;
|
||
use crate::dom::node::{
|
||
CloneChildrenFlag, Node, NodeDamage, NodeFlags, NodeTraits, ShadowIncluding,
|
||
};
|
||
use crate::dom::nodeiterator::NodeIterator;
|
||
use crate::dom::nodelist::NodeList;
|
||
use crate::dom::pagetransitionevent::PageTransitionEvent;
|
||
use crate::dom::performanceentry::PerformanceEntry;
|
||
use crate::dom::performancepainttiming::PerformancePaintTiming;
|
||
use crate::dom::processinginstruction::ProcessingInstruction;
|
||
use crate::dom::promise::Promise;
|
||
use crate::dom::range::Range;
|
||
use crate::dom::resizeobserver::{ResizeObservationDepth, ResizeObserver};
|
||
use crate::dom::scrolling_box::ScrollingBox;
|
||
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::touchevent::TouchEvent as DomTouchEvent;
|
||
use crate::dom::touchlist::TouchList;
|
||
use crate::dom::treewalker::TreeWalker;
|
||
use crate::dom::trustedhtml::TrustedHTML;
|
||
use crate::dom::types::VisibilityStateEntry;
|
||
use crate::dom::uievent::UIEvent;
|
||
use crate::dom::virtualmethods::vtable_for;
|
||
use crate::dom::webgl::webglrenderingcontext::WebGLRenderingContext;
|
||
#[cfg(feature = "webgpu")]
|
||
use crate::dom::webgpu::gpucanvascontext::GPUCanvasContext;
|
||
use crate::dom::window::Window;
|
||
use crate::dom::windowproxy::WindowProxy;
|
||
use crate::dom::xpathevaluator::XPathEvaluator;
|
||
use crate::fetch::FetchCanceller;
|
||
use crate::iframe_collection::IFrameCollection;
|
||
use crate::image_animation::ImageAnimationManager;
|
||
use crate::messaging::{CommonScriptMsg, MainThreadScriptMsg};
|
||
use crate::mime::{APPLICATION, CHARSET};
|
||
use crate::network_listener::{NetworkListener, PreInvoke};
|
||
use crate::realms::{AlreadyInRealm, InRealm, enter_realm};
|
||
use crate::script_runtime::{CanGc, ScriptThreadEventCategory};
|
||
use crate::script_thread::ScriptThread;
|
||
use crate::stylesheet_set::StylesheetSetRef;
|
||
use crate::task::NonSendTaskBox;
|
||
use crate::task_source::TaskSourceName;
|
||
use crate::timers::OneshotTimerCallback;
|
||
|
||
pub(crate) enum TouchEventResult {
|
||
Processed(bool),
|
||
Forwarded,
|
||
}
|
||
|
||
#[derive(Clone, Copy, PartialEq)]
|
||
pub(crate) enum FireMouseEventType {
|
||
Move,
|
||
Over,
|
||
Out,
|
||
Enter,
|
||
Leave,
|
||
}
|
||
|
||
impl FireMouseEventType {
|
||
pub(crate) fn as_str(&self) -> &str {
|
||
match *self {
|
||
FireMouseEventType::Move => "mousemove",
|
||
FireMouseEventType::Over => "mouseover",
|
||
FireMouseEventType::Out => "mouseout",
|
||
FireMouseEventType::Enter => "mouseenter",
|
||
FireMouseEventType::Leave => "mouseleave",
|
||
}
|
||
}
|
||
}
|
||
|
||
#[derive(JSTraceable, MallocSizeOf)]
|
||
pub(crate) struct RefreshRedirectDue {
|
||
#[no_trace]
|
||
pub(crate) url: ServoUrl,
|
||
#[ignore_malloc_size_of = "non-owning"]
|
||
pub(crate) window: DomRoot<Window>,
|
||
}
|
||
impl RefreshRedirectDue {
|
||
pub(crate) fn invoke(self, can_gc: CanGc) {
|
||
self.window.Location().navigate(
|
||
self.url.clone(),
|
||
NavigationHistoryBehavior::Replace,
|
||
NavigationType::DeclarativeRefresh,
|
||
can_gc,
|
||
);
|
||
}
|
||
}
|
||
|
||
#[derive(Clone, Copy, Debug, JSTraceable, MallocSizeOf, PartialEq)]
|
||
pub(crate) enum IsHTMLDocument {
|
||
HTMLDocument,
|
||
NonHTMLDocument,
|
||
}
|
||
|
||
#[derive(JSTraceable, MallocSizeOf)]
|
||
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
|
||
struct FocusTransaction {
|
||
/// The focused element of this document.
|
||
element: Option<Dom<Element>>,
|
||
/// See [`Document::has_focus`].
|
||
has_focus: bool,
|
||
/// Focus options for the transaction
|
||
focus_options: FocusOptions,
|
||
}
|
||
|
||
/// Information about a declarative refresh
|
||
#[derive(JSTraceable, MallocSizeOf)]
|
||
pub(crate) enum DeclarativeRefresh {
|
||
PendingLoad {
|
||
#[no_trace]
|
||
url: ServoUrl,
|
||
time: u64,
|
||
},
|
||
CreatedAfterLoad,
|
||
}
|
||
|
||
/// <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>,
|
||
/// A helper used to process and store data related to input event handling.
|
||
event_handler: DocumentEventHandler,
|
||
/// Caches for the getElement methods. It is safe to use FxHash for these maps
|
||
/// as Atoms are `string_cache` items that will have the hash computed from a u32.
|
||
id_map: DomRefCell<HashMapTracedValues<Atom, Vec<Dom<Element>>, FxBuildHasher>>,
|
||
name_map: DomRefCell<HashMapTracedValues<Atom, Vec<Dom<Element>>, FxBuildHasher>>,
|
||
tag_map: DomRefCell<HashMapTracedValues<LocalName, Dom<HTMLCollection>, FxBuildHasher>>,
|
||
tagns_map: DomRefCell<HashMapTracedValues<QualName, Dom<HTMLCollection>, FxBuildHasher>>,
|
||
classes_map: DomRefCell<HashMapTracedValues<Vec<Atom>, Dom<HTMLCollection>>>,
|
||
images: MutNullableDom<HTMLCollection>,
|
||
embeds: MutNullableDom<HTMLCollection>,
|
||
links: MutNullableDom<HTMLCollection>,
|
||
forms: MutNullableDom<HTMLCollection>,
|
||
scripts: MutNullableDom<HTMLCollection>,
|
||
anchors: MutNullableDom<HTMLCollection>,
|
||
applets: MutNullableDom<HTMLCollection>,
|
||
/// Information about the `<iframes>` in this [`Document`].
|
||
iframes: RefCell<IFrameCollection>,
|
||
/// 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<ServoStylesheetInDocument>>,
|
||
stylesheet_list: MutNullableDom<StyleSheetList>,
|
||
ready_state: Cell<DocumentReadyState>,
|
||
/// Whether the DOMContentLoaded event has already been dispatched.
|
||
domcontentloaded_dispatched: Cell<bool>,
|
||
/// The state of this document's focus transaction.
|
||
focus_transaction: DomRefCell<Option<FocusTransaction>>,
|
||
/// The element that currently has the document focus context.
|
||
focused: MutNullableDom<Element>,
|
||
/// The last sequence number sent to the constellation.
|
||
#[no_trace]
|
||
focus_sequence: Cell<FocusSequenceNumber>,
|
||
/// Indicates whether the container is included in the top-level browsing
|
||
/// context's focus chain (not considering system focus). Permanently `true`
|
||
/// for a top-level document.
|
||
has_focus: Cell<bool>,
|
||
/// The script element that is currently executing.
|
||
current_script: MutNullableDom<HTMLScriptElement>,
|
||
/// <https://html.spec.whatwg.org/multipage/#pending-parsing-blocking-script>
|
||
pending_parsing_blocking_script: DomRefCell<Option<PendingScript>>,
|
||
/// Number of stylesheets that block executing the next parser-inserted script
|
||
script_blocking_stylesheets_count: Cell<u32>,
|
||
/// Number of elements that block the rendering of the page.
|
||
/// <https://html.spec.whatwg.org/multipage/#implicitly-potentially-render-blocking>
|
||
render_blocking_element_count: Cell<u32>,
|
||
/// <https://html.spec.whatwg.org/multipage/#list-of-scripts-that-will-execute-when-the-document-has-finished-parsing>
|
||
deferred_scripts: PendingInOrderScriptVec,
|
||
/// <https://html.spec.whatwg.org/multipage/#list-of-scripts-that-will-execute-in-order-as-soon-as-possible>
|
||
asap_in_order_scripts_list: PendingInOrderScriptVec,
|
||
/// <https://html.spec.whatwg.org/multipage/#set-of-scripts-that-will-execute-as-soon-as-possible>
|
||
asap_scripts_set: DomRefCell<Vec<Dom<HTMLScriptElement>>>,
|
||
/// <https://html.spec.whatwg.org/multipage/#animation-frame-callback-identifier>
|
||
/// Current identifier of animation frame callback
|
||
animation_frame_ident: Cell<u32>,
|
||
/// <https://html.spec.whatwg.org/multipage/#list-of-animation-frame-callbacks>
|
||
/// List of animation frame callbacks
|
||
animation_frame_list: DomRefCell<VecDeque<(u32, Option<AnimationFrameCallback>)>>,
|
||
/// Whether we're in the process of running animation callbacks.
|
||
///
|
||
/// Tracking this is not necessary for correctness. Instead, it is an optimization to avoid
|
||
/// sending needless `ChangeRunningAnimationsState` messages to 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<FxHashMap<Dom<Element>, NoTrace<PendingRestyle>>>,
|
||
/// A collection of reasons that the [`Document`] needs to be restyled at the next
|
||
/// opportunity for a reflow. If this is empty, then the [`Document`] does not need to
|
||
/// be restyled.
|
||
#[no_trace]
|
||
needs_restyle: Cell<RestyleReason>,
|
||
/// 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://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>
|
||
/// It is safe to use FxBuildHasher here as Atoms are in the string_cache
|
||
form_id_listener_map:
|
||
DomRefCell<HashMapTracedValues<Atom, HashSet<Dom<Element>>, FxBuildHasher>>,
|
||
#[no_trace]
|
||
interactive_time: DomRefCell<ProgressiveWebMetrics>,
|
||
#[no_trace]
|
||
tti_window: DomRefCell<InteractiveWindow>,
|
||
/// RAII canceller for Fetch
|
||
canceller: FetchCanceller,
|
||
/// <https://html.spec.whatwg.org/multipage/#throw-on-dynamic-markup-insertion-counter>
|
||
throw_on_dynamic_markup_insertion_counter: Cell<u64>,
|
||
/// <https://html.spec.whatwg.org/multipage/#page-showing>
|
||
page_showing: Cell<bool>,
|
||
/// Whether the document is salvageable.
|
||
salvageable: Cell<bool>,
|
||
/// Whether the document was aborted with an active parser
|
||
active_parser_was_aborted: Cell<bool>,
|
||
/// Whether the unload event has already been fired.
|
||
fired_unload: Cell<bool>,
|
||
/// List of responsive images
|
||
responsive_images: DomRefCell<Vec<Dom<HTMLImageElement>>>,
|
||
/// 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 NonSendTaskBox>>>,
|
||
/// <https://html.spec.whatwg.org/multipage/#completely-loaded>
|
||
completely_loaded: Cell<bool>,
|
||
/// Set of shadow roots connected to the document tree.
|
||
shadow_roots: DomRefCell<HashSet<Dom<ShadowRoot>>>,
|
||
/// Whether any of the shadow roots need the stylesheets flushed.
|
||
shadow_roots_styles_changed: Cell<bool>,
|
||
/// List of registered media controls.
|
||
/// We need to keep this list to allow the media controls to
|
||
/// access the "privileged" document.servoGetMediaControls(id) API,
|
||
/// where `id` needs to match any of the registered ShadowRoots
|
||
/// hosting the media controls UI.
|
||
media_controls: DomRefCell<HashMap<String, Dom<ShadowRoot>>>,
|
||
/// List of all context 2d IDs that need flushing.
|
||
dirty_2d_contexts:
|
||
DomRefCell<HashMapTracedValues<CanvasId, Dom<CanvasRenderingContext2D>, FxBuildHasher>>,
|
||
/// List of all WebGL context IDs that need flushing.
|
||
dirty_webgl_contexts:
|
||
DomRefCell<HashMapTracedValues<WebGLContextId, Dom<WebGLRenderingContext>, FxBuildHasher>>,
|
||
/// Whether or not animated images need to have their contents updated.
|
||
has_pending_animated_image_update: Cell<bool>,
|
||
/// List of all WebGPU contexts that need flushing.
|
||
#[cfg(feature = "webgpu")]
|
||
dirty_webgpu_contexts:
|
||
DomRefCell<HashMapTracedValues<WebGPUContextId, Dom<GPUCanvasContext>, FxBuildHasher>>,
|
||
/// <https://w3c.github.io/slection-api/#dfn-selection>
|
||
selection: MutNullableDom<Selection>,
|
||
/// A timeline for animations which is used for synchronizing animations.
|
||
/// <https://drafts.csswg.org/web-animations/#timeline>
|
||
animation_timeline: DomRefCell<AnimationTimeline>,
|
||
/// Animations for this Document
|
||
animations: DomRefCell<Animations>,
|
||
/// Image Animation Manager for this Document
|
||
image_animation_manager: DomRefCell<ImageAnimationManager>,
|
||
/// The nearest inclusive ancestors to all the nodes that require a restyle.
|
||
dirty_root: MutNullableDom<Element>,
|
||
/// <https://html.spec.whatwg.org/multipage/#will-declaratively-refresh>
|
||
declarative_refresh: DomRefCell<Option<DeclarativeRefresh>>,
|
||
/// <https://drafts.csswg.org/resize-observer/#dom-document-resizeobservers-slot>
|
||
///
|
||
/// Note: we are storing, but never removing, resize observers.
|
||
/// The lifetime of resize observers is specified at
|
||
/// <https://drafts.csswg.org/resize-observer/#lifetime>.
|
||
/// But implementing it comes with known problems:
|
||
/// - <https://bugzilla.mozilla.org/show_bug.cgi?id=1596992>
|
||
/// - <https://github.com/w3c/csswg-drafts/issues/4518>
|
||
resize_observers: DomRefCell<Vec<Dom<ResizeObserver>>>,
|
||
/// The set of all fonts loaded by this document.
|
||
/// <https://drafts.csswg.org/css-font-loading/#font-face-source>
|
||
fonts: MutNullableDom<FontFaceSet>,
|
||
/// <https://html.spec.whatwg.org/multipage/#visibility-state>
|
||
visibility_state: Cell<DocumentVisibilityState>,
|
||
/// <https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml>
|
||
status_code: Option<u16>,
|
||
/// <https://html.spec.whatwg.org/multipage/#is-initial-about:blank>
|
||
is_initial_about_blank: Cell<bool>,
|
||
/// <https://dom.spec.whatwg.org/#document-allow-declarative-shadow-roots>
|
||
allow_declarative_shadow_roots: Cell<bool>,
|
||
/// <https://w3c.github.io/webappsec-upgrade-insecure-requests/#insecure-requests-policy>
|
||
#[no_trace]
|
||
inherited_insecure_requests_policy: Cell<Option<InsecureRequestsPolicy>>,
|
||
//// <https://w3c.github.io/webappsec-mixed-content/#categorize-settings-object>
|
||
has_trustworthy_ancestor_origin: Cell<bool>,
|
||
/// <https://w3c.github.io/IntersectionObserver/#document-intersectionobservertaskqueued>
|
||
intersection_observer_task_queued: Cell<bool>,
|
||
/// Active intersection observers that should be processed by this document in
|
||
/// the update intersection observation steps.
|
||
/// <https://w3c.github.io/IntersectionObserver/#run-the-update-intersection-observations-steps>
|
||
/// > Let observer list be a list of all IntersectionObservers whose root is in the DOM tree of document.
|
||
/// > For the top-level browsing context, this includes implicit root observers.
|
||
///
|
||
/// Details of which document that should process an observers is discussed further at
|
||
/// <https://github.com/w3c/IntersectionObserver/issues/525>.
|
||
///
|
||
/// The lifetime of an intersection observer is specified at
|
||
/// <https://github.com/w3c/IntersectionObserver/issues/525>.
|
||
intersection_observers: DomRefCell<Vec<Dom<IntersectionObserver>>>,
|
||
/// The node that is currently highlighted by the devtools
|
||
highlighted_dom_node: MutNullableDom<Node>,
|
||
/// The constructed stylesheet that is adopted by this [Document].
|
||
/// <https://drafts.csswg.org/cssom/#dom-documentorshadowroot-adoptedstylesheets>
|
||
adopted_stylesheets: DomRefCell<Vec<Dom<CSSStyleSheet>>>,
|
||
/// Cached frozen array of [`Self::adopted_stylesheets`]
|
||
#[ignore_malloc_size_of = "mozjs"]
|
||
adopted_stylesheets_frozen_types: CachedFrozenArray,
|
||
/// <https://drafts.csswg.org/cssom-view/#document-pending-scroll-event-targets>
|
||
pending_scroll_event_targets: DomRefCell<Vec<Dom<EventTarget>>>,
|
||
/// When a `ResizeObserver` starts observing a target, this becomes true, which in turn is a
|
||
/// signal to the [`ScriptThread`] that a rendering update should happen.
|
||
resize_observer_started_observing_target: Cell<bool>,
|
||
/// Whether or not this [`Document`] is waiting on canvas image updates. If it is
|
||
/// waiting it will not do any new layout until the canvas images are up-to-date in
|
||
/// the renderer.
|
||
waiting_on_canvas_image_updates: Cell<bool>,
|
||
/// The current canvas epoch, which is used to track when canvas images have been
|
||
/// uploaded to the renderer after a rendering update. Until those images are uploaded
|
||
/// this `Document` will not perform any more rendering updates.
|
||
#[no_trace]
|
||
current_canvas_epoch: RefCell<Epoch>,
|
||
|
||
/// The global custom element reaction stack for this script thread.
|
||
#[conditional_malloc_size_of]
|
||
custom_element_reaction_stack: Rc<CustomElementReactionStack>,
|
||
#[no_trace]
|
||
#[ignore_malloc_size_of = "type from external crate"]
|
||
/// <https://html.spec.whatwg.org/multipage/#active-sandboxing-flag-set>,
|
||
active_sandboxing_flag_set: Cell<SandboxingFlagSet>,
|
||
}
|
||
|
||
#[allow(non_snake_case)]
|
||
impl Document {
|
||
pub(crate) fn note_node_with_dirty_descendants(&self, node: &Node) {
|
||
debug_assert!(*node.owner_doc() == *self);
|
||
if !node.is_connected() {
|
||
return;
|
||
}
|
||
|
||
let parent = match node.parent_in_flat_tree() {
|
||
Some(parent) => parent,
|
||
None => {
|
||
// There is no parent so this is the Document node, so we
|
||
// behave as if we were called with the document element.
|
||
let document_element = match self.GetDocumentElement() {
|
||
Some(element) => element,
|
||
None => return,
|
||
};
|
||
if let Some(dirty_root) = self.dirty_root.get() {
|
||
// There was an existing dirty root so we mark its
|
||
// ancestors as dirty until the document element.
|
||
for ancestor in dirty_root
|
||
.upcast::<Node>()
|
||
.inclusive_ancestors_in_flat_tree()
|
||
{
|
||
if ancestor.is::<Element>() {
|
||
ancestor.set_flag(NodeFlags::HAS_DIRTY_DESCENDANTS, true);
|
||
}
|
||
}
|
||
}
|
||
self.dirty_root.set(Some(&document_element));
|
||
return;
|
||
},
|
||
};
|
||
|
||
if parent.is::<Element>() {
|
||
if !parent.is_styled() {
|
||
return;
|
||
}
|
||
|
||
if parent.is_display_none() {
|
||
return;
|
||
}
|
||
}
|
||
|
||
let element_parent: DomRoot<Element>;
|
||
let element = match node.downcast::<Element>() {
|
||
Some(element) => element,
|
||
None => {
|
||
// Current node is not an element, it's probably a text node,
|
||
// we try to get its element parent.
|
||
match DomRoot::downcast::<Element>(parent) {
|
||
Some(parent) => {
|
||
element_parent = parent;
|
||
&element_parent
|
||
},
|
||
None => {
|
||
// Parent is not an element so it must be a document,
|
||
// and this is not an element either, so there is
|
||
// nothing to do.
|
||
return;
|
||
},
|
||
}
|
||
},
|
||
};
|
||
|
||
let dirty_root = match self.dirty_root.get() {
|
||
None => {
|
||
element
|
||
.upcast::<Node>()
|
||
.set_flag(NodeFlags::HAS_DIRTY_DESCENDANTS, true);
|
||
self.dirty_root.set(Some(element));
|
||
return;
|
||
},
|
||
Some(root) => root,
|
||
};
|
||
|
||
for ancestor in element.upcast::<Node>().inclusive_ancestors_in_flat_tree() {
|
||
if ancestor.get_flag(NodeFlags::HAS_DIRTY_DESCENDANTS) {
|
||
return;
|
||
}
|
||
|
||
if ancestor.is::<Element>() {
|
||
ancestor.set_flag(NodeFlags::HAS_DIRTY_DESCENDANTS, true);
|
||
}
|
||
}
|
||
|
||
let new_dirty_root = element
|
||
.upcast::<Node>()
|
||
.common_ancestor_in_flat_tree(dirty_root.upcast())
|
||
.expect("Couldn't find common ancestor");
|
||
|
||
let mut has_dirty_descendants = true;
|
||
for ancestor in dirty_root
|
||
.upcast::<Node>()
|
||
.inclusive_ancestors_in_flat_tree()
|
||
{
|
||
ancestor.set_flag(NodeFlags::HAS_DIRTY_DESCENDANTS, has_dirty_descendants);
|
||
has_dirty_descendants &= *ancestor != *new_dirty_root;
|
||
}
|
||
|
||
self.dirty_root
|
||
.set(Some(new_dirty_root.downcast::<Element>().unwrap()));
|
||
}
|
||
|
||
pub(crate) fn take_dirty_root(&self) -> Option<DomRoot<Element>> {
|
||
self.dirty_root.take()
|
||
}
|
||
|
||
#[inline]
|
||
pub(crate) fn loader(&self) -> Ref<'_, DocumentLoader> {
|
||
self.loader.borrow()
|
||
}
|
||
|
||
#[inline]
|
||
pub(crate) fn loader_mut(&self) -> RefMut<'_, DocumentLoader> {
|
||
self.loader.borrow_mut()
|
||
}
|
||
|
||
#[inline]
|
||
pub(crate) fn has_browsing_context(&self) -> bool {
|
||
self.has_browsing_context
|
||
}
|
||
|
||
/// <https://html.spec.whatwg.org/multipage/#concept-document-bc>
|
||
#[inline]
|
||
pub(crate) fn browsing_context(&self) -> Option<DomRoot<WindowProxy>> {
|
||
if self.has_browsing_context {
|
||
self.window.undiscarded_window_proxy()
|
||
} else {
|
||
None
|
||
}
|
||
}
|
||
|
||
pub(crate) fn webview_id(&self) -> WebViewId {
|
||
self.window.webview_id()
|
||
}
|
||
|
||
#[inline]
|
||
pub(crate) fn window(&self) -> &Window {
|
||
&self.window
|
||
}
|
||
|
||
#[inline]
|
||
pub(crate) fn is_html_document(&self) -> bool {
|
||
self.is_html_document
|
||
}
|
||
|
||
pub(crate) fn is_xhtml_document(&self) -> bool {
|
||
self.content_type.matches(APPLICATION, "xhtml+xml")
|
||
}
|
||
|
||
pub(crate) fn set_https_state(&self, https_state: HttpsState) {
|
||
self.https_state.set(https_state);
|
||
}
|
||
|
||
pub(crate) fn is_fully_active(&self) -> bool {
|
||
self.activity.get() == DocumentActivity::FullyActive
|
||
}
|
||
|
||
pub(crate) fn is_active(&self) -> bool {
|
||
self.activity.get() != DocumentActivity::Inactive
|
||
}
|
||
|
||
pub(crate) fn set_activity(&self, activity: DocumentActivity, can_gc: CanGc) {
|
||
// This function should only be called on documents with a browsing context
|
||
assert!(self.has_browsing_context);
|
||
if activity == self.activity.get() {
|
||
return;
|
||
}
|
||
|
||
// Set the document's activity level, reflow if necessary, and suspend or resume timers.
|
||
self.activity.set(activity);
|
||
let media = ServoMedia::get();
|
||
let pipeline_id = self.window().pipeline_id();
|
||
let client_context_id =
|
||
ClientContextId::build(pipeline_id.namespace_id.0, pipeline_id.index.0.get());
|
||
|
||
if activity != DocumentActivity::FullyActive {
|
||
self.window().suspend(can_gc);
|
||
media.suspend(&client_context_id);
|
||
return;
|
||
}
|
||
|
||
self.title_changed();
|
||
self.dirty_all_nodes();
|
||
self.window().resume(can_gc);
|
||
media.resume(&client_context_id);
|
||
|
||
if self.ready_state.get() != DocumentReadyState::Complete {
|
||
return;
|
||
}
|
||
|
||
// This step used to be Step 4.6 in html.spec.whatwg.org/multipage/#history-traversal
|
||
// But it's now Step 4 in https://html.spec.whatwg.org/multipage/#reactivate-a-document
|
||
// TODO: See #32687 for more information.
|
||
let document = Trusted::new(self);
|
||
self.owner_global()
|
||
.task_manager()
|
||
.dom_manipulation_task_source()
|
||
.queue(task!(fire_pageshow_event: move || {
|
||
let document = document.root();
|
||
let window = document.window();
|
||
// Step 4.6.1
|
||
if document.page_showing.get() {
|
||
return;
|
||
}
|
||
// Step 4.6.2 Set document's page showing flag to true.
|
||
document.page_showing.set(true);
|
||
// Step 4.6.3 Update the visibility state of document to "visible".
|
||
document.update_visibility_state(DocumentVisibilityState::Visible, CanGc::note());
|
||
// Step 4.6.4 Fire a page transition event named pageshow at document's relevant
|
||
// global object with true.
|
||
let event = PageTransitionEvent::new(
|
||
window,
|
||
atom!("pageshow"),
|
||
false, // bubbles
|
||
false, // cancelable
|
||
true, // persisted
|
||
CanGc::note(),
|
||
);
|
||
let event = event.upcast::<Event>();
|
||
event.set_trusted(true);
|
||
window.dispatch_event_with_target_override(event, CanGc::note());
|
||
}))
|
||
}
|
||
|
||
pub(crate) fn origin(&self) -> &MutableOrigin {
|
||
&self.origin
|
||
}
|
||
|
||
/// <https://dom.spec.whatwg.org/#concept-document-url>
|
||
pub(crate) fn url(&self) -> ServoUrl {
|
||
self.url.borrow().clone()
|
||
}
|
||
|
||
pub(crate) fn set_url(&self, url: ServoUrl) {
|
||
*self.url.borrow_mut() = url;
|
||
}
|
||
|
||
/// <https://html.spec.whatwg.org/multipage/#fallback-base-url>
|
||
pub(crate) fn fallback_base_url(&self) -> ServoUrl {
|
||
let document_url = self.url();
|
||
if let Some(browsing_context) = self.browsing_context() {
|
||
// Step 1: If document is an iframe srcdoc document, then return the
|
||
// document base URL of document's browsing context's container document.
|
||
let container_base_url = browsing_context
|
||
.parent()
|
||
.and_then(|parent| parent.document())
|
||
.map(|document| document.base_url());
|
||
if document_url.as_str() == "about:srcdoc" {
|
||
if let Some(base_url) = container_base_url {
|
||
return base_url;
|
||
}
|
||
}
|
||
// Step 2: If document's URL is about:blank, and document's browsing
|
||
// context's creator base URL is non-null, then return that creator base URL.
|
||
if document_url.as_str() == "about:blank" && browsing_context.has_creator_base_url() {
|
||
return browsing_context.creator_base_url().unwrap();
|
||
}
|
||
}
|
||
// Step 3: Return document's URL.
|
||
document_url
|
||
}
|
||
|
||
/// <https://html.spec.whatwg.org/multipage/#document-base-url>
|
||
pub(crate) fn base_url(&self) -> ServoUrl {
|
||
match self.base_element() {
|
||
// Step 1.
|
||
None => self.fallback_base_url(),
|
||
// Step 2.
|
||
Some(base) => base.frozen_base_url(),
|
||
}
|
||
}
|
||
|
||
pub(crate) fn add_restyle_reason(&self, reason: RestyleReason) {
|
||
self.needs_restyle.set(self.needs_restyle.get() | reason)
|
||
}
|
||
|
||
pub(crate) fn clear_restyle_reasons(&self) {
|
||
self.needs_restyle.set(RestyleReason::empty());
|
||
}
|
||
|
||
pub(crate) fn restyle_reason(&self) -> RestyleReason {
|
||
let mut condition = self.needs_restyle.get();
|
||
if self.stylesheets.borrow().has_changed() {
|
||
condition.insert(RestyleReason::StylesheetsChanged);
|
||
}
|
||
|
||
// FIXME: This should check the dirty bit on the document,
|
||
// not the document element. Needs some layout changes to make
|
||
// that workable.
|
||
if let Some(root) = self.GetDocumentElement() {
|
||
if root.upcast::<Node>().has_dirty_descendants() {
|
||
condition.insert(RestyleReason::DOMChanged);
|
||
}
|
||
}
|
||
|
||
if !self.pending_restyles.borrow().is_empty() {
|
||
condition.insert(RestyleReason::PendingRestyles);
|
||
}
|
||
|
||
condition
|
||
}
|
||
|
||
/// Returns the first `base` element in the DOM that has an `href` attribute.
|
||
pub(crate) fn base_element(&self) -> Option<DomRoot<HTMLBaseElement>> {
|
||
self.base_element.get()
|
||
}
|
||
|
||
/// 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::ContentOrHeritage);
|
||
}
|
||
|
||
/// Remove any existing association between the provided id and any elements in this document.
|
||
pub(crate) fn unregister_element_id(&self, to_unregister: &Element, id: Atom, can_gc: CanGc) {
|
||
self.document_or_shadow_root
|
||
.unregister_named_element(&self.id_map, to_unregister, &id);
|
||
self.reset_form_owner_for_listeners(&id, can_gc);
|
||
}
|
||
|
||
/// Associate an element present in this document with the provided id.
|
||
pub(crate) fn register_element_id(&self, element: &Element, id: Atom, can_gc: CanGc) {
|
||
let root = self.GetDocumentElement().expect(
|
||
"The element is in the document, so there must be a document \
|
||
element.",
|
||
);
|
||
self.document_or_shadow_root.register_named_element(
|
||
&self.id_map,
|
||
element,
|
||
&id,
|
||
DomRoot::from_ref(root.upcast::<Node>()),
|
||
);
|
||
self.reset_form_owner_for_listeners(&id, can_gc);
|
||
}
|
||
|
||
/// Remove any existing association between the provided name and any elements in this document.
|
||
pub(crate) fn unregister_element_name(&self, to_unregister: &Element, name: Atom) {
|
||
self.document_or_shadow_root
|
||
.unregister_named_element(&self.name_map, to_unregister, &name);
|
||
}
|
||
|
||
/// Associate an element present in this document with the provided name.
|
||
pub(crate) fn register_element_name(&self, element: &Element, name: Atom) {
|
||
let root = self.GetDocumentElement().expect(
|
||
"The element is in the document, so there must be a document \
|
||
element.",
|
||
);
|
||
self.document_or_shadow_root.register_named_element(
|
||
&self.name_map,
|
||
element,
|
||
&name,
|
||
DomRoot::from_ref(root.upcast::<Node>()),
|
||
);
|
||
}
|
||
|
||
pub(crate) fn register_form_id_listener<T: ?Sized + FormControl>(
|
||
&self,
|
||
id: DOMString,
|
||
listener: &T,
|
||
) {
|
||
let mut map = self.form_id_listener_map.borrow_mut();
|
||
let listener = listener.to_element();
|
||
let set = map.entry(Atom::from(id)).or_default();
|
||
set.insert(Dom::from_ref(listener));
|
||
}
|
||
|
||
pub(crate) fn unregister_form_id_listener<T: ?Sized + FormControl>(
|
||
&self,
|
||
id: DOMString,
|
||
listener: &T,
|
||
) {
|
||
let mut map = self.form_id_listener_map.borrow_mut();
|
||
if let Occupied(mut entry) = map.entry(Atom::from(id)) {
|
||
entry
|
||
.get_mut()
|
||
.remove(&Dom::from_ref(listener.to_element()));
|
||
if entry.get().is_empty() {
|
||
entry.remove();
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Attempt to find a named element in this page's document.
|
||
/// <https://html.spec.whatwg.org/multipage/#the-indicated-part-of-the-document>
|
||
pub(crate) fn find_fragment_node(&self, fragid: &str) -> Option<DomRoot<Element>> {
|
||
// Step 1 is not handled here; the fragid is already obtained by the calling function
|
||
// Step 2: Simply use None to indicate the top of the document.
|
||
// Step 3 & 4
|
||
percent_decode(fragid.as_bytes())
|
||
.decode_utf8()
|
||
.ok()
|
||
// Step 5
|
||
.and_then(|decoded_fragid| self.get_element_by_id(&Atom::from(decoded_fragid)))
|
||
// Step 6
|
||
.or_else(|| self.get_anchor_by_name(fragid))
|
||
// Step 7 & 8
|
||
}
|
||
|
||
/// Scroll to the target element, and when we do not find a target
|
||
/// and the fragment is empty or "top", scroll to the top.
|
||
/// <https://html.spec.whatwg.org/multipage/#scroll-to-the-fragment-identifier>
|
||
pub(crate) fn check_and_scroll_fragment(&self, fragment: &str) {
|
||
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>().border_box().unwrap_or_default();
|
||
|
||
// 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, y, ScrollBehavior::Instant)
|
||
}
|
||
}
|
||
|
||
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
|
||
/// <https://html.spec.whatwg.org/multipage/#concept-n-script>
|
||
pub(crate) fn scripting_enabled(&self) -> bool {
|
||
// Scripting is enabled for a node node if node's node document's browsing context is non-null,
|
||
// and scripting is enabled for node's relevant settings object.
|
||
self.has_browsing_context() &&
|
||
// Either settings's global object is not a Window object,
|
||
// or settings's global object's associated Document's active sandboxing flag
|
||
// set does not have its sandboxed scripts browsing context flag set.
|
||
!self.has_active_sandboxing_flag(
|
||
SandboxingFlagSet::SANDBOXED_SCRIPTS_BROWSING_CONTEXT_FLAG,
|
||
)
|
||
}
|
||
|
||
/// Return the element that currently has focus.
|
||
// https://w3c.github.io/uievents/#events-focusevent-doc-focus
|
||
pub(crate) fn get_focused_element(&self) -> Option<DomRoot<Element>> {
|
||
self.focused.get()
|
||
}
|
||
|
||
/// Get the last sequence number sent to the constellation.
|
||
///
|
||
/// Received focus-related messages with sequence numbers less than the one
|
||
/// returned by this method must be discarded.
|
||
pub fn get_focus_sequence(&self) -> FocusSequenceNumber {
|
||
self.focus_sequence.get()
|
||
}
|
||
|
||
/// Generate the next sequence number for focus-related messages.
|
||
fn increment_fetch_focus_sequence(&self) -> FocusSequenceNumber {
|
||
self.focus_sequence.set(FocusSequenceNumber(
|
||
self.focus_sequence
|
||
.get()
|
||
.0
|
||
.checked_add(1)
|
||
.expect("too many focus messages have been sent"),
|
||
));
|
||
self.focus_sequence.get()
|
||
}
|
||
|
||
pub(crate) fn has_focus_transaction(&self) -> bool {
|
||
self.focus_transaction.borrow().is_some()
|
||
}
|
||
|
||
/// 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.
|
||
pub(crate) fn begin_focus_transaction(&self) {
|
||
// Initialize it with the current state
|
||
*self.focus_transaction.borrow_mut() = Some(FocusTransaction {
|
||
element: self.focused.get().as_deref().map(Dom::from_ref),
|
||
has_focus: self.has_focus.get(),
|
||
focus_options: FocusOptions {
|
||
preventScroll: true,
|
||
},
|
||
});
|
||
}
|
||
|
||
/// <https://html.spec.whatwg.org/multipage/#focus-fixup-rule>
|
||
pub(crate) fn perform_focus_fixup_rule(&self, not_focusable: &Element, can_gc: CanGc) {
|
||
// Return if `not_focusable` is not the designated focused area of the
|
||
// `Document`.
|
||
if Some(not_focusable) != self.focused.get().as_deref() {
|
||
return;
|
||
}
|
||
|
||
let implicit_transaction = self.focus_transaction.borrow().is_none();
|
||
|
||
if implicit_transaction {
|
||
self.begin_focus_transaction();
|
||
}
|
||
|
||
// Designate the viewport as the new focused area of the `Document`, but
|
||
// do not run the focusing steps.
|
||
{
|
||
let mut focus_transaction = self.focus_transaction.borrow_mut();
|
||
focus_transaction.as_mut().unwrap().element = None;
|
||
}
|
||
|
||
if implicit_transaction {
|
||
self.commit_focus_transaction(FocusInitiator::Local, can_gc);
|
||
}
|
||
}
|
||
|
||
/// Request that the given element receive focus with default options.
|
||
/// See [`Self::request_focus_with_options`] for the details.
|
||
pub(crate) fn request_focus(
|
||
&self,
|
||
elem: Option<&Element>,
|
||
focus_initiator: FocusInitiator,
|
||
can_gc: CanGc,
|
||
) {
|
||
self.request_focus_with_options(
|
||
elem,
|
||
focus_initiator,
|
||
FocusOptions {
|
||
preventScroll: true,
|
||
},
|
||
can_gc,
|
||
);
|
||
}
|
||
|
||
/// Request that the given element receive focus once the current
|
||
/// transaction is complete. `None` specifies to focus the document.
|
||
///
|
||
/// If there's no ongoing transaction, this method automatically starts and
|
||
/// commits an implicit transaction.
|
||
pub(crate) fn request_focus_with_options(
|
||
&self,
|
||
elem: Option<&Element>,
|
||
focus_initiator: FocusInitiator,
|
||
focus_options: FocusOptions,
|
||
can_gc: CanGc,
|
||
) {
|
||
// If an element is specified, and it's non-focusable, ignore the
|
||
// request.
|
||
if elem.is_some_and(|e| !e.is_focusable_area()) {
|
||
return;
|
||
}
|
||
|
||
let implicit_transaction = self.focus_transaction.borrow().is_none();
|
||
|
||
if implicit_transaction {
|
||
self.begin_focus_transaction();
|
||
}
|
||
|
||
{
|
||
let mut focus_transaction = self.focus_transaction.borrow_mut();
|
||
let focus_transaction = focus_transaction.as_mut().unwrap();
|
||
focus_transaction.element = elem.map(Dom::from_ref);
|
||
focus_transaction.has_focus = true;
|
||
focus_transaction.focus_options = focus_options;
|
||
}
|
||
|
||
if implicit_transaction {
|
||
self.commit_focus_transaction(focus_initiator, can_gc);
|
||
}
|
||
}
|
||
|
||
/// Update the local focus state accordingly after being notified that the
|
||
/// document's container is removed from the top-level browsing context's
|
||
/// focus chain (not considering system focus).
|
||
pub(crate) fn handle_container_unfocus(&self, can_gc: CanGc) {
|
||
if self.window().parent_info().is_none() {
|
||
warn!("Top-level document cannot be unfocused");
|
||
return;
|
||
}
|
||
|
||
// Since this method is called from an event loop, there mustn't be
|
||
// an in-progress focus transaction
|
||
assert!(
|
||
self.focus_transaction.borrow().is_none(),
|
||
"there mustn't be an in-progress focus transaction at this point"
|
||
);
|
||
|
||
// Start an implicit focus transaction
|
||
self.begin_focus_transaction();
|
||
|
||
// Update the transaction
|
||
{
|
||
let mut focus_transaction = self.focus_transaction.borrow_mut();
|
||
focus_transaction.as_mut().unwrap().has_focus = false;
|
||
}
|
||
|
||
// Commit the implicit focus transaction
|
||
self.commit_focus_transaction(FocusInitiator::Remote, can_gc);
|
||
}
|
||
|
||
/// Reassign the focus context to the element that last requested focus during this
|
||
/// transaction, or the document if no elements requested it.
|
||
pub(crate) fn commit_focus_transaction(&self, focus_initiator: FocusInitiator, can_gc: CanGc) {
|
||
let (mut new_focused, new_focus_state, prevent_scroll) = {
|
||
let focus_transaction = self.focus_transaction.borrow();
|
||
let focus_transaction = focus_transaction
|
||
.as_ref()
|
||
.expect("no focus transaction in progress");
|
||
(
|
||
focus_transaction
|
||
.element
|
||
.as_ref()
|
||
.map(|e| DomRoot::from_ref(&**e)),
|
||
focus_transaction.has_focus,
|
||
focus_transaction.focus_options.preventScroll,
|
||
)
|
||
};
|
||
*self.focus_transaction.borrow_mut() = None;
|
||
|
||
if !new_focus_state {
|
||
// In many browsers, a document forgets its focused area when the
|
||
// document is removed from the top-level BC's focus chain
|
||
if new_focused.take().is_some() {
|
||
trace!(
|
||
"Forgetting the document's focused area because the \
|
||
document's container was removed from the top-level BC's \
|
||
focus chain"
|
||
);
|
||
}
|
||
}
|
||
|
||
let old_focused = self.focused.get();
|
||
let old_focus_state = self.has_focus.get();
|
||
|
||
debug!(
|
||
"Committing focus transaction: {:?} → {:?}",
|
||
(&old_focused, old_focus_state),
|
||
(&new_focused, new_focus_state),
|
||
);
|
||
|
||
// `*_focused_filtered` indicates the local element (if any) included in
|
||
// the top-level BC's focus chain.
|
||
let old_focused_filtered = old_focused.as_ref().filter(|_| old_focus_state);
|
||
let new_focused_filtered = new_focused.as_ref().filter(|_| new_focus_state);
|
||
|
||
let trace_focus_chain = |name, element, doc| {
|
||
trace!(
|
||
"{} local focus chain: {}",
|
||
name,
|
||
match (element, doc) {
|
||
(Some(e), _) => format!("[{:?}, document]", e),
|
||
(None, true) => "[document]".to_owned(),
|
||
(None, false) => "[]".to_owned(),
|
||
}
|
||
);
|
||
};
|
||
|
||
trace_focus_chain("Old", old_focused_filtered, old_focus_state);
|
||
trace_focus_chain("New", new_focused_filtered, new_focus_state);
|
||
|
||
if old_focused_filtered != new_focused_filtered {
|
||
if let Some(elem) = &old_focused_filtered {
|
||
let node = elem.upcast::<Node>();
|
||
elem.set_focus_state(false);
|
||
// FIXME: pass appropriate relatedTarget
|
||
if node.is_connected() {
|
||
self.fire_focus_event(FocusEventType::Blur, node.upcast(), None, can_gc);
|
||
}
|
||
|
||
// Notify the embedder to hide the input method.
|
||
if elem.input_method_type().is_some() {
|
||
self.send_to_embedder(EmbedderMsg::HideIME(self.webview_id()));
|
||
}
|
||
}
|
||
}
|
||
|
||
if old_focus_state != new_focus_state && !new_focus_state {
|
||
self.fire_focus_event(FocusEventType::Blur, self.global().upcast(), None, can_gc);
|
||
}
|
||
|
||
self.focused.set(new_focused.as_deref());
|
||
self.has_focus.set(new_focus_state);
|
||
|
||
if old_focus_state != new_focus_state && new_focus_state {
|
||
self.fire_focus_event(FocusEventType::Focus, self.global().upcast(), None, can_gc);
|
||
}
|
||
|
||
if old_focused_filtered != new_focused_filtered {
|
||
if let Some(elem) = &new_focused_filtered {
|
||
elem.set_focus_state(true);
|
||
let node = elem.upcast::<Node>();
|
||
// FIXME: pass appropriate relatedTarget
|
||
self.fire_focus_event(FocusEventType::Focus, node.upcast(), None, can_gc);
|
||
|
||
// Notify the embedder to display an input method.
|
||
if let Some(kind) = elem.input_method_type() {
|
||
let rect = elem.upcast::<Node>().border_box().unwrap_or_default();
|
||
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()),
|
||
));
|
||
}
|
||
// Scroll operation to happen after element gets focus.
|
||
// This is needed to ensure that the focused element is visible.
|
||
// Only scroll if preventScroll was not specified
|
||
if !prevent_scroll {
|
||
elem.ScrollIntoView(BooleanOrScrollIntoViewOptions::ScrollIntoViewOptions(
|
||
ScrollIntoViewOptions {
|
||
parent: ScrollOptions {
|
||
behavior: ScrollBehavior::Smooth,
|
||
},
|
||
block: ScrollLogicalPosition::Center,
|
||
inline: ScrollLogicalPosition::Center,
|
||
container: ScrollIntoViewContainer::All,
|
||
},
|
||
));
|
||
}
|
||
}
|
||
}
|
||
|
||
if focus_initiator != FocusInitiator::Local {
|
||
return;
|
||
}
|
||
|
||
// We are the initiator of the focus operation, so we must broadcast
|
||
// the change we intend to make.
|
||
match (old_focus_state, new_focus_state) {
|
||
(_, true) => {
|
||
// Advertise the change in the focus chain.
|
||
// <https://html.spec.whatwg.org/multipage/#focus-chain>
|
||
// <https://html.spec.whatwg.org/multipage/#focusing-steps>
|
||
//
|
||
// If the top-level BC doesn't have system focus, this won't
|
||
// have an immediate effect, but it will when we gain system
|
||
// focus again. Therefore we still have to send `ScriptMsg::
|
||
// Focus`.
|
||
//
|
||
// When a container with a non-null nested browsing context is
|
||
// focused, its active document becomes the focused area of the
|
||
// top-level browsing context instead. Therefore we need to let
|
||
// the constellation know if such a container is focused.
|
||
//
|
||
// > The focusing steps for an object `new focus target` [...]
|
||
// >
|
||
// > 3. If `new focus target` is a browsing context container
|
||
// > with non-null nested browsing context, then set
|
||
// > `new focus target` to the nested browsing context's
|
||
// > active document.
|
||
let child_browsing_context_id = new_focused
|
||
.as_ref()
|
||
.and_then(|elem| elem.downcast::<HTMLIFrameElement>())
|
||
.and_then(|iframe| iframe.browsing_context_id());
|
||
|
||
let sequence = self.increment_fetch_focus_sequence();
|
||
|
||
debug!(
|
||
"Advertising the focus request to the constellation \
|
||
with sequence number {} and child BC ID {}",
|
||
sequence,
|
||
child_browsing_context_id
|
||
.as_ref()
|
||
.map(|id| id as &dyn std::fmt::Display)
|
||
.unwrap_or(&"(none)"),
|
||
);
|
||
|
||
self.window()
|
||
.send_to_constellation(ScriptToConstellationMessage::Focus(
|
||
child_browsing_context_id,
|
||
sequence,
|
||
));
|
||
},
|
||
(false, false) => {
|
||
// Our `Document` doesn't have focus, and we intend to keep it
|
||
// this way.
|
||
},
|
||
(true, false) => {
|
||
unreachable!(
|
||
"Can't lose the document's focus without specifying \
|
||
another one to focus"
|
||
);
|
||
},
|
||
}
|
||
}
|
||
|
||
/// Handles any updates when the document's title has changed.
|
||
pub(crate) fn title_changed(&self) {
|
||
if self.browsing_context().is_some() {
|
||
self.send_title_to_embedder();
|
||
let title = String::from(self.Title());
|
||
self.window
|
||
.send_to_constellation(ScriptToConstellationMessage::TitleChanged(
|
||
self.window.pipeline_id(),
|
||
title.clone(),
|
||
));
|
||
if let Some(chan) = self.window.as_global_scope().devtools_chan() {
|
||
let _ = chan.send(ScriptToDevtoolsControlMsg::TitleChanged(
|
||
self.window.pipeline_id(),
|
||
title,
|
||
));
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Determine the title of the [`Document`] according to the specification at:
|
||
/// <https://html.spec.whatwg.org/multipage/#document.title>. The difference
|
||
/// here is that when the title isn't specified `None` is returned.
|
||
fn title(&self) -> Option<DOMString> {
|
||
let title = self.GetDocumentElement().and_then(|root| {
|
||
if root.namespace() == &ns!(svg) && root.local_name() == &local_name!("svg") {
|
||
// Step 1.
|
||
root.upcast::<Node>()
|
||
.child_elements()
|
||
.find(|node| {
|
||
node.namespace() == &ns!(svg) && node.local_name() == &local_name!("title")
|
||
})
|
||
.map(DomRoot::upcast::<Node>)
|
||
} else {
|
||
// Step 2.
|
||
root.upcast::<Node>()
|
||
.traverse_preorder(ShadowIncluding::No)
|
||
.find(|node| node.is::<HTMLTitleElement>())
|
||
}
|
||
});
|
||
|
||
title.map(|title| {
|
||
// Steps 3-4.
|
||
let value = title.child_text_content();
|
||
DOMString::from(str_join(value.split_html_space_characters(), " "))
|
||
})
|
||
}
|
||
|
||
/// Sends this document's title to the constellation.
|
||
pub(crate) fn send_title_to_embedder(&self) {
|
||
let window = self.window();
|
||
if window.is_top_level() {
|
||
let title = self.title().map(String::from);
|
||
self.send_to_embedder(EmbedderMsg::ChangePageTitle(self.webview_id(), title));
|
||
}
|
||
}
|
||
|
||
pub(crate) fn send_to_embedder(&self, msg: EmbedderMsg) {
|
||
let window = self.window();
|
||
window.send_to_embedder(msg);
|
||
}
|
||
|
||
pub(crate) fn dirty_all_nodes(&self) {
|
||
let root = match self.GetDocumentElement() {
|
||
Some(root) => root,
|
||
None => return,
|
||
};
|
||
for node in root
|
||
.upcast::<Node>()
|
||
.traverse_preorder(ShadowIncluding::Yes)
|
||
{
|
||
node.dirty(NodeDamage::Other)
|
||
}
|
||
}
|
||
|
||
/// <https://drafts.csswg.org/cssom-view/#document-run-the-scroll-steps>
|
||
pub(crate) fn run_the_scroll_steps(&self, can_gc: CanGc) {
|
||
// Step 1.
|
||
// > Run the steps to dispatch pending scrollsnapchanging events for doc.
|
||
// TODO(#7673): Implement scroll snapping
|
||
|
||
// Step 2
|
||
// > For each item target in doc’s pending scroll event targets, in the order they
|
||
// > were added to the list, run these substeps:
|
||
// Step 3.
|
||
// > Empty doc’s pending scroll event targets.
|
||
// Since the scroll event callback could trigger another scroll event, we are taking all of the
|
||
// current scroll event to avoid borrow checking error.
|
||
rooted_vec!(let notify_list <- self.pending_scroll_event_targets.take().into_iter());
|
||
for target in notify_list.iter() {
|
||
if target.downcast::<Document>().is_some() {
|
||
// Step 2.1
|
||
// > If target is a Document, fire an event named scroll that bubbles at target.
|
||
target.fire_bubbling_event(Atom::from("scroll"), can_gc);
|
||
} else if target.downcast::<Element>().is_some() {
|
||
// Step 2.2
|
||
// > Otherwise, fire an event named scroll at target.
|
||
target.fire_event(Atom::from("scroll"), can_gc);
|
||
}
|
||
}
|
||
|
||
// Step 4.
|
||
// > Run the steps to dispatch pending scrollsnapchange events for doc.
|
||
// TODO(#7673): Implement scroll snapping
|
||
}
|
||
|
||
/// Whenever a viewport gets scrolled (whether in response to user interaction or by an
|
||
/// API), the user agent must run these steps:
|
||
/// <https://drafts.csswg.org/cssom-view/#scrolling-events>
|
||
pub(crate) fn handle_viewport_scroll_event(&self) {
|
||
// Step 2.
|
||
// > If doc is a snap container, run the steps to update scrollsnapchanging targets
|
||
// > for doc with doc’s eventual snap target in the block axis as newBlockTarget and
|
||
// > doc’s eventual snap target in the inline axis as newInlineTarget.
|
||
// TODO(#7673): Implement scroll snapping
|
||
|
||
// Step 3.
|
||
// > If doc is already in doc’s pending scroll event targets, abort these steps.
|
||
let target = self.upcast::<EventTarget>();
|
||
if self
|
||
.pending_scroll_event_targets
|
||
.borrow()
|
||
.iter()
|
||
.any(|other_target| *other_target == target)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// Step 4.
|
||
// > Append doc to doc’s pending scroll event targets.
|
||
self.pending_scroll_event_targets
|
||
.borrow_mut()
|
||
.push(Dom::from_ref(target));
|
||
}
|
||
|
||
/// Whenever an element gets scrolled (whether in response to user interaction or by an
|
||
/// API), the user agent must run these steps:
|
||
/// <https://drafts.csswg.org/cssom-view/#scrolling-events>
|
||
pub(crate) fn handle_element_scroll_event(&self, element: &Element) {
|
||
// Step 2.
|
||
// > If the element is a snap container, run the steps to update scrollsnapchanging
|
||
// > targets for the element with the element’s eventual snap target in the block
|
||
// > axis as newBlockTarget and the element’s eventual snap target in the inline axis
|
||
// > as newInlineTarget.
|
||
// TODO(#7673): Implement scroll snapping
|
||
|
||
// Step 3.
|
||
// > If the element is already in doc’s pending scroll event targets, abort these steps.
|
||
let target = element.upcast::<EventTarget>();
|
||
if self
|
||
.pending_scroll_event_targets
|
||
.borrow()
|
||
.iter()
|
||
.any(|other_target| *other_target == target)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// Step 4.
|
||
// > Append the element to doc’s pending scroll event targets.
|
||
self.pending_scroll_event_targets
|
||
.borrow_mut()
|
||
.push(Dom::from_ref(target));
|
||
}
|
||
|
||
// https://dom.spec.whatwg.org/#converting-nodes-into-a-node
|
||
pub(crate) fn node_from_nodes_and_strings(
|
||
&self,
|
||
mut nodes: Vec<NodeOrString>,
|
||
can_gc: CanGc,
|
||
) -> Fallible<DomRoot<Node>> {
|
||
if nodes.len() == 1 {
|
||
Ok(match nodes.pop().unwrap() {
|
||
NodeOrString::Node(node) => node,
|
||
NodeOrString::String(string) => {
|
||
DomRoot::upcast(self.CreateTextNode(string, can_gc))
|
||
},
|
||
})
|
||
} else {
|
||
let fragment = DomRoot::upcast::<Node>(self.CreateDocumentFragment(can_gc));
|
||
for node in nodes {
|
||
match node {
|
||
NodeOrString::Node(node) => {
|
||
fragment.AppendChild(&node, can_gc)?;
|
||
},
|
||
NodeOrString::String(string) => {
|
||
let node = DomRoot::upcast::<Node>(self.CreateTextNode(string, can_gc));
|
||
// No try!() here because appending a text node
|
||
// should not fail.
|
||
fragment.AppendChild(&node, can_gc).unwrap();
|
||
},
|
||
}
|
||
}
|
||
Ok(fragment)
|
||
}
|
||
}
|
||
|
||
pub(crate) fn get_body_attribute(&self, local_name: &LocalName) -> DOMString {
|
||
match self.GetBody() {
|
||
Some(ref body) if body.is_body_element() => {
|
||
body.upcast::<Element>().get_string_attribute(local_name)
|
||
},
|
||
_ => DOMString::new(),
|
||
}
|
||
}
|
||
|
||
pub(crate) fn set_body_attribute(
|
||
&self,
|
||
local_name: &LocalName,
|
||
value: DOMString,
|
||
can_gc: CanGc,
|
||
) {
|
||
if let Some(ref body) = self.GetBody().filter(|elem| elem.is_body_element()) {
|
||
let body = body.upcast::<Element>();
|
||
let value = body.parse_attribute(&ns!(), local_name, value);
|
||
body.set_attribute(local_name, value, can_gc);
|
||
}
|
||
}
|
||
|
||
pub(crate) fn set_current_script(&self, script: Option<&HTMLScriptElement>) {
|
||
self.current_script.set(script);
|
||
}
|
||
|
||
pub(crate) fn get_script_blocking_stylesheets_count(&self) -> u32 {
|
||
self.script_blocking_stylesheets_count.get()
|
||
}
|
||
|
||
pub(crate) fn increment_script_blocking_stylesheet_count(&self) {
|
||
let count_cell = &self.script_blocking_stylesheets_count;
|
||
count_cell.set(count_cell.get() + 1);
|
||
}
|
||
|
||
pub(crate) fn decrement_script_blocking_stylesheet_count(&self) {
|
||
let count_cell = &self.script_blocking_stylesheets_count;
|
||
assert!(count_cell.get() > 0);
|
||
count_cell.set(count_cell.get() - 1);
|
||
}
|
||
|
||
pub(crate) fn render_blocking_element_count(&self) -> u32 {
|
||
self.render_blocking_element_count.get()
|
||
}
|
||
|
||
pub(crate) fn increment_render_blocking_element_count(&self) {
|
||
let count_cell = &self.render_blocking_element_count;
|
||
count_cell.set(count_cell.get() + 1);
|
||
}
|
||
|
||
pub(crate) fn decrement_render_blocking_element_count(&self) {
|
||
let count_cell = &self.render_blocking_element_count;
|
||
assert!(count_cell.get() > 0);
|
||
count_cell.set(count_cell.get() - 1);
|
||
}
|
||
|
||
pub(crate) fn invalidate_stylesheets(&self) {
|
||
self.stylesheets.borrow_mut().force_dirty(OriginSet::all());
|
||
|
||
// Mark the document element dirty so a reflow will be performed.
|
||
//
|
||
// FIXME(emilio): Use the DocumentStylesheetSet invalidation stuff.
|
||
if let Some(element) = self.GetDocumentElement() {
|
||
element.upcast::<Node>().dirty(NodeDamage::Style);
|
||
}
|
||
}
|
||
|
||
/// Whether or not this `Document` has any active requestAnimationFrame callbacks
|
||
/// registered.
|
||
pub(crate) fn has_active_request_animation_frame_callbacks(&self) -> bool {
|
||
!self.animation_frame_list.borrow().is_empty()
|
||
}
|
||
|
||
/// <https://html.spec.whatwg.org/multipage/#dom-window-requestanimationframe>
|
||
pub(crate) fn request_animation_frame(&self, callback: AnimationFrameCallback) -> u32 {
|
||
let ident = self.animation_frame_ident.get() + 1;
|
||
self.animation_frame_ident.set(ident);
|
||
|
||
let had_animation_frame_callbacks;
|
||
{
|
||
let mut animation_frame_list = self.animation_frame_list.borrow_mut();
|
||
had_animation_frame_callbacks = !animation_frame_list.is_empty();
|
||
animation_frame_list.push_back((ident, Some(callback)));
|
||
}
|
||
|
||
// No need to send a `ChangeRunningAnimationsState` if we're running animation callbacks:
|
||
// we're guaranteed to already be in the "animation callbacks present" state.
|
||
//
|
||
// This reduces CPU usage by avoiding needless thread wakeups in the common case of
|
||
// repeated rAF.
|
||
if !self.running_animation_callbacks.get() && !had_animation_frame_callbacks {
|
||
self.window().send_to_constellation(
|
||
ScriptToConstellationMessage::ChangeRunningAnimationsState(
|
||
AnimationState::AnimationCallbacksPresent,
|
||
),
|
||
);
|
||
}
|
||
|
||
ident
|
||
}
|
||
|
||
/// <https://html.spec.whatwg.org/multipage/#dom-window-cancelanimationframe>
|
||
pub(crate) fn cancel_animation_frame(&self, ident: u32) {
|
||
let mut list = self.animation_frame_list.borrow_mut();
|
||
if let Some(pair) = list.iter_mut().find(|pair| pair.0 == ident) {
|
||
pair.1 = None;
|
||
}
|
||
}
|
||
|
||
/// <https://html.spec.whatwg.org/multipage/#run-the-animation-frame-callbacks>
|
||
pub(crate) fn run_the_animation_frame_callbacks(&self, can_gc: CanGc) {
|
||
let _realm = enter_realm(self);
|
||
|
||
self.running_animation_callbacks.set(true);
|
||
let timing = self.global().performance().Now();
|
||
|
||
let num_callbacks = self.animation_frame_list.borrow().len();
|
||
for _ in 0..num_callbacks {
|
||
let (_, maybe_callback) = self.animation_frame_list.borrow_mut().pop_front().unwrap();
|
||
if let Some(callback) = maybe_callback {
|
||
callback.call(self, *timing, can_gc);
|
||
}
|
||
}
|
||
self.running_animation_callbacks.set(false);
|
||
|
||
if self.animation_frame_list.borrow().is_empty() {
|
||
self.window().send_to_constellation(
|
||
ScriptToConstellationMessage::ChangeRunningAnimationsState(
|
||
AnimationState::NoAnimationCallbacksPresent,
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
pub(crate) fn policy_container(&self) -> Ref<'_, PolicyContainer> {
|
||
self.policy_container.borrow()
|
||
}
|
||
|
||
pub(crate) fn set_policy_container(&self, policy_container: PolicyContainer) {
|
||
*self.policy_container.borrow_mut() = policy_container;
|
||
}
|
||
|
||
pub(crate) fn set_csp_list(&self, csp_list: Option<CspList>) {
|
||
self.policy_container.borrow_mut().set_csp_list(csp_list);
|
||
}
|
||
|
||
pub(crate) fn get_csp_list(&self) -> Option<CspList> {
|
||
self.policy_container.borrow().csp_list.clone()
|
||
}
|
||
|
||
/// Add the policy container and HTTPS state to a given request.
|
||
///
|
||
/// TODO: Can this hapen for all requests that go through the document?
|
||
pub(crate) fn prepare_request(&self, request: RequestBuilder) -> RequestBuilder {
|
||
request
|
||
.policy_container(self.policy_container().to_owned())
|
||
.https_state(self.https_state.get())
|
||
}
|
||
|
||
pub(crate) fn fetch<Listener: FetchResponseListener + PreInvoke + Send + 'static>(
|
||
&self,
|
||
load: LoadType,
|
||
mut request: RequestBuilder,
|
||
listener: Listener,
|
||
) {
|
||
request = request
|
||
.insecure_requests_policy(self.insecure_requests_policy())
|
||
.has_trustworthy_ancestor_origin(self.has_trustworthy_ancestor_or_current_origin());
|
||
let callback = NetworkListener {
|
||
context: std::sync::Arc::new(Mutex::new(listener)),
|
||
task_source: self
|
||
.owner_global()
|
||
.task_manager()
|
||
.networking_task_source()
|
||
.into(),
|
||
}
|
||
.into_callback();
|
||
self.loader_mut()
|
||
.fetch_async_with_callback(load, request, callback);
|
||
}
|
||
|
||
pub(crate) fn fetch_background<Listener: FetchResponseListener + PreInvoke + Send + 'static>(
|
||
&self,
|
||
mut request: RequestBuilder,
|
||
listener: Listener,
|
||
) {
|
||
request = request
|
||
.insecure_requests_policy(self.insecure_requests_policy())
|
||
.has_trustworthy_ancestor_origin(self.has_trustworthy_ancestor_or_current_origin());
|
||
let callback = NetworkListener {
|
||
context: std::sync::Arc::new(Mutex::new(listener)),
|
||
task_source: self
|
||
.owner_global()
|
||
.task_manager()
|
||
.networking_task_source()
|
||
.into(),
|
||
}
|
||
.into_callback();
|
||
self.loader_mut().fetch_async_background(request, callback);
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#the-end
|
||
// https://html.spec.whatwg.org/multipage/#delay-the-load-event
|
||
pub(crate) fn finish_load(&self, load: LoadType, can_gc: CanGc) {
|
||
// This does not delay the load event anymore.
|
||
debug!("Document got finish_load: {:?}", load);
|
||
self.loader.borrow_mut().finish_load(&load);
|
||
|
||
match load {
|
||
LoadType::Stylesheet(_) => {
|
||
// A stylesheet finishing to load may unblock any pending
|
||
// parsing-blocking script or deferred script.
|
||
self.process_pending_parsing_blocking_script(can_gc);
|
||
|
||
// Step 3.
|
||
self.process_deferred_scripts(can_gc);
|
||
},
|
||
LoadType::PageSource(_) => {
|
||
// We finished loading the page, so if the `Window` is still waiting for
|
||
// the first layout, allow it.
|
||
if self.has_browsing_context && self.is_fully_active() {
|
||
self.window().allow_layout_if_necessary();
|
||
}
|
||
|
||
// Deferred scripts have to wait for page to finish loading,
|
||
// this is the first opportunity to process them.
|
||
|
||
// Step 3.
|
||
self.process_deferred_scripts(can_gc);
|
||
},
|
||
_ => {},
|
||
}
|
||
|
||
// Step 4 is in another castle, namely at the end of
|
||
// process_deferred_scripts.
|
||
|
||
// Step 5 can be found in asap_script_loaded and
|
||
// asap_in_order_script_loaded.
|
||
|
||
let loader = self.loader.borrow();
|
||
|
||
// Servo measures when the top-level content (not iframes) is loaded.
|
||
if self.top_level_dom_complete.get().is_none() && loader.is_only_blocked_by_iframes() {
|
||
update_with_current_instant(&self.top_level_dom_complete);
|
||
}
|
||
|
||
if loader.is_blocked() || loader.events_inhibited() {
|
||
// Step 6.
|
||
return;
|
||
}
|
||
|
||
ScriptThread::mark_document_with_no_blocked_loads(self);
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#prompt-to-unload-a-document
|
||
pub(crate) fn prompt_to_unload(&self, recursive_flag: bool, can_gc: CanGc) -> bool {
|
||
// TODO: Step 1, increase the event loop's termination nesting level by 1.
|
||
// Step 2
|
||
self.incr_ignore_opens_during_unload_counter();
|
||
// Step 3-5.
|
||
let beforeunload_event = BeforeUnloadEvent::new(
|
||
&self.window,
|
||
atom!("beforeunload"),
|
||
EventBubbles::Bubbles,
|
||
EventCancelable::Cancelable,
|
||
can_gc,
|
||
);
|
||
let event = beforeunload_event.upcast::<Event>();
|
||
event.set_trusted(true);
|
||
let event_target = self.window.upcast::<EventTarget>();
|
||
let has_listeners = event_target.has_listeners_for(&atom!("beforeunload"));
|
||
self.window
|
||
.dispatch_event_with_target_override(event, can_gc);
|
||
// TODO: Step 6, decrease the event loop's termination nesting level by 1.
|
||
// Step 7
|
||
if has_listeners {
|
||
self.salvageable.set(false);
|
||
}
|
||
let mut can_unload = true;
|
||
// TODO: Step 8, also check sandboxing modals flag.
|
||
let default_prevented = event.DefaultPrevented();
|
||
let return_value_not_empty = !event
|
||
.downcast::<BeforeUnloadEvent>()
|
||
.unwrap()
|
||
.ReturnValue()
|
||
.is_empty();
|
||
if default_prevented || return_value_not_empty {
|
||
let (chan, port) = generic_channel::channel().expect("Failed to create IPC channel!");
|
||
let msg = EmbedderMsg::AllowUnload(self.webview_id(), chan);
|
||
self.send_to_embedder(msg);
|
||
can_unload = port.recv().unwrap() == AllowOrDeny::Allow;
|
||
}
|
||
// Step 9
|
||
if !recursive_flag {
|
||
// `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);
|
||
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"));
|
||
self.window
|
||
.dispatch_event_with_target_override(&event, can_gc);
|
||
self.fired_unload.set(true);
|
||
// Step 9
|
||
if has_listeners {
|
||
self.salvageable.set(false);
|
||
}
|
||
}
|
||
// TODO: Step 8, decrease the event loop's termination nesting level by 1.
|
||
|
||
// Step 13
|
||
if !recursive_flag {
|
||
// `unload` might cause futher modifications to the DOM so collecting here prevents
|
||
// a double borrow if the `IFrameCollection` needs to be validated again.
|
||
let iframes: Vec<_> = self.iframes().iter().collect();
|
||
for iframe in &iframes {
|
||
// TODO: handle the case of cross origin iframes.
|
||
let document = iframe.owner_document();
|
||
document.unload(true, can_gc);
|
||
if !document.salvageable() {
|
||
self.salvageable.set(false);
|
||
}
|
||
}
|
||
}
|
||
|
||
let global_scope = self.window.as_global_scope();
|
||
// Step 10, 14
|
||
// https://html.spec.whatwg.org/multipage/#unloading-document-cleanup-steps
|
||
if !self.salvageable.get() {
|
||
// Step 1 of clean-up steps.
|
||
global_scope.close_event_sources();
|
||
let msg = ScriptToConstellationMessage::DiscardDocument;
|
||
let _ = global_scope.script_to_constellation_chan().send(msg);
|
||
}
|
||
// https://w3c.github.io/FileAPI/#lifeTime
|
||
global_scope.clean_up_all_file_resources();
|
||
|
||
// Step 15, End
|
||
self.decr_ignore_opens_during_unload_counter();
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#the-end
|
||
pub(crate) fn maybe_queue_document_completion(&self) {
|
||
// https://html.spec.whatwg.org/multipage/#delaying-load-events-mode
|
||
let is_in_delaying_load_events_mode = match self.window.undiscarded_window_proxy() {
|
||
Some(window_proxy) => window_proxy.is_delaying_load_events_mode(),
|
||
None => false,
|
||
};
|
||
|
||
// Note: if the document is not fully active, layout will have exited already,
|
||
// and this method will panic.
|
||
// The underlying problem might actually be that layout exits while it should be kept alive.
|
||
// See https://github.com/servo/servo/issues/22507
|
||
let not_ready_for_load = self.loader.borrow().is_blocked() ||
|
||
!self.is_fully_active() ||
|
||
is_in_delaying_load_events_mode;
|
||
|
||
if not_ready_for_load {
|
||
// Step 6.
|
||
return;
|
||
}
|
||
|
||
assert!(!self.loader.borrow().events_inhibited());
|
||
self.loader.borrow_mut().inhibit_events();
|
||
|
||
// The rest will ever run only once per document.
|
||
// Step 7.
|
||
debug!("Document loads are complete.");
|
||
let document = Trusted::new(self);
|
||
self.owner_global()
|
||
.task_manager()
|
||
.dom_manipulation_task_source()
|
||
.queue(task!(fire_load_event: move || {
|
||
let document = document.root();
|
||
let window = document.window();
|
||
if !window.is_alive() {
|
||
return;
|
||
}
|
||
|
||
// Step 7.1.
|
||
document.set_ready_state(DocumentReadyState::Complete, CanGc::note());
|
||
|
||
// Step 7.2.
|
||
if document.browsing_context().is_none() {
|
||
return;
|
||
}
|
||
let event = Event::new(
|
||
window.upcast(),
|
||
atom!("load"),
|
||
EventBubbles::DoesNotBubble,
|
||
EventCancelable::NotCancelable,
|
||
CanGc::note(),
|
||
);
|
||
event.set_trusted(true);
|
||
|
||
// http://w3c.github.io/navigation-timing/#widl-PerformanceNavigationTiming-loadEventStart
|
||
update_with_current_instant(&document.load_event_start);
|
||
|
||
debug!("About to dispatch load for {:?}", document.url());
|
||
window.dispatch_event_with_target_override(&event, CanGc::note());
|
||
|
||
// http://w3c.github.io/navigation-timing/#widl-PerformanceNavigationTiming-loadEventEnd
|
||
update_with_current_instant(&document.load_event_end);
|
||
|
||
if let Some(fragment) = document.url().fragment() {
|
||
document.check_and_scroll_fragment(fragment);
|
||
}
|
||
}));
|
||
|
||
// Step 8.
|
||
let document = Trusted::new(self);
|
||
if document.root().browsing_context().is_some() {
|
||
self.owner_global()
|
||
.task_manager()
|
||
.dom_manipulation_task_source()
|
||
.queue(task!(fire_pageshow_event: move || {
|
||
let document = document.root();
|
||
let window = document.window();
|
||
if document.page_showing.get() || !window.is_alive() {
|
||
return;
|
||
}
|
||
|
||
document.page_showing.set(true);
|
||
|
||
let event = PageTransitionEvent::new(
|
||
window,
|
||
atom!("pageshow"),
|
||
false, // bubbles
|
||
false, // cancelable
|
||
false, // persisted
|
||
CanGc::note(),
|
||
);
|
||
let event = event.upcast::<Event>();
|
||
event.set_trusted(true);
|
||
|
||
window.dispatch_event_with_target_override(event, CanGc::note());
|
||
}));
|
||
}
|
||
|
||
// Step 9.
|
||
// TODO: pending application cache download process tasks.
|
||
|
||
// Step 10.
|
||
// TODO: printing steps.
|
||
|
||
// Step 11.
|
||
// TODO: ready for post-load tasks.
|
||
|
||
// The dom.webxr.sessionavailable pref allows webxr
|
||
// content to immediately begin a session without waiting for a user gesture.
|
||
// TODO: should this only happen on the first document loaded?
|
||
// https://immersive-web.github.io/webxr/#user-intention
|
||
// https://github.com/immersive-web/navigation/issues/10
|
||
#[cfg(feature = "webxr")]
|
||
if pref!(dom_webxr_sessionavailable) && self.window.is_top_level() {
|
||
self.window.Navigator().Xr().dispatch_sessionavailable();
|
||
}
|
||
|
||
// Step 12: completely loaded.
|
||
// https://html.spec.whatwg.org/multipage/#completely-loaded
|
||
// TODO: fully implement "completely loaded".
|
||
let document = Trusted::new(self);
|
||
if document.root().browsing_context().is_some() {
|
||
self.owner_global()
|
||
.task_manager()
|
||
.dom_manipulation_task_source()
|
||
.queue(task!(completely_loaded: move || {
|
||
let document = document.root();
|
||
document.completely_loaded.set(true);
|
||
if let Some(DeclarativeRefresh::PendingLoad {
|
||
url,
|
||
time
|
||
}) = &*document.declarative_refresh.borrow() {
|
||
// https://html.spec.whatwg.org/multipage/#shared-declarative-refresh-steps
|
||
document.window.as_global_scope().schedule_callback(
|
||
OneshotTimerCallback::RefreshRedirectDue(RefreshRedirectDue {
|
||
window: DomRoot::from_ref(document.window()),
|
||
url: url.clone(),
|
||
}),
|
||
Duration::from_secs(*time),
|
||
);
|
||
}
|
||
// Note: this will, among others, result in the "iframe-load-event-steps" being run.
|
||
// https://html.spec.whatwg.org/multipage/#iframe-load-event-steps
|
||
document.notify_constellation_load();
|
||
}));
|
||
}
|
||
}
|
||
|
||
pub(crate) fn completely_loaded(&self) -> bool {
|
||
self.completely_loaded.get()
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#pending-parsing-blocking-script
|
||
pub(crate) fn set_pending_parsing_blocking_script(
|
||
&self,
|
||
script: &HTMLScriptElement,
|
||
load: Option<ScriptResult>,
|
||
) {
|
||
assert!(!self.has_pending_parsing_blocking_script());
|
||
*self.pending_parsing_blocking_script.borrow_mut() =
|
||
Some(PendingScript::new_with_load(script, load));
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#pending-parsing-blocking-script
|
||
pub(crate) fn has_pending_parsing_blocking_script(&self) -> bool {
|
||
self.pending_parsing_blocking_script.borrow().is_some()
|
||
}
|
||
|
||
/// <https://html.spec.whatwg.org/multipage/#prepare-a-script> step 22.d.
|
||
pub(crate) fn pending_parsing_blocking_script_loaded(
|
||
&self,
|
||
element: &HTMLScriptElement,
|
||
result: ScriptResult,
|
||
can_gc: CanGc,
|
||
) {
|
||
{
|
||
let mut blocking_script = self.pending_parsing_blocking_script.borrow_mut();
|
||
let entry = blocking_script.as_mut().unwrap();
|
||
assert!(&*entry.element == element);
|
||
entry.loaded(result);
|
||
}
|
||
self.process_pending_parsing_blocking_script(can_gc);
|
||
}
|
||
|
||
fn process_pending_parsing_blocking_script(&self, can_gc: CanGc) {
|
||
if self.script_blocking_stylesheets_count.get() > 0 {
|
||
return;
|
||
}
|
||
let pair = self
|
||
.pending_parsing_blocking_script
|
||
.borrow_mut()
|
||
.as_mut()
|
||
.and_then(PendingScript::take_result);
|
||
if let Some((element, result)) = pair {
|
||
*self.pending_parsing_blocking_script.borrow_mut() = None;
|
||
self.get_current_parser()
|
||
.unwrap()
|
||
.resume_with_pending_parsing_blocking_script(&element, result, can_gc);
|
||
}
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#set-of-scripts-that-will-execute-as-soon-as-possible
|
||
pub(crate) fn add_asap_script(&self, script: &HTMLScriptElement) {
|
||
self.asap_scripts_set
|
||
.borrow_mut()
|
||
.push(Dom::from_ref(script));
|
||
}
|
||
|
||
/// <https://html.spec.whatwg.org/multipage/#the-end> step 5.
|
||
/// <https://html.spec.whatwg.org/multipage/#prepare-a-script> step 22.d.
|
||
pub(crate) fn asap_script_loaded(
|
||
&self,
|
||
element: &HTMLScriptElement,
|
||
result: ScriptResult,
|
||
can_gc: CanGc,
|
||
) {
|
||
{
|
||
let mut scripts = self.asap_scripts_set.borrow_mut();
|
||
let idx = scripts
|
||
.iter()
|
||
.position(|entry| &**entry == element)
|
||
.unwrap();
|
||
scripts.swap_remove(idx);
|
||
}
|
||
element.execute(result, can_gc);
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#list-of-scripts-that-will-execute-in-order-as-soon-as-possible
|
||
pub(crate) fn push_asap_in_order_script(&self, script: &HTMLScriptElement) {
|
||
self.asap_in_order_scripts_list.push(script);
|
||
}
|
||
|
||
/// <https://html.spec.whatwg.org/multipage/#the-end> step 5.
|
||
/// <https://html.spec.whatwg.org/multipage/#prepare-a-script> step> 22.c.
|
||
pub(crate) fn asap_in_order_script_loaded(
|
||
&self,
|
||
element: &HTMLScriptElement,
|
||
result: ScriptResult,
|
||
can_gc: CanGc,
|
||
) {
|
||
self.asap_in_order_scripts_list.loaded(element, result);
|
||
while let Some((element, result)) = self
|
||
.asap_in_order_scripts_list
|
||
.take_next_ready_to_be_executed()
|
||
{
|
||
element.execute(result, can_gc);
|
||
}
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#list-of-scripts-that-will-execute-when-the-document-has-finished-parsing
|
||
pub(crate) fn add_deferred_script(&self, script: &HTMLScriptElement) {
|
||
self.deferred_scripts.push(script);
|
||
}
|
||
|
||
/// <https://html.spec.whatwg.org/multipage/#the-end> step 3.
|
||
/// <https://html.spec.whatwg.org/multipage/#prepare-a-script> step 22.d.
|
||
pub(crate) fn deferred_script_loaded(
|
||
&self,
|
||
element: &HTMLScriptElement,
|
||
result: ScriptResult,
|
||
can_gc: CanGc,
|
||
) {
|
||
self.deferred_scripts.loaded(element, result);
|
||
self.process_deferred_scripts(can_gc);
|
||
}
|
||
|
||
/// <https://html.spec.whatwg.org/multipage/#the-end> step 3.
|
||
fn process_deferred_scripts(&self, can_gc: CanGc) {
|
||
if self.ready_state.get() != DocumentReadyState::Interactive {
|
||
return;
|
||
}
|
||
// Part of substep 1.
|
||
loop {
|
||
if self.script_blocking_stylesheets_count.get() > 0 {
|
||
return;
|
||
}
|
||
if let Some((element, result)) = self.deferred_scripts.take_next_ready_to_be_executed()
|
||
{
|
||
element.execute(result, can_gc);
|
||
} else {
|
||
break;
|
||
}
|
||
}
|
||
if self.deferred_scripts.is_empty() {
|
||
// https://html.spec.whatwg.org/multipage/#the-end step 4.
|
||
self.maybe_dispatch_dom_content_loaded();
|
||
}
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#the-end step 4.
|
||
pub(crate) fn maybe_dispatch_dom_content_loaded(&self) {
|
||
if self.domcontentloaded_dispatched.get() {
|
||
return;
|
||
}
|
||
self.domcontentloaded_dispatched.set(true);
|
||
assert_ne!(
|
||
self.ReadyState(),
|
||
DocumentReadyState::Complete,
|
||
"Complete before DOMContentLoaded?"
|
||
);
|
||
|
||
update_with_current_instant(&self.dom_content_loaded_event_start);
|
||
|
||
// Step 4.1.
|
||
let document = Trusted::new(self);
|
||
self.owner_global()
|
||
.task_manager()
|
||
.dom_manipulation_task_source()
|
||
.queue(
|
||
task!(fire_dom_content_loaded_event: move || {
|
||
let document = document.root();
|
||
document.upcast::<EventTarget>().fire_bubbling_event(atom!("DOMContentLoaded"), CanGc::note());
|
||
update_with_current_instant(&document.dom_content_loaded_event_end);
|
||
})
|
||
);
|
||
|
||
// html parsing has finished - set dom content loaded
|
||
self.interactive_time
|
||
.borrow()
|
||
.maybe_set_tti(InteractiveFlag::DOMContentLoaded);
|
||
|
||
// Step 4.2.
|
||
// TODO: client message queue.
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#abort-a-document
|
||
pub(crate) fn abort(&self, can_gc: CanGc) {
|
||
// We need to inhibit the loader before anything else.
|
||
self.loader.borrow_mut().inhibit_events();
|
||
|
||
// Step 1.
|
||
for iframe in self.iframes().iter() {
|
||
if let Some(document) = iframe.GetContentDocument() {
|
||
// TODO: abort the active documents of every child browsing context.
|
||
document.abort(can_gc);
|
||
// TODO: salvageable flag.
|
||
}
|
||
}
|
||
|
||
// Step 2.
|
||
self.script_blocking_stylesheets_count.set(0);
|
||
*self.pending_parsing_blocking_script.borrow_mut() = None;
|
||
*self.asap_scripts_set.borrow_mut() = vec![];
|
||
self.asap_in_order_scripts_list.clear();
|
||
self.deferred_scripts.clear();
|
||
let loads_cancelled = self.loader.borrow_mut().cancel_all_loads();
|
||
let event_sources_canceled = self.window.as_global_scope().close_event_sources();
|
||
if loads_cancelled || event_sources_canceled {
|
||
// If any loads were canceled.
|
||
self.salvageable.set(false);
|
||
};
|
||
|
||
// Also Step 2.
|
||
// Note: the spec says to discard any tasks queued for fetch.
|
||
// This cancels all tasks on the networking task source, which might be too broad.
|
||
// See https://github.com/whatwg/html/issues/3837
|
||
self.owner_global()
|
||
.task_manager()
|
||
.cancel_pending_tasks_for_source(TaskSourceName::Networking);
|
||
|
||
// Step 3.
|
||
if let Some(parser) = self.get_current_parser() {
|
||
self.active_parser_was_aborted.set(true);
|
||
parser.abort(can_gc);
|
||
self.salvageable.set(false);
|
||
}
|
||
}
|
||
|
||
pub(crate) fn notify_constellation_load(&self) {
|
||
self.window()
|
||
.send_to_constellation(ScriptToConstellationMessage::LoadComplete);
|
||
}
|
||
|
||
pub(crate) fn set_current_parser(&self, script: Option<&ServoParser>) {
|
||
self.current_parser.set(script);
|
||
}
|
||
|
||
pub(crate) fn get_current_parser(&self) -> Option<DomRoot<ServoParser>> {
|
||
self.current_parser.get()
|
||
}
|
||
|
||
/// A reference to the [`IFrameCollection`] of this [`Document`], holding information about
|
||
/// `<iframe>`s found within it.
|
||
pub(crate) fn iframes(&self) -> Ref<'_, IFrameCollection> {
|
||
self.iframes.borrow_mut().validate(self);
|
||
self.iframes.borrow()
|
||
}
|
||
|
||
/// A mutable reference to the [`IFrameCollection`] of this [`Document`], holding information about
|
||
/// `<iframe>`s found within it.
|
||
pub(crate) fn iframes_mut(&self) -> RefMut<'_, IFrameCollection> {
|
||
self.iframes.borrow_mut().validate(self);
|
||
self.iframes.borrow_mut()
|
||
}
|
||
|
||
pub(crate) fn invalidate_iframes_collection(&self) {
|
||
self.iframes.borrow_mut().invalidate();
|
||
}
|
||
|
||
pub(crate) fn get_dom_interactive(&self) -> Option<CrossProcessInstant> {
|
||
self.dom_interactive.get()
|
||
}
|
||
|
||
pub(crate) fn set_navigation_start(&self, navigation_start: CrossProcessInstant) {
|
||
self.interactive_time
|
||
.borrow_mut()
|
||
.set_navigation_start(navigation_start);
|
||
}
|
||
|
||
pub(crate) fn get_interactive_metrics(&self) -> Ref<'_, ProgressiveWebMetrics> {
|
||
self.interactive_time.borrow()
|
||
}
|
||
|
||
pub(crate) fn has_recorded_tti_metric(&self) -> bool {
|
||
self.get_interactive_metrics().get_tti().is_some()
|
||
}
|
||
|
||
pub(crate) fn get_dom_content_loaded_event_start(&self) -> Option<CrossProcessInstant> {
|
||
self.dom_content_loaded_event_start.get()
|
||
}
|
||
|
||
pub(crate) fn get_dom_content_loaded_event_end(&self) -> Option<CrossProcessInstant> {
|
||
self.dom_content_loaded_event_end.get()
|
||
}
|
||
|
||
pub(crate) fn get_dom_complete(&self) -> Option<CrossProcessInstant> {
|
||
self.dom_complete.get()
|
||
}
|
||
|
||
pub(crate) fn get_top_level_dom_complete(&self) -> Option<CrossProcessInstant> {
|
||
self.top_level_dom_complete.get()
|
||
}
|
||
|
||
pub(crate) fn get_load_event_start(&self) -> Option<CrossProcessInstant> {
|
||
self.load_event_start.get()
|
||
}
|
||
|
||
pub(crate) fn get_load_event_end(&self) -> Option<CrossProcessInstant> {
|
||
self.load_event_end.get()
|
||
}
|
||
|
||
pub(crate) fn get_unload_event_start(&self) -> Option<CrossProcessInstant> {
|
||
self.unload_event_start.get()
|
||
}
|
||
|
||
pub(crate) fn get_unload_event_end(&self) -> Option<CrossProcessInstant> {
|
||
self.unload_event_end.get()
|
||
}
|
||
|
||
pub(crate) fn start_tti(&self) {
|
||
if self.get_interactive_metrics().needs_tti() {
|
||
self.tti_window.borrow_mut().start_window();
|
||
}
|
||
}
|
||
|
||
/// check tti for this document
|
||
/// if it's been 10s since this doc encountered a task over 50ms, then we consider the
|
||
/// main thread available and try to set tti
|
||
pub(crate) fn record_tti_if_necessary(&self) {
|
||
if self.has_recorded_tti_metric() {
|
||
return;
|
||
}
|
||
if self.tti_window.borrow().needs_check() {
|
||
self.get_interactive_metrics()
|
||
.maybe_set_tti(InteractiveFlag::TimeToInteractive(
|
||
self.tti_window.borrow().get_start(),
|
||
));
|
||
}
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#fire-a-focus-event
|
||
fn fire_focus_event(
|
||
&self,
|
||
focus_event_type: FocusEventType,
|
||
event_target: &EventTarget,
|
||
related_target: Option<&EventTarget>,
|
||
can_gc: CanGc,
|
||
) {
|
||
let (event_name, does_bubble) = match focus_event_type {
|
||
FocusEventType::Focus => (DOMString::from("focus"), EventBubbles::DoesNotBubble),
|
||
FocusEventType::Blur => (DOMString::from("blur"), EventBubbles::DoesNotBubble),
|
||
};
|
||
let event = FocusEvent::new(
|
||
&self.window,
|
||
event_name,
|
||
does_bubble,
|
||
EventCancelable::NotCancelable,
|
||
Some(&self.window),
|
||
0i32,
|
||
related_target,
|
||
can_gc,
|
||
);
|
||
let event = event.upcast::<Event>();
|
||
event.set_trusted(true);
|
||
event.fire(event_target, can_gc);
|
||
}
|
||
|
||
/// <https://html.spec.whatwg.org/multipage/#cookie-averse-document-object>
|
||
pub(crate) fn is_cookie_averse(&self) -> bool {
|
||
!self.has_browsing_context || !url_has_network_scheme(&self.url())
|
||
}
|
||
|
||
/// <https://html.spec.whatwg.org/multipage/#look-up-a-custom-element-definition>
|
||
pub(crate) fn lookup_custom_element_definition(
|
||
&self,
|
||
namespace: &Namespace,
|
||
local_name: &LocalName,
|
||
is: Option<&LocalName>,
|
||
) -> Option<Rc<CustomElementDefinition>> {
|
||
if !pref!(dom_customelements_enabled) {
|
||
return None;
|
||
}
|
||
|
||
// Step 1
|
||
if *namespace != ns!(html) {
|
||
return None;
|
||
}
|
||
|
||
// Step 2
|
||
if !self.has_browsing_context {
|
||
return None;
|
||
}
|
||
|
||
// Step 3
|
||
let registry = self.window.CustomElements();
|
||
|
||
registry.lookup_definition(local_name, is)
|
||
}
|
||
|
||
pub(crate) fn increment_throw_on_dynamic_markup_insertion_counter(&self) {
|
||
let counter = self.throw_on_dynamic_markup_insertion_counter.get();
|
||
self.throw_on_dynamic_markup_insertion_counter
|
||
.set(counter + 1);
|
||
}
|
||
|
||
pub(crate) fn decrement_throw_on_dynamic_markup_insertion_counter(&self) {
|
||
let counter = self.throw_on_dynamic_markup_insertion_counter.get();
|
||
self.throw_on_dynamic_markup_insertion_counter
|
||
.set(counter - 1);
|
||
}
|
||
|
||
pub(crate) fn react_to_environment_changes(&self) {
|
||
for image in self.responsive_images.borrow().iter() {
|
||
image.react_to_environment_changes();
|
||
}
|
||
}
|
||
|
||
pub(crate) fn register_responsive_image(&self, img: &HTMLImageElement) {
|
||
self.responsive_images.borrow_mut().push(Dom::from_ref(img));
|
||
}
|
||
|
||
pub(crate) fn unregister_responsive_image(&self, img: &HTMLImageElement) {
|
||
let index = self
|
||
.responsive_images
|
||
.borrow()
|
||
.iter()
|
||
.position(|x| **x == *img);
|
||
if let Some(i) = index {
|
||
self.responsive_images.borrow_mut().remove(i);
|
||
}
|
||
}
|
||
|
||
pub(crate) fn register_media_controls(&self, controls: &ShadowRoot) -> String {
|
||
let id = Uuid::new_v4().to_string();
|
||
self.media_controls
|
||
.borrow_mut()
|
||
.insert(id.clone(), Dom::from_ref(controls));
|
||
id
|
||
}
|
||
|
||
pub(crate) fn unregister_media_controls(&self, id: &str) {
|
||
let did_have_these_media_controls = self.media_controls.borrow_mut().remove(id).is_some();
|
||
debug_assert!(
|
||
did_have_these_media_controls,
|
||
"Trying to unregister unknown media controls"
|
||
);
|
||
}
|
||
|
||
pub(crate) fn 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 add_dirty_2d_canvas(&self, context: &CanvasRenderingContext2D) {
|
||
self.dirty_2d_contexts
|
||
.borrow_mut()
|
||
.entry(context.context_id())
|
||
.or_insert_with(|| Dom::from_ref(context));
|
||
}
|
||
|
||
#[cfg(feature = "webgpu")]
|
||
pub(crate) fn add_dirty_webgpu_context(&self, context: &GPUCanvasContext) {
|
||
self.dirty_webgpu_contexts
|
||
.borrow_mut()
|
||
.entry(context.context_id())
|
||
.or_insert_with(|| Dom::from_ref(context));
|
||
}
|
||
|
||
/// Whether or not this [`Document`] needs a rendering update, due to changed
|
||
/// contents or pending events. This is used to decide whether or not to schedule
|
||
/// a call to the "update the rendering" algorithm.
|
||
pub(crate) fn needs_rendering_update(&self) -> bool {
|
||
if !self.is_fully_active() {
|
||
return false;
|
||
}
|
||
if !self.window().layout_blocked() &&
|
||
(!self.restyle_reason().is_empty() ||
|
||
self.window().layout().needs_new_display_list())
|
||
{
|
||
return true;
|
||
}
|
||
if self.resize_observer_started_observing_target.get() {
|
||
return true;
|
||
}
|
||
if self.event_handler.has_pending_input_events() {
|
||
return true;
|
||
}
|
||
if self.has_pending_scroll_events() {
|
||
return true;
|
||
}
|
||
if self.window().has_unhandled_resize_event() {
|
||
return true;
|
||
}
|
||
if self.has_pending_animated_image_update.get() {
|
||
return true;
|
||
}
|
||
|
||
false
|
||
}
|
||
|
||
/// An implementation of step 22 from
|
||
/// <https://html.spec.whatwg.org/multipage/#update-the-rendering>:
|
||
///
|
||
// > Step 22: For each doc of docs, update the rendering or user interface of
|
||
// > doc and its node navigable to reflect the current state.
|
||
//
|
||
// Returns the set of reflow phases run as a [`ReflowPhasesRun`].
|
||
pub(crate) fn update_the_rendering(&self) -> ReflowPhasesRun {
|
||
if self.render_blocking_element_count() > 0 {
|
||
return Default::default();
|
||
}
|
||
|
||
if self.has_pending_animated_image_update.get() {
|
||
self.image_animation_manager
|
||
.borrow()
|
||
.update_active_frames(&self.window, self.current_animation_timeline_value());
|
||
self.has_pending_animated_image_update.set(false);
|
||
}
|
||
|
||
// All dirty canvases are flushed before updating the rendering.
|
||
self.current_canvas_epoch.borrow_mut().next();
|
||
let canvas_epoch = *self.current_canvas_epoch.borrow();
|
||
let mut image_keys = Vec::new();
|
||
|
||
#[cfg(feature = "webgpu")]
|
||
image_keys.extend(
|
||
self.dirty_webgpu_contexts
|
||
.borrow_mut()
|
||
.drain()
|
||
.filter(|(_, context)| context.update_rendering(canvas_epoch))
|
||
.map(|(_, context)| context.image_key()),
|
||
);
|
||
|
||
image_keys.extend(
|
||
self.dirty_2d_contexts
|
||
.borrow_mut()
|
||
.drain()
|
||
.filter(|(_, context)| context.update_rendering(canvas_epoch))
|
||
.map(|(_, context)| context.image_key()),
|
||
);
|
||
|
||
let dirty_webgl_context_ids: Vec<_> = self
|
||
.dirty_webgl_contexts
|
||
.borrow_mut()
|
||
.drain()
|
||
.filter(|(_, context)| context.onscreen())
|
||
.map(|(id, context)| {
|
||
image_keys.push(context.image_key());
|
||
id
|
||
})
|
||
.collect();
|
||
|
||
if !dirty_webgl_context_ids.is_empty() {
|
||
self.window
|
||
.webgl_chan()
|
||
.expect("Where's the WebGL channel?")
|
||
.send(WebGLMsg::SwapBuffers(
|
||
dirty_webgl_context_ids,
|
||
Some(canvas_epoch),
|
||
0,
|
||
))
|
||
.unwrap();
|
||
}
|
||
|
||
// The renderer should wait to display the frame until all canvas images are
|
||
// uploaded. This allows canvas image uploading to happen asynchronously.
|
||
if !image_keys.is_empty() {
|
||
self.waiting_on_canvas_image_updates.set(true);
|
||
self.window().compositor_api().delay_new_frame_for_canvas(
|
||
self.window().pipeline_id(),
|
||
canvas_epoch,
|
||
image_keys.into_iter().flatten().collect(),
|
||
);
|
||
}
|
||
|
||
self.window().reflow(ReflowGoal::UpdateTheRendering)
|
||
}
|
||
|
||
pub(crate) fn handle_no_longer_waiting_on_asynchronous_image_updates(&self) {
|
||
self.waiting_on_canvas_image_updates.set(false);
|
||
}
|
||
|
||
pub(crate) fn waiting_on_canvas_image_updates(&self) -> bool {
|
||
self.waiting_on_canvas_image_updates.get()
|
||
}
|
||
|
||
/// From <https://drafts.csswg.org/css-font-loading/#fontfaceset-pending-on-the-environment>:
|
||
///
|
||
/// > A FontFaceSet is pending on the environment if any of the following are true:
|
||
/// > - the document is still loading
|
||
/// > - the document has pending stylesheet requests
|
||
/// > - the document has pending layout operations which might cause the user agent to request
|
||
/// > a font, or which depend on recently-loaded fonts
|
||
///
|
||
/// Returns true if the promise was fulfilled.
|
||
pub(crate) fn maybe_fulfill_font_ready_promise(&self, can_gc: CanGc) -> bool {
|
||
if !self.is_fully_active() {
|
||
return false;
|
||
}
|
||
|
||
let fonts = self.Fonts(can_gc);
|
||
if !fonts.waiting_to_fullfill_promise() {
|
||
return false;
|
||
}
|
||
if self.window().font_context().web_fonts_still_loading() != 0 {
|
||
return false;
|
||
}
|
||
if self.ReadyState() != DocumentReadyState::Complete {
|
||
return false;
|
||
}
|
||
if !self.restyle_reason().is_empty() {
|
||
return false;
|
||
}
|
||
fonts.fulfill_ready_promise_if_needed(can_gc)
|
||
}
|
||
|
||
pub(crate) fn id_map(
|
||
&self,
|
||
) -> Ref<'_, HashMapTracedValues<Atom, Vec<Dom<Element>>, FxBuildHasher>> {
|
||
self.id_map.borrow()
|
||
}
|
||
|
||
pub(crate) fn name_map(
|
||
&self,
|
||
) -> Ref<'_, HashMapTracedValues<Atom, Vec<Dom<Element>>, FxBuildHasher>> {
|
||
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));
|
||
}
|
||
|
||
/// Whether or not this [`Document`] has any active [`ResizeObserver`].
|
||
pub(crate) fn has_resize_observers(&self) -> bool {
|
||
!self.resize_observers.borrow().is_empty()
|
||
}
|
||
|
||
/// <https://drafts.csswg.org/resize-observer/#gather-active-observations-h>
|
||
/// <https://drafts.csswg.org/resize-observer/#has-active-resize-observations>
|
||
pub(crate) fn gather_active_resize_observations_at_depth(
|
||
&self,
|
||
depth: &ResizeObservationDepth,
|
||
) -> bool {
|
||
let mut has_active_resize_observations = false;
|
||
for observer in self.resize_observers.borrow_mut().iter_mut() {
|
||
observer.gather_active_resize_observations_at_depth(
|
||
depth,
|
||
&mut has_active_resize_observations,
|
||
);
|
||
}
|
||
has_active_resize_observations
|
||
}
|
||
|
||
/// <https://drafts.csswg.org/resize-observer/#broadcast-active-resize-observations>
|
||
pub(crate) fn broadcast_active_resize_observations(
|
||
&self,
|
||
can_gc: CanGc,
|
||
) -> ResizeObservationDepth {
|
||
let mut shallowest = ResizeObservationDepth::max();
|
||
// Breaking potential re-borrow cycle on `resize_observers`:
|
||
// broadcasting resize observations calls into a JS callback,
|
||
// which can add new observers.
|
||
let iterator: Vec<DomRoot<ResizeObserver>> = self
|
||
.resize_observers
|
||
.borrow()
|
||
.iter()
|
||
.cloned()
|
||
.map(|obs| DomRoot::from_ref(&*obs))
|
||
.collect();
|
||
for observer in iterator {
|
||
observer.broadcast_active_resize_observations(&mut shallowest, can_gc);
|
||
}
|
||
shallowest
|
||
}
|
||
|
||
/// <https://drafts.csswg.org/resize-observer/#has-skipped-observations-h>
|
||
pub(crate) fn has_skipped_resize_observations(&self) -> bool {
|
||
self.resize_observers
|
||
.borrow()
|
||
.iter()
|
||
.any(|observer| observer.has_skipped_resize_observations())
|
||
}
|
||
|
||
/// <https://drafts.csswg.org/resize-observer/#deliver-resize-loop-error-notification>
|
||
pub(crate) fn deliver_resize_loop_error_notification(&self, can_gc: CanGc) {
|
||
let error_info: ErrorInfo = crate::dom::bindings::error::ErrorInfo {
|
||
message: "ResizeObserver loop completed with undelivered notifications.".to_string(),
|
||
..Default::default()
|
||
};
|
||
self.window
|
||
.as_global_scope()
|
||
.report_an_error(error_info, HandleValue::null(), can_gc);
|
||
}
|
||
|
||
pub(crate) fn status_code(&self) -> Option<u16> {
|
||
self.status_code
|
||
}
|
||
|
||
/// <https://html.spec.whatwg.org/multipage/#encoding-parsing-a-url>
|
||
pub(crate) fn encoding_parse_a_url(&self, url: &str) -> Result<ServoUrl, url::ParseError> {
|
||
// NOTE: This algorithm is defined for both Document and environment settings objects.
|
||
// This implementation is only for documents.
|
||
|
||
// Step 1. Let encoding be UTF-8.
|
||
// Step 2. If environment is a Document object, then set encoding to environment's character encoding.
|
||
let encoding = self.encoding.get();
|
||
|
||
// Step 3. Otherwise, if environment's relevant global object is a Window object, set encoding to environment's
|
||
// relevant global object's associated Document's character encoding.
|
||
|
||
// Step 4. Let baseURL be environment's base URL, if environment is a Document object;
|
||
// otherwise environment's API base URL.
|
||
let base_url = self.base_url();
|
||
|
||
// Step 5. Return the result of applying the URL parser to url, with baseURL and encoding.
|
||
url::Url::options()
|
||
.base_url(Some(base_url.as_url()))
|
||
.encoding_override(Some(&|input| {
|
||
servo_url::encoding::encode_as_url_query_string(input, encoding)
|
||
}))
|
||
.parse(url)
|
||
.map(ServoUrl::from)
|
||
}
|
||
|
||
/// <https://html.spec.whatwg.org/multipage/#allowed-to-use>
|
||
pub(crate) fn allowed_to_use_feature(&self, _feature: PermissionName) -> bool {
|
||
// Step 1. If document's browsing context is null, then return false.
|
||
if !self.has_browsing_context {
|
||
return false;
|
||
}
|
||
|
||
// Step 2. If document is not fully active, then return false.
|
||
if !self.is_fully_active() {
|
||
return false;
|
||
}
|
||
|
||
// Step 3. If the result of running is feature enabled in document for origin on
|
||
// feature, document, and document's origin is "Enabled", then return true.
|
||
// Step 4. Return false.
|
||
// TODO: All features are currently enabled for `Document`s because we do not
|
||
// implement the Permissions Policy specification.
|
||
true
|
||
}
|
||
|
||
/// Add an [`IntersectionObserver`] to the [`Document`], to be processed in the [`Document`]'s event loop.
|
||
/// <https://github.com/w3c/IntersectionObserver/issues/525>
|
||
pub(crate) fn add_intersection_observer(&self, intersection_observer: &IntersectionObserver) {
|
||
self.intersection_observers
|
||
.borrow_mut()
|
||
.push(Dom::from_ref(intersection_observer));
|
||
}
|
||
|
||
/// Remove an [`IntersectionObserver`] from [`Document`], ommiting it from the event loop.
|
||
/// An observer without any target, ideally should be removed to be conformant with
|
||
/// <https://w3c.github.io/IntersectionObserver/#lifetime>.
|
||
pub(crate) fn remove_intersection_observer(
|
||
&self,
|
||
intersection_observer: &IntersectionObserver,
|
||
) {
|
||
self.intersection_observers
|
||
.borrow_mut()
|
||
.retain(|observer| *observer != intersection_observer)
|
||
}
|
||
|
||
/// <https://w3c.github.io/IntersectionObserver/#update-intersection-observations-algo>
|
||
pub(crate) fn update_intersection_observer_steps(
|
||
&self,
|
||
time: CrossProcessInstant,
|
||
can_gc: CanGc,
|
||
) {
|
||
// Step 1-2
|
||
for intersection_observer in &*self.intersection_observers.borrow() {
|
||
self.update_single_intersection_observer_steps(intersection_observer, time, can_gc);
|
||
}
|
||
}
|
||
|
||
/// Step 2.1-2.2 of <https://w3c.github.io/IntersectionObserver/#update-intersection-observations-algo>
|
||
fn update_single_intersection_observer_steps(
|
||
&self,
|
||
intersection_observer: &IntersectionObserver,
|
||
time: CrossProcessInstant,
|
||
can_gc: CanGc,
|
||
) {
|
||
// Step 1
|
||
// > Let rootBounds be observer’s root intersection rectangle.
|
||
let root_bounds = intersection_observer.root_intersection_rectangle(self);
|
||
|
||
// Step 2
|
||
// > For each target in observer’s internal [[ObservationTargets]] slot,
|
||
// > processed in the same order that observe() was called on each target:
|
||
intersection_observer.update_intersection_observations_steps(
|
||
self,
|
||
time,
|
||
root_bounds,
|
||
can_gc,
|
||
);
|
||
}
|
||
|
||
/// <https://w3c.github.io/IntersectionObserver/#notify-intersection-observers-algo>
|
||
pub(crate) fn notify_intersection_observers(&self, can_gc: CanGc) {
|
||
// Step 1
|
||
// > Set document’s IntersectionObserverTaskQueued flag to false.
|
||
self.intersection_observer_task_queued.set(false);
|
||
|
||
// Step 2
|
||
// > Let notify list be a list of all IntersectionObservers whose root is in the DOM tree of document.
|
||
// We will copy the observers because callback could modify the current list.
|
||
// It will rooted to prevent GC in the iteration.
|
||
rooted_vec!(let notify_list <- self.intersection_observers.clone().take().into_iter());
|
||
|
||
// Step 3
|
||
// > For each IntersectionObserver object observer in notify list, run these steps:
|
||
for intersection_observer in notify_list.iter() {
|
||
// Step 3.1-3.5
|
||
intersection_observer.invoke_callback_if_necessary(can_gc);
|
||
}
|
||
}
|
||
|
||
/// <https://w3c.github.io/IntersectionObserver/#queue-intersection-observer-task>
|
||
pub(crate) fn queue_an_intersection_observer_task(&self) {
|
||
// Step 1
|
||
// > If document’s IntersectionObserverTaskQueued flag is set to true, return.
|
||
if self.intersection_observer_task_queued.get() {
|
||
return;
|
||
}
|
||
|
||
// Step 2
|
||
// > Set document’s IntersectionObserverTaskQueued flag to true.
|
||
self.intersection_observer_task_queued.set(true);
|
||
|
||
// Step 3
|
||
// > Queue a task on the IntersectionObserver task source associated with
|
||
// > the document's event loop to notify intersection observers.
|
||
let document = Trusted::new(self);
|
||
self.owner_global()
|
||
.task_manager()
|
||
.intersection_observer_task_source()
|
||
.queue(task!(notify_intersection_observers: move || {
|
||
document.root().notify_intersection_observers(CanGc::note());
|
||
}));
|
||
}
|
||
|
||
pub(crate) fn handle_paint_metric(
|
||
&self,
|
||
metric_type: ProgressiveWebMetricType,
|
||
metric_value: CrossProcessInstant,
|
||
first_reflow: bool,
|
||
can_gc: CanGc,
|
||
) {
|
||
let metrics = self.interactive_time.borrow();
|
||
match metric_type {
|
||
ProgressiveWebMetricType::FirstPaint => {
|
||
metrics.set_first_paint(metric_value, first_reflow)
|
||
},
|
||
ProgressiveWebMetricType::FirstContentfulPaint => {
|
||
metrics.set_first_contentful_paint(metric_value, first_reflow)
|
||
},
|
||
ProgressiveWebMetricType::TimeToInteractive => {
|
||
unreachable!("Unexpected non-paint metric.")
|
||
},
|
||
}
|
||
|
||
let entry = PerformancePaintTiming::new(
|
||
self.window.as_global_scope(),
|
||
metric_type,
|
||
metric_value,
|
||
can_gc,
|
||
);
|
||
self.window
|
||
.Performance()
|
||
.queue_entry(entry.upcast::<PerformanceEntry>(), can_gc);
|
||
}
|
||
|
||
/// <https://html.spec.whatwg.org/multipage/#document-write-steps>
|
||
fn write(
|
||
&self,
|
||
text: Vec<TrustedHTMLOrString>,
|
||
line_feed: bool,
|
||
containing_class: &str,
|
||
field: &str,
|
||
can_gc: CanGc,
|
||
) -> ErrorResult {
|
||
// Step 1: Let string be the empty string.
|
||
let mut strings: Vec<String> = Vec::with_capacity(text.len());
|
||
// Step 2: Let isTrusted be false if text contains a string; otherwise true.
|
||
let mut is_trusted = true;
|
||
// Step 3: For each value of text:
|
||
for value in text {
|
||
match value {
|
||
// Step 3.1: If value is a TrustedHTML object, then append value's associated data to string.
|
||
TrustedHTMLOrString::TrustedHTML(trusted_html) => {
|
||
strings.push(trusted_html.to_string().to_owned());
|
||
},
|
||
TrustedHTMLOrString::String(str_) => {
|
||
// Step 2: Let isTrusted be false if text contains a string; otherwise true.
|
||
is_trusted = false;
|
||
// Step 3.2: Otherwise, append value to string.
|
||
strings.push(str_.into());
|
||
},
|
||
};
|
||
}
|
||
let mut string = itertools::join(strings, "");
|
||
// Step 4: If isTrusted is false, set string to the result of invoking the
|
||
// Get Trusted Type compliant string algorithm with TrustedHTML,
|
||
// this's relevant global object, string, sink, and "script".
|
||
if !is_trusted {
|
||
string = TrustedHTML::get_trusted_script_compliant_string(
|
||
&self.global(),
|
||
TrustedHTMLOrString::String(string.into()),
|
||
&format!("{} {}", containing_class, field),
|
||
can_gc,
|
||
)?
|
||
.str()
|
||
.to_owned();
|
||
}
|
||
// Step 5: If lineFeed is true, append U+000A LINE FEED to string.
|
||
if line_feed {
|
||
string.push('\n');
|
||
}
|
||
// Step 6: If document is an XML document, then throw an "InvalidStateError" DOMException.
|
||
if !self.is_html_document() {
|
||
return Err(Error::InvalidState);
|
||
}
|
||
|
||
// Step 7: If document's throw-on-dynamic-markup-insertion counter is greater than 0,
|
||
// then throw an "InvalidStateError" DOMException.
|
||
if self.throw_on_dynamic_markup_insertion_counter.get() > 0 {
|
||
return Err(Error::InvalidState);
|
||
}
|
||
|
||
// Step 8: If document's active parser was aborted is true, then return.
|
||
if !self.is_active() || self.active_parser_was_aborted.get() {
|
||
return Ok(());
|
||
}
|
||
|
||
let parser = match self.get_current_parser() {
|
||
Some(ref parser) if parser.can_write() => DomRoot::from_ref(&**parser),
|
||
// Step 9: If the insertion point is undefined, then:
|
||
_ => {
|
||
// Step 9.1: If document's unload counter is greater than 0 or
|
||
// document's ignore-destructive-writes counter is greater than 0, then return.
|
||
if self.is_prompting_or_unloading() ||
|
||
self.ignore_destructive_writes_counter.get() > 0
|
||
{
|
||
return Ok(());
|
||
}
|
||
// Step 9.2: Run the document open steps with document.
|
||
self.Open(None, None, can_gc)?;
|
||
self.get_current_parser().unwrap()
|
||
},
|
||
};
|
||
|
||
// Steps 10-11.
|
||
parser.write(string.into(), can_gc);
|
||
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
#[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: &DOMString,
|
||
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.str()) {
|
||
Ok(host) => host,
|
||
Err(_) => return None,
|
||
};
|
||
|
||
// Step 4.
|
||
if host != original_host {
|
||
// Step 4.1
|
||
let host = match host {
|
||
Host::Domain(ref host) => host,
|
||
_ => return None,
|
||
};
|
||
let original_host = match original_host {
|
||
Host::Domain(ref original_host) => original_host,
|
||
_ => return None,
|
||
};
|
||
|
||
// Step 4.2
|
||
let index = original_host.len().checked_sub(host.len())?;
|
||
let (prefix, suffix) = original_host.split_at(index);
|
||
|
||
if !prefix.ends_with('.') {
|
||
return None;
|
||
}
|
||
if suffix != host {
|
||
return None;
|
||
}
|
||
|
||
// Step 4.3
|
||
if is_pub_domain(host) {
|
||
return None;
|
||
}
|
||
}
|
||
|
||
// Step 5
|
||
Some(host)
|
||
}
|
||
|
||
/// <https://url.spec.whatwg.org/#network-scheme>
|
||
fn url_has_network_scheme(url: &ServoUrl) -> bool {
|
||
matches!(url.scheme(), "ftp" | "http" | "https")
|
||
}
|
||
|
||
#[derive(Clone, Copy, Eq, JSTraceable, MallocSizeOf, PartialEq)]
|
||
pub(crate) enum HasBrowsingContext {
|
||
No,
|
||
Yes,
|
||
}
|
||
|
||
impl Document {
|
||
#[allow(clippy::too_many_arguments)]
|
||
pub(crate) fn new_inherited(
|
||
window: &Window,
|
||
has_browsing_context: HasBrowsingContext,
|
||
url: Option<ServoUrl>,
|
||
origin: MutableOrigin,
|
||
is_html_document: IsHTMLDocument,
|
||
content_type: Option<Mime>,
|
||
last_modified: Option<String>,
|
||
activity: DocumentActivity,
|
||
source: DocumentSource,
|
||
doc_loader: DocumentLoader,
|
||
referrer: Option<String>,
|
||
status_code: Option<u16>,
|
||
canceller: FetchCanceller,
|
||
is_initial_about_blank: bool,
|
||
allow_declarative_shadow_roots: bool,
|
||
inherited_insecure_requests_policy: Option<InsecureRequestsPolicy>,
|
||
has_trustworthy_ancestor_origin: bool,
|
||
custom_element_reaction_stack: Rc<CustomElementReactionStack>,
|
||
) -> Document {
|
||
let url = url.unwrap_or_else(|| ServoUrl::parse("about:blank").unwrap());
|
||
|
||
let (ready_state, domcontentloaded_dispatched) = if source == DocumentSource::FromParser {
|
||
(DocumentReadyState::Loading, false)
|
||
} else {
|
||
(DocumentReadyState::Complete, true)
|
||
};
|
||
|
||
let frame_type = match window.is_top_level() {
|
||
true => TimerMetadataFrameType::RootWindow,
|
||
false => TimerMetadataFrameType::IFrame,
|
||
};
|
||
let interactive_time = ProgressiveWebMetrics::new(
|
||
window.time_profiler_chan().clone(),
|
||
url.clone(),
|
||
frame_type,
|
||
);
|
||
|
||
let content_type = content_type.unwrap_or_else(|| {
|
||
match is_html_document {
|
||
// https://dom.spec.whatwg.org/#dom-domimplementation-createhtmldocument
|
||
IsHTMLDocument::HTMLDocument => "text/html",
|
||
// https://dom.spec.whatwg.org/#concept-document-content-type
|
||
IsHTMLDocument::NonHTMLDocument => "application/xml",
|
||
}
|
||
.parse()
|
||
.unwrap()
|
||
});
|
||
|
||
let encoding = content_type
|
||
.get_parameter(CHARSET)
|
||
.and_then(|charset| Encoding::for_label(charset.as_bytes()))
|
||
.unwrap_or(UTF_8);
|
||
|
||
let has_focus = window.parent_info().is_none();
|
||
|
||
let has_browsing_context = has_browsing_context == HasBrowsingContext::Yes;
|
||
|
||
Document {
|
||
node: Node::new_document_node(),
|
||
document_or_shadow_root: DocumentOrShadowRoot::new(window),
|
||
window: Dom::from_ref(window),
|
||
has_browsing_context,
|
||
implementation: Default::default(),
|
||
content_type,
|
||
last_modified,
|
||
url: DomRefCell::new(url),
|
||
// https://dom.spec.whatwg.org/#concept-document-quirks
|
||
quirks_mode: Cell::new(QuirksMode::NoQuirks),
|
||
event_handler: DocumentEventHandler::new(window),
|
||
id_map: DomRefCell::new(HashMapTracedValues::new_fx()),
|
||
name_map: DomRefCell::new(HashMapTracedValues::new_fx()),
|
||
// https://dom.spec.whatwg.org/#concept-document-encoding
|
||
encoding: Cell::new(encoding),
|
||
is_html_document: is_html_document == IsHTMLDocument::HTMLDocument,
|
||
activity: Cell::new(activity),
|
||
tag_map: DomRefCell::new(HashMapTracedValues::new_fx()),
|
||
tagns_map: DomRefCell::new(HashMapTracedValues::new_fx()),
|
||
classes_map: DomRefCell::new(HashMapTracedValues::new()),
|
||
images: Default::default(),
|
||
embeds: Default::default(),
|
||
links: Default::default(),
|
||
forms: Default::default(),
|
||
scripts: Default::default(),
|
||
anchors: Default::default(),
|
||
applets: Default::default(),
|
||
iframes: RefCell::new(IFrameCollection::new()),
|
||
style_shared_lock: {
|
||
/// Per-process shared lock for author-origin stylesheets
|
||
///
|
||
/// FIXME: make it per-document or per-pipeline instead:
|
||
/// <https://github.com/servo/servo/issues/16027>
|
||
/// (Need to figure out what to do with the style attribute
|
||
/// of elements adopted into another document.)
|
||
static PER_PROCESS_AUTHOR_SHARED_LOCK: LazyLock<StyleSharedRwLock> =
|
||
LazyLock::new(StyleSharedRwLock::new);
|
||
|
||
PER_PROCESS_AUTHOR_SHARED_LOCK.clone()
|
||
// StyleSharedRwLock::new()
|
||
},
|
||
stylesheets: DomRefCell::new(DocumentStylesheetSet::new()),
|
||
stylesheet_list: MutNullableDom::new(None),
|
||
ready_state: Cell::new(ready_state),
|
||
domcontentloaded_dispatched: Cell::new(domcontentloaded_dispatched),
|
||
focus_transaction: DomRefCell::new(None),
|
||
focused: Default::default(),
|
||
focus_sequence: Cell::new(FocusSequenceNumber::default()),
|
||
has_focus: Cell::new(has_focus),
|
||
current_script: Default::default(),
|
||
pending_parsing_blocking_script: Default::default(),
|
||
script_blocking_stylesheets_count: Default::default(),
|
||
render_blocking_element_count: Default::default(),
|
||
deferred_scripts: Default::default(),
|
||
asap_in_order_scripts_list: Default::default(),
|
||
asap_scripts_set: Default::default(),
|
||
animation_frame_ident: Cell::new(0),
|
||
animation_frame_list: DomRefCell::new(VecDeque::new()),
|
||
running_animation_callbacks: Cell::new(false),
|
||
loader: DomRefCell::new(doc_loader),
|
||
current_parser: Default::default(),
|
||
base_element: Default::default(),
|
||
appropriate_template_contents_owner_document: Default::default(),
|
||
pending_restyles: DomRefCell::new(FxHashMap::default()),
|
||
needs_restyle: Cell::new(RestyleReason::DOMChanged),
|
||
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()),
|
||
ignore_destructive_writes_counter: Default::default(),
|
||
ignore_opens_during_unload_counter: Default::default(),
|
||
spurious_animation_frames: Cell::new(0),
|
||
dom_count: Cell::new(1),
|
||
fullscreen_element: MutNullableDom::new(None),
|
||
form_id_listener_map: Default::default(),
|
||
interactive_time: DomRefCell::new(interactive_time),
|
||
tti_window: DomRefCell::new(InteractiveWindow::default()),
|
||
canceller,
|
||
throw_on_dynamic_markup_insertion_counter: Cell::new(0),
|
||
page_showing: Cell::new(false),
|
||
salvageable: Cell::new(true),
|
||
active_parser_was_aborted: Cell::new(false),
|
||
fired_unload: Cell::new(false),
|
||
responsive_images: Default::default(),
|
||
redirect_count: Cell::new(0),
|
||
completely_loaded: Cell::new(false),
|
||
script_and_layout_blockers: Cell::new(0),
|
||
delayed_tasks: Default::default(),
|
||
shadow_roots: DomRefCell::new(HashSet::new()),
|
||
shadow_roots_styles_changed: Cell::new(false),
|
||
media_controls: DomRefCell::new(HashMap::new()),
|
||
dirty_2d_contexts: DomRefCell::new(HashMapTracedValues::new_fx()),
|
||
dirty_webgl_contexts: DomRefCell::new(HashMapTracedValues::new_fx()),
|
||
has_pending_animated_image_update: Cell::new(false),
|
||
#[cfg(feature = "webgpu")]
|
||
dirty_webgpu_contexts: DomRefCell::new(HashMapTracedValues::new_fx()),
|
||
selection: MutNullableDom::new(None),
|
||
animation_timeline: if pref!(layout_animations_test_enabled) {
|
||
DomRefCell::new(AnimationTimeline::new_for_testing())
|
||
} else {
|
||
DomRefCell::new(AnimationTimeline::new())
|
||
},
|
||
animations: DomRefCell::new(Animations::new()),
|
||
image_animation_manager: DomRefCell::new(ImageAnimationManager::default()),
|
||
dirty_root: Default::default(),
|
||
declarative_refresh: Default::default(),
|
||
resize_observers: Default::default(),
|
||
fonts: Default::default(),
|
||
visibility_state: Cell::new(DocumentVisibilityState::Hidden),
|
||
status_code,
|
||
is_initial_about_blank: Cell::new(is_initial_about_blank),
|
||
allow_declarative_shadow_roots: Cell::new(allow_declarative_shadow_roots),
|
||
inherited_insecure_requests_policy: Cell::new(inherited_insecure_requests_policy),
|
||
has_trustworthy_ancestor_origin: Cell::new(has_trustworthy_ancestor_origin),
|
||
intersection_observer_task_queued: Cell::new(false),
|
||
intersection_observers: Default::default(),
|
||
highlighted_dom_node: Default::default(),
|
||
adopted_stylesheets: Default::default(),
|
||
adopted_stylesheets_frozen_types: CachedFrozenArray::new(),
|
||
pending_scroll_event_targets: Default::default(),
|
||
resize_observer_started_observing_target: Cell::new(false),
|
||
waiting_on_canvas_image_updates: Cell::new(false),
|
||
current_canvas_epoch: RefCell::new(Epoch(0)),
|
||
custom_element_reaction_stack,
|
||
active_sandboxing_flag_set: Cell::new(SandboxingFlagSet::empty()),
|
||
}
|
||
}
|
||
|
||
/// Returns a policy value that should be used for fetches initiated by this document.
|
||
pub(crate) fn insecure_requests_policy(&self) -> InsecureRequestsPolicy {
|
||
if let Some(csp_list) = self.get_csp_list() {
|
||
for policy in &csp_list.0 {
|
||
if policy.contains_a_directive_whose_name_is("upgrade-insecure-requests") &&
|
||
policy.disposition == PolicyDisposition::Enforce
|
||
{
|
||
return InsecureRequestsPolicy::Upgrade;
|
||
}
|
||
}
|
||
}
|
||
|
||
self.inherited_insecure_requests_policy
|
||
.get()
|
||
.unwrap_or(InsecureRequestsPolicy::DoNotUpgrade)
|
||
}
|
||
|
||
/// Get the [`Document`]'s [`DocumentEventHandler`].
|
||
pub(crate) fn event_handler(&self) -> &DocumentEventHandler {
|
||
&self.event_handler
|
||
}
|
||
|
||
/// Whether or not this [`Document`] has any pending scroll events to be processed during
|
||
/// "update the rendering."
|
||
fn has_pending_scroll_events(&self) -> bool {
|
||
!self.pending_scroll_event_targets.borrow().is_empty()
|
||
}
|
||
|
||
pub(crate) fn set_resize_observer_started_observing_target(&self, value: bool) {
|
||
self.resize_observer_started_observing_target.set(value);
|
||
}
|
||
|
||
/// 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 + NonSendTaskBox>(&self, task: T) {
|
||
self.delayed_tasks.borrow_mut().push(Box::new(task));
|
||
}
|
||
|
||
/// Assert that the DOM is in a state that will allow running content JS or
|
||
/// performing a layout operation.
|
||
pub(crate) fn ensure_safe_to_run_script_or_layout(&self) {
|
||
assert_eq!(
|
||
self.script_and_layout_blockers.get(),
|
||
0,
|
||
"Attempt to use script or layout while DOM not in a stable state"
|
||
);
|
||
}
|
||
|
||
#[allow(clippy::too_many_arguments)]
|
||
pub(crate) fn new(
|
||
window: &Window,
|
||
has_browsing_context: HasBrowsingContext,
|
||
url: Option<ServoUrl>,
|
||
origin: MutableOrigin,
|
||
doctype: IsHTMLDocument,
|
||
content_type: Option<Mime>,
|
||
last_modified: Option<String>,
|
||
activity: DocumentActivity,
|
||
source: DocumentSource,
|
||
doc_loader: DocumentLoader,
|
||
referrer: Option<String>,
|
||
status_code: Option<u16>,
|
||
canceller: FetchCanceller,
|
||
is_initial_about_blank: bool,
|
||
allow_declarative_shadow_roots: bool,
|
||
inherited_insecure_requests_policy: Option<InsecureRequestsPolicy>,
|
||
has_trustworthy_ancestor_origin: bool,
|
||
custom_element_reaction_stack: Rc<CustomElementReactionStack>,
|
||
can_gc: CanGc,
|
||
) -> DomRoot<Document> {
|
||
Self::new_with_proto(
|
||
window,
|
||
None,
|
||
has_browsing_context,
|
||
url,
|
||
origin,
|
||
doctype,
|
||
content_type,
|
||
last_modified,
|
||
activity,
|
||
source,
|
||
doc_loader,
|
||
referrer,
|
||
status_code,
|
||
canceller,
|
||
is_initial_about_blank,
|
||
allow_declarative_shadow_roots,
|
||
inherited_insecure_requests_policy,
|
||
has_trustworthy_ancestor_origin,
|
||
custom_element_reaction_stack,
|
||
can_gc,
|
||
)
|
||
}
|
||
|
||
#[allow(clippy::too_many_arguments)]
|
||
fn new_with_proto(
|
||
window: &Window,
|
||
proto: Option<HandleObject>,
|
||
has_browsing_context: HasBrowsingContext,
|
||
url: Option<ServoUrl>,
|
||
origin: MutableOrigin,
|
||
doctype: IsHTMLDocument,
|
||
content_type: Option<Mime>,
|
||
last_modified: Option<String>,
|
||
activity: DocumentActivity,
|
||
source: DocumentSource,
|
||
doc_loader: DocumentLoader,
|
||
referrer: Option<String>,
|
||
status_code: Option<u16>,
|
||
canceller: FetchCanceller,
|
||
is_initial_about_blank: bool,
|
||
allow_declarative_shadow_roots: bool,
|
||
inherited_insecure_requests_policy: Option<InsecureRequestsPolicy>,
|
||
has_trustworthy_ancestor_origin: bool,
|
||
custom_element_reaction_stack: Rc<CustomElementReactionStack>,
|
||
can_gc: CanGc,
|
||
) -> DomRoot<Document> {
|
||
let document = reflect_dom_object_with_proto(
|
||
Box::new(Document::new_inherited(
|
||
window,
|
||
has_browsing_context,
|
||
url,
|
||
origin,
|
||
doctype,
|
||
content_type,
|
||
last_modified,
|
||
activity,
|
||
source,
|
||
doc_loader,
|
||
referrer,
|
||
status_code,
|
||
canceller,
|
||
is_initial_about_blank,
|
||
allow_declarative_shadow_roots,
|
||
inherited_insecure_requests_policy,
|
||
has_trustworthy_ancestor_origin,
|
||
custom_element_reaction_stack,
|
||
)),
|
||
window,
|
||
proto,
|
||
can_gc,
|
||
);
|
||
{
|
||
let node = document.upcast::<Node>();
|
||
node.set_owner_doc(&document);
|
||
}
|
||
document
|
||
}
|
||
|
||
pub(crate) fn get_redirect_count(&self) -> u16 {
|
||
self.redirect_count.get()
|
||
}
|
||
|
||
pub(crate) fn set_redirect_count(&self, count: u16) {
|
||
self.redirect_count.set(count)
|
||
}
|
||
|
||
pub(crate) fn elements_by_name_count(&self, name: &DOMString) -> u32 {
|
||
if name.is_empty() {
|
||
return 0;
|
||
}
|
||
self.count_node_list(|n| Document::is_element_in_get_by_name(n, name))
|
||
}
|
||
|
||
pub(crate) fn nth_element_by_name(
|
||
&self,
|
||
index: u32,
|
||
name: &DOMString,
|
||
) -> Option<DomRoot<Node>> {
|
||
if name.is_empty() {
|
||
return None;
|
||
}
|
||
self.nth_in_node_list(index, |n| Document::is_element_in_get_by_name(n, name))
|
||
}
|
||
|
||
// Note that document.getByName does not match on the same conditions
|
||
// as the document named getter.
|
||
fn is_element_in_get_by_name(node: &Node, name: &DOMString) -> bool {
|
||
let element = match node.downcast::<Element>() {
|
||
Some(element) => element,
|
||
None => return false,
|
||
};
|
||
if element.namespace() != &ns!(html) {
|
||
return false;
|
||
}
|
||
element.get_name().is_some_and(|n| *n == *name)
|
||
}
|
||
|
||
fn count_node_list<F: Fn(&Node) -> bool>(&self, callback: F) -> u32 {
|
||
let doc = self.GetDocumentElement();
|
||
let maybe_node = doc.as_deref().map(Castable::upcast::<Node>);
|
||
maybe_node
|
||
.iter()
|
||
.flat_map(|node| node.traverse_preorder(ShadowIncluding::No))
|
||
.filter(|node| callback(node))
|
||
.count() as u32
|
||
}
|
||
|
||
fn nth_in_node_list<F: Fn(&Node) -> bool>(
|
||
&self,
|
||
index: u32,
|
||
callback: F,
|
||
) -> Option<DomRoot<Node>> {
|
||
let doc = self.GetDocumentElement();
|
||
let maybe_node = doc.as_deref().map(Castable::upcast::<Node>);
|
||
maybe_node
|
||
.iter()
|
||
.flat_map(|node| node.traverse_preorder(ShadowIncluding::No))
|
||
.filter(|node| callback(node))
|
||
.nth(index as usize)
|
||
.map(|n| DomRoot::from_ref(&*n))
|
||
}
|
||
|
||
fn get_html_element(&self) -> Option<DomRoot<HTMLHtmlElement>> {
|
||
self.GetDocumentElement().and_then(DomRoot::downcast)
|
||
}
|
||
|
||
/// Return a reference to the per-document shared lock used in stylesheets.
|
||
pub(crate) fn style_shared_lock(&self) -> &StyleSharedRwLock {
|
||
&self.style_shared_lock
|
||
}
|
||
|
||
/// Flushes the stylesheet list, and returns whether any stylesheet changed.
|
||
pub(crate) fn flush_stylesheets_for_reflow(&self) -> bool {
|
||
// NOTE(emilio): The invalidation machinery is used on the replicated
|
||
// list in layout.
|
||
//
|
||
// FIXME(emilio): This really should differentiate between CSSOM changes
|
||
// and normal stylesheets additions / removals, because in the last case
|
||
// layout already has that information and we could avoid dirtying the whole thing.
|
||
let mut stylesheets = self.stylesheets.borrow_mut();
|
||
let have_changed = stylesheets.has_changed();
|
||
stylesheets.flush_without_invalidation();
|
||
have_changed
|
||
}
|
||
|
||
pub(crate) fn salvageable(&self) -> bool {
|
||
self.salvageable.get()
|
||
}
|
||
|
||
/// <https://html.spec.whatwg.org/multipage/#appropriate-template-contents-owner-document>
|
||
pub(crate) fn appropriate_template_contents_owner_document(
|
||
&self,
|
||
can_gc: CanGc,
|
||
) -> DomRoot<Document> {
|
||
self.appropriate_template_contents_owner_document
|
||
.or_init(|| {
|
||
let doctype = if self.is_html_document {
|
||
IsHTMLDocument::HTMLDocument
|
||
} else {
|
||
IsHTMLDocument::NonHTMLDocument
|
||
};
|
||
let new_doc = Document::new(
|
||
self.window(),
|
||
HasBrowsingContext::No,
|
||
None,
|
||
// https://github.com/whatwg/html/issues/2109
|
||
MutableOrigin::new(ImmutableOrigin::new_opaque()),
|
||
doctype,
|
||
None,
|
||
None,
|
||
DocumentActivity::Inactive,
|
||
DocumentSource::NotFromParser,
|
||
DocumentLoader::new(&self.loader()),
|
||
None,
|
||
None,
|
||
Default::default(),
|
||
false,
|
||
self.allow_declarative_shadow_roots(),
|
||
Some(self.insecure_requests_policy()),
|
||
self.has_trustworthy_ancestor_or_current_origin(),
|
||
self.custom_element_reaction_stack.clone(),
|
||
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_attr_will_change(&self, el: &Element, attr: &Attr) {
|
||
// FIXME(emilio): Kind of a shame we have to duplicate this.
|
||
//
|
||
// I'm getting rid of the whole hashtable soon anyway, since all it does
|
||
// right now is populate the element restyle data in layout, and we
|
||
// could in theory do it in the DOM I think.
|
||
let mut entry = self.ensure_pending_restyle(el);
|
||
if entry.snapshot.is_none() {
|
||
entry.snapshot = Some(Snapshot::new());
|
||
}
|
||
if attr.local_name() == &local_name!("style") {
|
||
entry.hint.insert(RestyleHint::RESTYLE_STYLE_ATTRIBUTE);
|
||
}
|
||
|
||
if vtable_for(el.upcast()).attribute_affects_presentational_hints(attr) {
|
||
entry.hint.insert(RestyleHint::RESTYLE_SELF);
|
||
}
|
||
|
||
let snapshot = entry.snapshot.as_mut().unwrap();
|
||
if attr.local_name() == &local_name!("id") {
|
||
if snapshot.id_changed {
|
||
return;
|
||
}
|
||
snapshot.id_changed = true;
|
||
} else if attr.local_name() == &local_name!("class") {
|
||
if snapshot.class_changed {
|
||
return;
|
||
}
|
||
snapshot.class_changed = true;
|
||
} else {
|
||
snapshot.other_attributes_changed = true;
|
||
}
|
||
let local_name = style::LocalName::cast(attr.local_name());
|
||
if !snapshot.changed_attrs.contains(local_name) {
|
||
snapshot.changed_attrs.push(local_name.clone());
|
||
}
|
||
if snapshot.attrs.is_none() {
|
||
let attrs = el
|
||
.attrs()
|
||
.iter()
|
||
.map(|attr| (attr.identifier().clone(), attr.value().clone()))
|
||
.collect();
|
||
snapshot.attrs = Some(attrs);
|
||
}
|
||
}
|
||
|
||
pub(crate) fn set_referrer_policy(&self, policy: ReferrerPolicy) {
|
||
self.policy_container
|
||
.borrow_mut()
|
||
.set_referrer_policy(policy);
|
||
}
|
||
|
||
pub(crate) fn get_referrer_policy(&self) -> ReferrerPolicy {
|
||
self.policy_container.borrow().get_referrer_policy()
|
||
}
|
||
|
||
pub(crate) fn set_target_element(&self, node: Option<&Element>) {
|
||
if let Some(ref element) = self.target_element.get() {
|
||
element.set_target_state(false);
|
||
}
|
||
|
||
self.target_element.set(node);
|
||
|
||
if let Some(ref element) = self.target_element.get() {
|
||
element.set_target_state(true);
|
||
}
|
||
}
|
||
|
||
pub(crate) fn incr_ignore_destructive_writes_counter(&self) {
|
||
self.ignore_destructive_writes_counter
|
||
.set(self.ignore_destructive_writes_counter.get() + 1);
|
||
}
|
||
|
||
pub(crate) fn decr_ignore_destructive_writes_counter(&self) {
|
||
self.ignore_destructive_writes_counter
|
||
.set(self.ignore_destructive_writes_counter.get() - 1);
|
||
}
|
||
|
||
pub(crate) fn is_prompting_or_unloading(&self) -> bool {
|
||
self.ignore_opens_during_unload_counter.get() > 0
|
||
}
|
||
|
||
fn incr_ignore_opens_during_unload_counter(&self) {
|
||
self.ignore_opens_during_unload_counter
|
||
.set(self.ignore_opens_during_unload_counter.get() + 1);
|
||
}
|
||
|
||
fn decr_ignore_opens_during_unload_counter(&self) {
|
||
self.ignore_opens_during_unload_counter
|
||
.set(self.ignore_opens_during_unload_counter.get() - 1);
|
||
}
|
||
|
||
// https://fullscreen.spec.whatwg.org/#dom-element-requestfullscreen
|
||
pub(crate) fn enter_fullscreen(&self, pending: &Element, can_gc: CanGc) -> Rc<Promise> {
|
||
// Step 1
|
||
let in_realm_proof = AlreadyInRealm::assert::<crate::DomTypeHolder>();
|
||
let promise = Promise::new_in_current_realm(InRealm::Already(&in_realm_proof), can_gc);
|
||
let mut error = false;
|
||
|
||
// Step 4
|
||
// check namespace
|
||
match *pending.namespace() {
|
||
ns!(mathml) => {
|
||
if pending.local_name().as_ref() != "math" {
|
||
error = true;
|
||
}
|
||
},
|
||
ns!(svg) => {
|
||
if pending.local_name().as_ref() != "svg" {
|
||
error = true;
|
||
}
|
||
},
|
||
ns!(html) => (),
|
||
_ => error = true,
|
||
}
|
||
// fullscreen element ready check
|
||
if !pending.fullscreen_element_ready_check() {
|
||
error = true;
|
||
}
|
||
|
||
if pref!(dom_fullscreen_test) {
|
||
// For reftests we just take over the current window,
|
||
// and don't try to really enter fullscreen.
|
||
info!("Tests don't really enter fullscreen.");
|
||
} else {
|
||
// TODO fullscreen is supported
|
||
// TODO This algorithm is allowed to request fullscreen.
|
||
warn!("Fullscreen not supported yet");
|
||
}
|
||
|
||
// Step 5 Parallel start
|
||
|
||
let window = self.window();
|
||
// Step 6
|
||
if !error {
|
||
let event = EmbedderMsg::NotifyFullscreenStateChanged(self.webview_id(), true);
|
||
self.send_to_embedder(event);
|
||
}
|
||
|
||
let pipeline_id = self.window().pipeline_id();
|
||
|
||
// Step 7
|
||
let trusted_pending = Trusted::new(pending);
|
||
let trusted_promise = TrustedPromise::new(promise.clone());
|
||
let handler = ElementPerformFullscreenEnter::new(trusted_pending, trusted_promise, error);
|
||
// NOTE: This steps should be running in parallel
|
||
// https://fullscreen.spec.whatwg.org/#dom-element-requestfullscreen
|
||
let script_msg = CommonScriptMsg::Task(
|
||
ScriptThreadEventCategory::EnterFullscreen,
|
||
handler,
|
||
Some(pipeline_id),
|
||
TaskSourceName::DOMManipulation,
|
||
);
|
||
let msg = MainThreadScriptMsg::Common(script_msg);
|
||
window.main_thread_script_chan().send(msg).unwrap();
|
||
|
||
promise
|
||
}
|
||
|
||
// https://fullscreen.spec.whatwg.org/#exit-fullscreen
|
||
pub(crate) fn exit_fullscreen(&self, can_gc: CanGc) -> Rc<Promise> {
|
||
let global = self.global();
|
||
// Step 1
|
||
let in_realm_proof = AlreadyInRealm::assert::<crate::DomTypeHolder>();
|
||
let promise = Promise::new_in_current_realm(InRealm::Already(&in_realm_proof), can_gc);
|
||
// Step 2
|
||
if self.fullscreen_element.get().is_none() {
|
||
promise.reject_error(Error::Type(String::from("fullscreen is null")), can_gc);
|
||
return promise;
|
||
}
|
||
// TODO Step 3-6
|
||
let element = self.fullscreen_element.get().unwrap();
|
||
|
||
// Step 7 Parallel start
|
||
|
||
let window = self.window();
|
||
// Step 8
|
||
let event = EmbedderMsg::NotifyFullscreenStateChanged(self.webview_id(), false);
|
||
self.send_to_embedder(event);
|
||
|
||
// Step 9
|
||
let trusted_element = Trusted::new(&*element);
|
||
let trusted_promise = TrustedPromise::new(promise.clone());
|
||
let handler = ElementPerformFullscreenExit::new(trusted_element, trusted_promise);
|
||
let pipeline_id = Some(global.pipeline_id());
|
||
// NOTE: This steps should be running in parallel
|
||
// https://fullscreen.spec.whatwg.org/#exit-fullscreen
|
||
let script_msg = CommonScriptMsg::Task(
|
||
ScriptThreadEventCategory::ExitFullscreen,
|
||
handler,
|
||
pipeline_id,
|
||
TaskSourceName::DOMManipulation,
|
||
);
|
||
let msg = MainThreadScriptMsg::Common(script_msg);
|
||
window.main_thread_script_chan().send(msg).unwrap();
|
||
|
||
promise
|
||
}
|
||
|
||
pub(crate) fn set_fullscreen_element(&self, element: Option<&Element>) {
|
||
self.fullscreen_element.set(element);
|
||
}
|
||
|
||
pub(crate) fn get_allow_fullscreen(&self) -> bool {
|
||
// https://html.spec.whatwg.org/multipage/#allowed-to-use
|
||
match self.browsing_context() {
|
||
// Step 1
|
||
None => false,
|
||
Some(_) => {
|
||
// Step 2
|
||
let window = self.window();
|
||
if window.is_top_level() {
|
||
true
|
||
} else {
|
||
// Step 3
|
||
window
|
||
.GetFrameElement()
|
||
.is_some_and(|el| el.has_attribute(&local_name!("allowfullscreen")))
|
||
}
|
||
},
|
||
}
|
||
}
|
||
|
||
fn reset_form_owner_for_listeners(&self, id: &Atom, can_gc: CanGc) {
|
||
let map = self.form_id_listener_map.borrow();
|
||
if let Some(listeners) = map.get(id) {
|
||
for listener in listeners {
|
||
listener
|
||
.as_maybe_form_control()
|
||
.expect("Element must be a form control")
|
||
.reset_form_owner(can_gc);
|
||
}
|
||
}
|
||
}
|
||
|
||
pub(crate) fn register_shadow_root(&self, shadow_root: &ShadowRoot) {
|
||
self.shadow_roots
|
||
.borrow_mut()
|
||
.insert(Dom::from_ref(shadow_root));
|
||
self.invalidate_shadow_roots_stylesheets();
|
||
}
|
||
|
||
pub(crate) fn unregister_shadow_root(&self, shadow_root: &ShadowRoot) {
|
||
let mut shadow_roots = self.shadow_roots.borrow_mut();
|
||
shadow_roots.remove(&Dom::from_ref(shadow_root));
|
||
}
|
||
|
||
pub(crate) fn invalidate_shadow_roots_stylesheets(&self) {
|
||
self.shadow_roots_styles_changed.set(true);
|
||
}
|
||
|
||
pub(crate) fn shadow_roots_styles_changed(&self) -> bool {
|
||
self.shadow_roots_styles_changed.get()
|
||
}
|
||
|
||
pub(crate) fn flush_shadow_roots_stylesheets(&self) {
|
||
if !self.shadow_roots_styles_changed.get() {
|
||
return;
|
||
}
|
||
self.shadow_roots_styles_changed.set(false);
|
||
}
|
||
|
||
pub(crate) fn stylesheet_count(&self) -> usize {
|
||
self.stylesheets.borrow().len()
|
||
}
|
||
|
||
pub(crate) fn stylesheet_at(&self, index: usize) -> Option<DomRoot<CSSStyleSheet>> {
|
||
let stylesheets = self.stylesheets.borrow();
|
||
|
||
stylesheets
|
||
.get(Origin::Author, index)
|
||
.and_then(|s| s.owner.get_cssom_object())
|
||
}
|
||
|
||
/// Add a stylesheet owned by `owner_node` to the list of document sheets, in the
|
||
/// correct tree position. Additionally, ensure that the owned stylesheet is inserted
|
||
/// before any constructed stylesheet.
|
||
///
|
||
/// <https://drafts.csswg.org/cssom/#documentorshadowroot-final-css-style-sheets>
|
||
#[cfg_attr(crown, allow(crown::unrooted_must_root))] // Owner needs to be rooted already necessarily.
|
||
pub(crate) fn add_owned_stylesheet(&self, owner_node: &Element, sheet: Arc<Stylesheet>) {
|
||
let stylesheets = &mut *self.stylesheets.borrow_mut();
|
||
|
||
// FIXME(stevennovaryo): This is almost identical with the one in ShadowRoot::add_stylesheet.
|
||
let insertion_point = stylesheets
|
||
.iter()
|
||
.map(|(sheet, _origin)| sheet)
|
||
.find(|sheet_in_doc| {
|
||
match &sheet_in_doc.owner {
|
||
StylesheetSource::Element(other_node) => {
|
||
owner_node.upcast::<Node>().is_before(other_node.upcast())
|
||
},
|
||
// Non-constructed stylesheet should be ordered before the
|
||
// constructed ones.
|
||
StylesheetSource::Constructed(_) => true,
|
||
}
|
||
})
|
||
.cloned();
|
||
|
||
if self.has_browsing_context() {
|
||
self.window.layout_mut().add_stylesheet(
|
||
sheet.clone(),
|
||
insertion_point.as_ref().map(|s| s.sheet.clone()),
|
||
);
|
||
}
|
||
|
||
DocumentOrShadowRoot::add_stylesheet(
|
||
StylesheetSource::Element(Dom::from_ref(owner_node)),
|
||
StylesheetSetRef::Document(stylesheets),
|
||
sheet,
|
||
insertion_point,
|
||
self.style_shared_lock(),
|
||
);
|
||
}
|
||
|
||
/// Append a constructed stylesheet to the back of document stylesheet set. Because
|
||
/// it would be the last element, we therefore would not mess with the ordering.
|
||
///
|
||
/// <https://drafts.csswg.org/cssom/#documentorshadowroot-final-css-style-sheets>
|
||
#[cfg_attr(crown, allow(crown::unrooted_must_root))]
|
||
pub(crate) fn append_constructed_stylesheet(&self, cssom_stylesheet: &CSSStyleSheet) {
|
||
debug_assert!(cssom_stylesheet.is_constructed());
|
||
|
||
let stylesheets = &mut *self.stylesheets.borrow_mut();
|
||
let sheet = cssom_stylesheet.style_stylesheet().clone();
|
||
|
||
let insertion_point = stylesheets
|
||
.iter()
|
||
.last()
|
||
.map(|(sheet, _origin)| sheet)
|
||
.cloned();
|
||
|
||
if self.has_browsing_context() {
|
||
self.window.layout_mut().add_stylesheet(
|
||
sheet.clone(),
|
||
insertion_point.as_ref().map(|s| s.sheet.clone()),
|
||
);
|
||
}
|
||
|
||
DocumentOrShadowRoot::add_stylesheet(
|
||
StylesheetSource::Constructed(Dom::from_ref(cssom_stylesheet)),
|
||
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: StylesheetSource, stylesheet: &Arc<Stylesheet>) {
|
||
if self.has_browsing_context() {
|
||
self.window
|
||
.layout_mut()
|
||
.remove_stylesheet(stylesheet.clone());
|
||
}
|
||
|
||
DocumentOrShadowRoot::remove_stylesheet(
|
||
owner,
|
||
stylesheet,
|
||
StylesheetSetRef::Document(&mut *self.stylesheets.borrow_mut()),
|
||
)
|
||
}
|
||
|
||
pub(crate) fn get_elements_with_id(&self, id: &Atom) -> Ref<'_, [Dom<Element>]> {
|
||
Ref::map(self.id_map.borrow(), |map| {
|
||
map.get(id).map(|vec| &**vec).unwrap_or_default()
|
||
})
|
||
}
|
||
|
||
pub(crate) fn get_elements_with_name(&self, name: &Atom) -> Ref<'_, [Dom<Element>]> {
|
||
Ref::map(self.name_map.borrow(), |map| {
|
||
map.get(name).map(|vec| &**vec).unwrap_or_default()
|
||
})
|
||
}
|
||
|
||
#[cfg_attr(crown, allow(crown::unrooted_must_root))]
|
||
pub(crate) fn drain_pending_restyles(&self) -> Vec<(TrustedNodeAddress, PendingRestyle)> {
|
||
self.pending_restyles
|
||
.borrow_mut()
|
||
.drain()
|
||
.filter_map(|(elem, restyle)| {
|
||
let node = elem.upcast::<Node>();
|
||
if !node.get_flag(NodeFlags::IS_CONNECTED) {
|
||
return None;
|
||
}
|
||
node.note_dirty_descendants();
|
||
Some((node.to_trusted_node_address(), restyle.0))
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
pub(crate) fn advance_animation_timeline_for_testing(&self, delta: f64) {
|
||
self.animation_timeline.borrow_mut().advance_specific(delta);
|
||
let current_timeline_value = self.current_animation_timeline_value();
|
||
self.animations
|
||
.borrow()
|
||
.update_for_new_timeline_value(&self.window, current_timeline_value);
|
||
}
|
||
|
||
pub(crate) fn maybe_mark_animating_nodes_as_dirty(&self) {
|
||
let current_timeline_value = self.current_animation_timeline_value();
|
||
self.animations
|
||
.borrow()
|
||
.mark_animating_nodes_as_dirty(current_timeline_value);
|
||
}
|
||
|
||
pub(crate) fn current_animation_timeline_value(&self) -> f64 {
|
||
self.animation_timeline.borrow().current_value()
|
||
}
|
||
|
||
pub(crate) fn animations(&self) -> Ref<'_, Animations> {
|
||
self.animations.borrow()
|
||
}
|
||
|
||
pub(crate) fn update_animations_post_reflow(&self) {
|
||
self.animations
|
||
.borrow()
|
||
.do_post_reflow_update(&self.window, self.current_animation_timeline_value());
|
||
self.image_animation_manager
|
||
.borrow()
|
||
.update_rooted_dom_nodes(&self.window, self.current_animation_timeline_value());
|
||
}
|
||
|
||
pub(crate) fn cancel_animations_for_node(&self, node: &Node) {
|
||
self.animations.borrow().cancel_animations_for_node(node);
|
||
}
|
||
|
||
/// An implementation of <https://drafts.csswg.org/web-animations-1/#update-animations-and-send-events>.
|
||
pub(crate) fn update_animations_and_send_events(&self, can_gc: CanGc) {
|
||
// Only update the time if it isn't being managed by a test.
|
||
if !pref!(layout_animations_test_enabled) {
|
||
self.animation_timeline.borrow_mut().update();
|
||
}
|
||
|
||
// > 1. Update the current time of all timelines associated with doc passing now
|
||
// > as the timestamp.
|
||
// > 2. Remove replaced animations for doc.
|
||
//
|
||
// We still want to update the animations, because our timeline
|
||
// value might have been advanced previously via the TestBinding.
|
||
let current_timeline_value = self.current_animation_timeline_value();
|
||
self.animations
|
||
.borrow()
|
||
.update_for_new_timeline_value(&self.window, current_timeline_value);
|
||
self.maybe_mark_animating_nodes_as_dirty();
|
||
|
||
// > 3. Perform a microtask checkpoint.
|
||
self.window()
|
||
.as_global_scope()
|
||
.perform_a_microtask_checkpoint(can_gc);
|
||
|
||
// Steps 4 through 7 occur inside `send_pending_events().`
|
||
let _realm = enter_realm(self);
|
||
self.animations().send_pending_events(self.window(), can_gc);
|
||
}
|
||
|
||
pub(crate) fn image_animation_manager(&self) -> Ref<'_, ImageAnimationManager> {
|
||
self.image_animation_manager.borrow()
|
||
}
|
||
|
||
pub(crate) fn set_has_pending_animated_image_update(&self) {
|
||
self.has_pending_animated_image_update.set(true);
|
||
}
|
||
|
||
/// <https://html.spec.whatwg.org/multipage/#shared-declarative-refresh-steps>
|
||
pub(crate) fn shared_declarative_refresh_steps(&self, content: &[u8]) {
|
||
// 1. If document's will declaratively refresh is true, then return.
|
||
if self.will_declaratively_refresh() {
|
||
return;
|
||
}
|
||
|
||
// 2-11 Parsing
|
||
static REFRESH_REGEX: LazyLock<Regex> = LazyLock::new(|| {
|
||
// s flag is used to match . on newlines since the only places we use . in the
|
||
// regex is to go "to end of the string"
|
||
// (?s-u:.) is used to consume invalid unicode bytes
|
||
Regex::new(
|
||
r#"(?xs)
|
||
^
|
||
\s* # 3
|
||
((?<time>[0-9]+)|\.) # 5-6
|
||
[0-9.]* # 8
|
||
(
|
||
(
|
||
(\s*;|\s*,|\s) # 10.3
|
||
\s* # 10.4
|
||
)
|
||
(
|
||
(
|
||
(U|u)(R|r)(L|l) # 11.2-11.4
|
||
\s*=\s* # 11.5-11.7
|
||
)?
|
||
('(?<url1>[^']*)'(?s-u:.)*|"(?<url2>[^"]*)"(?s-u:.)*|['"]?(?<url3>(?s-u:.)*)) # 11.8 - 11.10
|
||
|
|
||
(?<url4>(?s-u:.)*)
|
||
)
|
||
)?
|
||
$
|
||
"#,
|
||
)
|
||
.unwrap()
|
||
});
|
||
|
||
// 9. Let urlRecord be document's URL.
|
||
let mut url_record = self.url();
|
||
let captures = if let Some(captures) = REFRESH_REGEX.captures(content) {
|
||
captures
|
||
} else {
|
||
return;
|
||
};
|
||
let time = if let Some(time_string) = captures.name("time") {
|
||
u64::from_str(&String::from_utf8_lossy(time_string.as_bytes())).unwrap_or(0)
|
||
} else {
|
||
0
|
||
};
|
||
let captured_url = captures.name("url1").or(captures
|
||
.name("url2")
|
||
.or(captures.name("url3").or(captures.name("url4"))));
|
||
|
||
// 11.11 Parse: Set urlRecord to the result of encoding-parsing a URL given urlString, relative to document.
|
||
if let Some(url_match) = captured_url {
|
||
url_record = if let Ok(url) = ServoUrl::parse_with_base(
|
||
Some(&url_record),
|
||
&String::from_utf8_lossy(url_match.as_bytes()),
|
||
) {
|
||
info!("Refresh to {}", url.debug_compact());
|
||
url
|
||
} else {
|
||
// 11.12 If urlRecord is failure, then return.
|
||
return;
|
||
}
|
||
}
|
||
// 12. Set document's will declaratively refresh to true.
|
||
if self.completely_loaded() {
|
||
// TODO: handle active sandboxing flag
|
||
self.window.as_global_scope().schedule_callback(
|
||
OneshotTimerCallback::RefreshRedirectDue(RefreshRedirectDue {
|
||
window: DomRoot::from_ref(self.window()),
|
||
url: url_record,
|
||
}),
|
||
Duration::from_secs(time),
|
||
);
|
||
self.set_declarative_refresh(DeclarativeRefresh::CreatedAfterLoad);
|
||
} else {
|
||
self.set_declarative_refresh(DeclarativeRefresh::PendingLoad {
|
||
url: url_record,
|
||
time,
|
||
});
|
||
}
|
||
}
|
||
|
||
pub(crate) fn will_declaratively_refresh(&self) -> bool {
|
||
self.declarative_refresh.borrow().is_some()
|
||
}
|
||
pub(crate) fn set_declarative_refresh(&self, refresh: DeclarativeRefresh) {
|
||
*self.declarative_refresh.borrow_mut() = Some(refresh);
|
||
}
|
||
|
||
/// <https://html.spec.whatwg.org/multipage/#visibility-state>
|
||
fn update_visibility_state(&self, visibility_state: DocumentVisibilityState, can_gc: CanGc) {
|
||
// Step 1 If document's visibility state equals visibilityState, then return.
|
||
if self.visibility_state.get() == visibility_state {
|
||
return;
|
||
}
|
||
// Step 2 Set document's visibility state to visibilityState.
|
||
self.visibility_state.set(visibility_state);
|
||
// Step 3 Queue a new VisibilityStateEntry whose visibility state is visibilityState and whose timestamp is
|
||
// the current high resolution time given document's relevant global object.
|
||
let entry = VisibilityStateEntry::new(
|
||
&self.global(),
|
||
visibility_state,
|
||
CrossProcessInstant::now(),
|
||
can_gc,
|
||
);
|
||
self.window
|
||
.Performance()
|
||
.queue_entry(entry.upcast::<PerformanceEntry>(), can_gc);
|
||
|
||
// Step 4 Run the screen orientation change steps with document.
|
||
// TODO ScreenOrientation hasn't implemented yet
|
||
|
||
// Step 5 Run the view transition page visibility change steps with document.
|
||
// TODO ViewTransition hasn't implemented yet
|
||
|
||
// Step 6 Run any page visibility change steps which may be defined in other specifications, with visibility
|
||
// state and document. Any other specs' visibility steps will go here.
|
||
|
||
// <https://www.w3.org/TR/gamepad/#handling-visibility-change>
|
||
if visibility_state == DocumentVisibilityState::Hidden {
|
||
self.window
|
||
.Navigator()
|
||
.GetGamepads()
|
||
.iter_mut()
|
||
.for_each(|gamepad| {
|
||
if let Some(g) = gamepad {
|
||
g.vibration_actuator().handle_visibility_change();
|
||
}
|
||
});
|
||
}
|
||
|
||
// Step 7 Fire an event named visibilitychange at document, with its bubbles attribute initialized to true.
|
||
self.upcast::<EventTarget>()
|
||
.fire_bubbling_event(atom!("visibilitychange"), can_gc);
|
||
}
|
||
|
||
/// <https://html.spec.whatwg.org/multipage/#is-initial-about:blank>
|
||
pub(crate) fn is_initial_about_blank(&self) -> bool {
|
||
self.is_initial_about_blank.get()
|
||
}
|
||
|
||
/// <https://dom.spec.whatwg.org/#document-allow-declarative-shadow-roots>
|
||
pub(crate) fn allow_declarative_shadow_roots(&self) -> bool {
|
||
self.allow_declarative_shadow_roots.get()
|
||
}
|
||
|
||
pub(crate) fn has_trustworthy_ancestor_origin(&self) -> bool {
|
||
self.has_trustworthy_ancestor_origin.get()
|
||
}
|
||
|
||
pub(crate) fn has_trustworthy_ancestor_or_current_origin(&self) -> bool {
|
||
self.has_trustworthy_ancestor_origin.get() ||
|
||
self.origin().immutable().is_potentially_trustworthy()
|
||
}
|
||
|
||
pub(crate) fn highlight_dom_node(&self, node: Option<&Node>) {
|
||
self.highlighted_dom_node.set(node);
|
||
self.add_restyle_reason(RestyleReason::HighlightedDOMNodeChanged);
|
||
}
|
||
|
||
pub(crate) fn highlighted_dom_node(&self) -> Option<DomRoot<Node>> {
|
||
self.highlighted_dom_node.get()
|
||
}
|
||
|
||
pub(crate) fn custom_element_reaction_stack(&self) -> Rc<CustomElementReactionStack> {
|
||
self.custom_element_reaction_stack.clone()
|
||
}
|
||
|
||
pub(crate) fn has_active_sandboxing_flag(&self, flag: SandboxingFlagSet) -> bool {
|
||
self.active_sandboxing_flag_set.get().contains(flag)
|
||
}
|
||
|
||
pub(crate) fn set_active_sandboxing_flag_set(&self, flags: SandboxingFlagSet) {
|
||
self.active_sandboxing_flag_set.set(flags)
|
||
}
|
||
|
||
pub(crate) fn viewport_scrolling_box(&self, flags: ScrollContainerQueryFlags) -> ScrollingBox {
|
||
self.window()
|
||
.scrolling_box_query(None, flags)
|
||
.expect("We should always have a ScrollingBox for the Viewport")
|
||
}
|
||
}
|
||
|
||
#[allow(non_snake_case)]
|
||
impl DocumentMethods<crate::DomTypeHolder> for Document {
|
||
// https://dom.spec.whatwg.org/#dom-document-document
|
||
fn Constructor(
|
||
window: &Window,
|
||
proto: Option<HandleObject>,
|
||
can_gc: CanGc,
|
||
) -> Fallible<DomRoot<Document>> {
|
||
let doc = window.Document();
|
||
let docloader = DocumentLoader::new(&doc.loader());
|
||
Ok(Document::new_with_proto(
|
||
window,
|
||
proto,
|
||
HasBrowsingContext::No,
|
||
None,
|
||
doc.origin().clone(),
|
||
IsHTMLDocument::NonHTMLDocument,
|
||
None,
|
||
None,
|
||
DocumentActivity::Inactive,
|
||
DocumentSource::NotFromParser,
|
||
docloader,
|
||
None,
|
||
None,
|
||
Default::default(),
|
||
false,
|
||
doc.allow_declarative_shadow_roots(),
|
||
Some(doc.insecure_requests_policy()),
|
||
doc.has_trustworthy_ancestor_or_current_origin(),
|
||
doc.custom_element_reaction_stack(),
|
||
can_gc,
|
||
))
|
||
}
|
||
|
||
// https://w3c.github.io/editing/ActiveDocuments/execCommand.html#querycommandsupported()
|
||
fn QueryCommandSupported(&self, _command: DOMString) -> bool {
|
||
false
|
||
}
|
||
|
||
// https://drafts.csswg.org/cssom/#dom-document-stylesheets
|
||
fn StyleSheets(&self, can_gc: CanGc) -> DomRoot<StyleSheetList> {
|
||
self.stylesheet_list.or_init(|| {
|
||
StyleSheetList::new(
|
||
&self.window,
|
||
StyleSheetListOwner::Document(Dom::from_ref(self)),
|
||
can_gc,
|
||
)
|
||
})
|
||
}
|
||
|
||
// https://dom.spec.whatwg.org/#dom-document-implementation
|
||
fn Implementation(&self, can_gc: CanGc) -> DomRoot<DOMImplementation> {
|
||
self.implementation
|
||
.or_init(|| DOMImplementation::new(self, can_gc))
|
||
}
|
||
|
||
// https://dom.spec.whatwg.org/#dom-document-url
|
||
fn URL(&self) -> USVString {
|
||
USVString(String::from(self.url().as_str()))
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#dom-document-activeelement
|
||
fn GetActiveElement(&self) -> Option<DomRoot<Element>> {
|
||
self.document_or_shadow_root.get_active_element(
|
||
self.get_focused_element(),
|
||
self.GetBody(),
|
||
self.GetDocumentElement(),
|
||
)
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#dom-document-hasfocus
|
||
fn HasFocus(&self) -> bool {
|
||
// <https://html.spec.whatwg.org/multipage/#has-focus-steps>
|
||
//
|
||
// > The has focus steps, given a `Document` object `target`, are as
|
||
// > follows:
|
||
// >
|
||
// > 1. If `target`'s browsing context's top-level browsing context does
|
||
// > not have system focus, then return false.
|
||
|
||
// > 2. Let `candidate` be `target`'s browsing context's top-level
|
||
// > browsing context's active document.
|
||
// >
|
||
// > 3. While true:
|
||
// >
|
||
// > 3.1. If `candidate` is target, then return true.
|
||
// >
|
||
// > 3.2. If the focused area of `candidate` is a browsing context
|
||
// > container with a non-null nested browsing context, then set
|
||
// > `candidate` to the active document of that browsing context
|
||
// > container's nested browsing context.
|
||
// >
|
||
// > 3.3. Otherwise, return false.
|
||
if self.window().parent_info().is_none() {
|
||
// 2 → 3 → (3.1 || ⋯ → 3.3)
|
||
self.is_fully_active()
|
||
} else {
|
||
// 2 → 3 → 3.2 → (⋯ → 3.1 || ⋯ → 3.3)
|
||
self.is_fully_active() && self.has_focus.get()
|
||
}
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#dom-document-domain
|
||
fn Domain(&self) -> DOMString {
|
||
// Step 1.
|
||
if !self.has_browsing_context {
|
||
return DOMString::new();
|
||
}
|
||
|
||
// Step 2.
|
||
match self.origin.effective_domain() {
|
||
// Step 3.
|
||
None => DOMString::new(),
|
||
// Step 4.
|
||
Some(Host::Domain(domain)) => DOMString::from(domain),
|
||
Some(host) => DOMString::from(host.to_string()),
|
||
}
|
||
}
|
||
|
||
/// <https://html.spec.whatwg.org/multipage/#dom-document-domain>
|
||
fn SetDomain(&self, value: DOMString) -> ErrorResult {
|
||
// Step 1.
|
||
if !self.has_browsing_context {
|
||
return Err(Error::Security);
|
||
}
|
||
|
||
// Step 2. If this Document object's active sandboxing flag set has its sandboxed
|
||
// document.domain browsing context flag set, then throw a "SecurityError" DOMException.
|
||
if self.has_active_sandboxing_flag(
|
||
SandboxingFlagSet::SANDBOXED_DOCUMENT_DOMAIN_BROWSING_CONTEXT_FLAG,
|
||
) {
|
||
return Err(Error::Security);
|
||
}
|
||
|
||
// Steps 3-4.
|
||
let effective_domain = match self.origin.effective_domain() {
|
||
Some(effective_domain) => effective_domain,
|
||
None => return Err(Error::Security),
|
||
};
|
||
|
||
// Step 5
|
||
let host = match get_registrable_domain_suffix_of_or_is_equal_to(&value, effective_domain) {
|
||
None => return Err(Error::Security),
|
||
Some(host) => host,
|
||
};
|
||
|
||
// Step 6
|
||
self.origin.set_domain(host);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#dom-document-referrer
|
||
fn Referrer(&self) -> DOMString {
|
||
match self.referrer {
|
||
Some(ref referrer) => DOMString::from(referrer.to_string()),
|
||
None => DOMString::new(),
|
||
}
|
||
}
|
||
|
||
// https://dom.spec.whatwg.org/#dom-document-documenturi
|
||
fn DocumentURI(&self) -> USVString {
|
||
self.URL()
|
||
}
|
||
|
||
// https://dom.spec.whatwg.org/#dom-document-compatmode
|
||
fn CompatMode(&self) -> DOMString {
|
||
DOMString::from(match self.quirks_mode.get() {
|
||
QuirksMode::LimitedQuirks | QuirksMode::NoQuirks => "CSS1Compat",
|
||
QuirksMode::Quirks => "BackCompat",
|
||
})
|
||
}
|
||
|
||
// https://dom.spec.whatwg.org/#dom-document-characterset
|
||
fn CharacterSet(&self) -> DOMString {
|
||
DOMString::from(self.encoding.get().name())
|
||
}
|
||
|
||
// https://dom.spec.whatwg.org/#dom-document-charset
|
||
fn Charset(&self) -> DOMString {
|
||
self.CharacterSet()
|
||
}
|
||
|
||
// https://dom.spec.whatwg.org/#dom-document-inputencoding
|
||
fn InputEncoding(&self) -> DOMString {
|
||
self.CharacterSet()
|
||
}
|
||
|
||
// https://dom.spec.whatwg.org/#dom-document-content_type
|
||
fn ContentType(&self) -> DOMString {
|
||
DOMString::from(self.content_type.to_string())
|
||
}
|
||
|
||
// https://dom.spec.whatwg.org/#dom-document-doctype
|
||
fn GetDoctype(&self) -> Option<DomRoot<DocumentType>> {
|
||
self.upcast::<Node>()
|
||
.children()
|
||
.filter_map(DomRoot::downcast)
|
||
.next()
|
||
}
|
||
|
||
// https://dom.spec.whatwg.org/#dom-document-documentelement
|
||
fn GetDocumentElement(&self) -> Option<DomRoot<Element>> {
|
||
self.upcast::<Node>().child_elements().next()
|
||
}
|
||
|
||
// https://dom.spec.whatwg.org/#dom-document-getelementsbytagname
|
||
fn GetElementsByTagName(
|
||
&self,
|
||
qualified_name: DOMString,
|
||
can_gc: CanGc,
|
||
) -> DomRoot<HTMLCollection> {
|
||
let qualified_name = LocalName::from(qualified_name.str());
|
||
if let Some(entry) = self.tag_map.borrow_mut().get(&qualified_name) {
|
||
return DomRoot::from_ref(entry);
|
||
}
|
||
let result = HTMLCollection::by_qualified_name(
|
||
&self.window,
|
||
self.upcast(),
|
||
qualified_name.clone(),
|
||
can_gc,
|
||
);
|
||
self.tag_map
|
||
.borrow_mut()
|
||
.insert(qualified_name, Dom::from_ref(&*result));
|
||
result
|
||
}
|
||
|
||
// https://dom.spec.whatwg.org/#dom-document-getelementsbytagnamens
|
||
fn GetElementsByTagNameNS(
|
||
&self,
|
||
maybe_ns: Option<DOMString>,
|
||
tag_name: DOMString,
|
||
can_gc: CanGc,
|
||
) -> DomRoot<HTMLCollection> {
|
||
let ns = namespace_from_domstring(maybe_ns);
|
||
let local = LocalName::from(tag_name);
|
||
let qname = QualName::new(None, ns, local);
|
||
if let Some(collection) = self.tagns_map.borrow().get(&qname) {
|
||
return DomRoot::from_ref(collection);
|
||
}
|
||
let result =
|
||
HTMLCollection::by_qual_tag_name(&self.window, self.upcast(), qname.clone(), can_gc);
|
||
self.tagns_map
|
||
.borrow_mut()
|
||
.insert(qname, Dom::from_ref(&*result));
|
||
result
|
||
}
|
||
|
||
// https://dom.spec.whatwg.org/#dom-document-getelementsbyclassname
|
||
fn GetElementsByClassName(&self, classes: DOMString, can_gc: CanGc) -> DomRoot<HTMLCollection> {
|
||
let class_atoms: Vec<Atom> = split_html_space_chars(classes.str())
|
||
.map(Atom::from)
|
||
.collect();
|
||
if let Some(collection) = self.classes_map.borrow().get(&class_atoms) {
|
||
return DomRoot::from_ref(collection);
|
||
}
|
||
let result = HTMLCollection::by_atomic_class_name(
|
||
&self.window,
|
||
self.upcast(),
|
||
class_atoms.clone(),
|
||
can_gc,
|
||
);
|
||
self.classes_map
|
||
.borrow_mut()
|
||
.insert(class_atoms, Dom::from_ref(&*result));
|
||
result
|
||
}
|
||
|
||
// https://dom.spec.whatwg.org/#dom-nonelementparentnode-getelementbyid
|
||
fn GetElementById(&self, id: DOMString) -> Option<DomRoot<Element>> {
|
||
self.get_element_by_id(&Atom::from(id))
|
||
}
|
||
|
||
/// <https://dom.spec.whatwg.org/#dom-document-createelement>
|
||
fn CreateElement(
|
||
&self,
|
||
mut local_name: DOMString,
|
||
options: StringOrElementCreationOptions,
|
||
can_gc: CanGc,
|
||
) -> Fallible<DomRoot<Element>> {
|
||
// Step 1. If localName is not a valid element local name,
|
||
// then throw an "InvalidCharacterError" DOMException.
|
||
if !is_valid_element_local_name(local_name.str()) {
|
||
debug!("Not a valid element name");
|
||
return Err(Error::InvalidCharacter);
|
||
}
|
||
|
||
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.str()))
|
||
},
|
||
};
|
||
Ok(Element::create(
|
||
name,
|
||
is,
|
||
self,
|
||
ElementCreator::ScriptCreated,
|
||
CustomElementCreationMode::Synchronous,
|
||
None,
|
||
can_gc,
|
||
))
|
||
}
|
||
|
||
/// <https://dom.spec.whatwg.org/#dom-document-createelementns>
|
||
fn CreateElementNS(
|
||
&self,
|
||
namespace: Option<DOMString>,
|
||
qualified_name: DOMString,
|
||
options: StringOrElementCreationOptions,
|
||
can_gc: CanGc,
|
||
) -> Fallible<DomRoot<Element>> {
|
||
// Step 1. Let (namespace, prefix, localName) be the result of
|
||
// validating and extracting namespace and qualifiedName given "element".
|
||
let context = domname::Context::Element;
|
||
let (namespace, prefix, local_name) =
|
||
domname::validate_and_extract(namespace, &qualified_name, context)?;
|
||
|
||
// Step 2. Let is be null.
|
||
// Step 3. If options is a dictionary and options["is"] exists, then set is to it.
|
||
let name = QualName::new(prefix, namespace, local_name);
|
||
let is = match options {
|
||
StringOrElementCreationOptions::String(_) => None,
|
||
StringOrElementCreationOptions::ElementCreationOptions(options) => {
|
||
options.is.as_ref().map(|is| LocalName::from(is.str()))
|
||
},
|
||
};
|
||
|
||
// Step 4. Return the result of creating an element given document, localName, namespace, prefix, is, and true.
|
||
Ok(Element::create(
|
||
name,
|
||
is,
|
||
self,
|
||
ElementCreator::ScriptCreated,
|
||
CustomElementCreationMode::Synchronous,
|
||
None,
|
||
can_gc,
|
||
))
|
||
}
|
||
|
||
/// <https://dom.spec.whatwg.org/#dom-document-createattribute>
|
||
fn CreateAttribute(&self, mut local_name: DOMString, can_gc: CanGc) -> Fallible<DomRoot<Attr>> {
|
||
// Step 1. If localName is not a valid attribute local name,
|
||
// then throw an "InvalidCharacterError" DOMException
|
||
if !is_valid_attribute_local_name(local_name.str()) {
|
||
debug!("Not a valid attribute name");
|
||
return Err(Error::InvalidCharacter);
|
||
}
|
||
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>> {
|
||
// Step 1. Let (namespace, prefix, localName) be the result of validating and
|
||
// extracting namespace and qualifiedName given "attribute".
|
||
let context = domname::Context::Attribute;
|
||
let (namespace, prefix, local_name) =
|
||
domname::validate_and_extract(namespace, &qualified_name, context)?;
|
||
let value = AttrValue::String("".to_owned());
|
||
let qualified_name = LocalName::from(qualified_name);
|
||
Ok(Attr::new(
|
||
self,
|
||
local_name,
|
||
value,
|
||
qualified_name,
|
||
namespace,
|
||
prefix,
|
||
None,
|
||
can_gc,
|
||
))
|
||
}
|
||
|
||
// https://dom.spec.whatwg.org/#dom-document-createdocumentfragment
|
||
fn CreateDocumentFragment(&self, can_gc: CanGc) -> DomRoot<DocumentFragment> {
|
||
DocumentFragment::new(self, can_gc)
|
||
}
|
||
|
||
// https://dom.spec.whatwg.org/#dom-document-createtextnode
|
||
fn CreateTextNode(&self, data: DOMString, can_gc: CanGc) -> DomRoot<Text> {
|
||
Text::new(data, self, can_gc)
|
||
}
|
||
|
||
// https://dom.spec.whatwg.org/#dom-document-createcdatasection
|
||
fn CreateCDATASection(
|
||
&self,
|
||
data: DOMString,
|
||
can_gc: CanGc,
|
||
) -> Fallible<DomRoot<CDATASection>> {
|
||
// Step 1
|
||
if self.is_html_document {
|
||
return Err(Error::NotSupported);
|
||
}
|
||
|
||
// Step 2
|
||
if data.contains("]]>") {
|
||
return Err(Error::InvalidCharacter);
|
||
}
|
||
|
||
// Step 3
|
||
Ok(CDATASection::new(data, self, can_gc))
|
||
}
|
||
|
||
// https://dom.spec.whatwg.org/#dom-document-createcomment
|
||
fn CreateComment(&self, data: DOMString, can_gc: CanGc) -> DomRoot<Comment> {
|
||
Comment::new(data, self, None, can_gc)
|
||
}
|
||
|
||
// https://dom.spec.whatwg.org/#dom-document-createprocessinginstruction
|
||
fn CreateProcessingInstruction(
|
||
&self,
|
||
target: DOMString,
|
||
data: DOMString,
|
||
can_gc: CanGc,
|
||
) -> Fallible<DomRoot<ProcessingInstruction>> {
|
||
// Step 1. If target does not match the Name production, then throw an "InvalidCharacterError" DOMException.
|
||
if !matches_name_production(target.str()) {
|
||
return Err(Error::InvalidCharacter);
|
||
}
|
||
|
||
// Step 2.
|
||
if data.contains("?>") {
|
||
return Err(Error::InvalidCharacter);
|
||
}
|
||
|
||
// Step 3.
|
||
Ok(ProcessingInstruction::new(target, data, self, can_gc))
|
||
}
|
||
|
||
// https://dom.spec.whatwg.org/#dom-document-importnode
|
||
fn ImportNode(&self, node: &Node, deep: bool, can_gc: CanGc) -> Fallible<DomRoot<Node>> {
|
||
// Step 1.
|
||
if node.is::<Document>() || node.is::<ShadowRoot>() {
|
||
return Err(Error::NotSupported);
|
||
}
|
||
|
||
// Step 2.
|
||
let clone_children = if deep {
|
||
CloneChildrenFlag::CloneChildren
|
||
} else {
|
||
CloneChildrenFlag::DoNotCloneChildren
|
||
};
|
||
|
||
Ok(Node::clone(node, Some(self), clone_children, can_gc))
|
||
}
|
||
|
||
// https://dom.spec.whatwg.org/#dom-document-adoptnode
|
||
fn AdoptNode(&self, node: &Node, can_gc: CanGc) -> Fallible<DomRoot<Node>> {
|
||
// Step 1.
|
||
if node.is::<Document>() {
|
||
return Err(Error::NotSupported);
|
||
}
|
||
|
||
// Step 2.
|
||
if node.is::<ShadowRoot>() {
|
||
return Err(Error::HierarchyRequest);
|
||
}
|
||
|
||
// Step 3.
|
||
Node::adopt(node, self, can_gc);
|
||
|
||
// Step 4.
|
||
Ok(DomRoot::from_ref(node))
|
||
}
|
||
|
||
// https://dom.spec.whatwg.org/#dom-document-createevent
|
||
fn CreateEvent(&self, mut interface: DOMString, can_gc: CanGc) -> Fallible<DomRoot<Event>> {
|
||
interface.make_ascii_lowercase();
|
||
match interface.str() {
|
||
"beforeunloadevent" => Ok(DomRoot::upcast(BeforeUnloadEvent::new_uninitialized(
|
||
&self.window,
|
||
can_gc,
|
||
))),
|
||
"compositionevent" | "textevent" => Ok(DomRoot::upcast(
|
||
CompositionEvent::new_uninitialized(&self.window, can_gc),
|
||
)),
|
||
"customevent" => Ok(DomRoot::upcast(CustomEvent::new_uninitialized(
|
||
self.window.upcast(),
|
||
can_gc,
|
||
))),
|
||
// FIXME(#25136): devicemotionevent, deviceorientationevent
|
||
// FIXME(#7529): dragevent
|
||
"events" | "event" | "htmlevents" | "svgevents" => {
|
||
Ok(Event::new_uninitialized(self.window.upcast(), can_gc))
|
||
},
|
||
"focusevent" => Ok(DomRoot::upcast(FocusEvent::new_uninitialized(
|
||
&self.window,
|
||
can_gc,
|
||
))),
|
||
"hashchangeevent" => Ok(DomRoot::upcast(HashChangeEvent::new_uninitialized(
|
||
&self.window,
|
||
can_gc,
|
||
))),
|
||
"keyboardevent" => Ok(DomRoot::upcast(KeyboardEvent::new_uninitialized(
|
||
&self.window,
|
||
can_gc,
|
||
))),
|
||
"messageevent" => Ok(DomRoot::upcast(MessageEvent::new_uninitialized(
|
||
self.window.upcast(),
|
||
can_gc,
|
||
))),
|
||
"mouseevent" | "mouseevents" => Ok(DomRoot::upcast(MouseEvent::new_uninitialized(
|
||
&self.window,
|
||
can_gc,
|
||
))),
|
||
"storageevent" => Ok(DomRoot::upcast(StorageEvent::new_uninitialized(
|
||
&self.window,
|
||
"".into(),
|
||
can_gc,
|
||
))),
|
||
"touchevent" => Ok(DomRoot::upcast(DomTouchEvent::new_uninitialized(
|
||
&self.window,
|
||
&TouchList::new(&self.window, &[], can_gc),
|
||
&TouchList::new(&self.window, &[], can_gc),
|
||
&TouchList::new(&self.window, &[], can_gc),
|
||
can_gc,
|
||
))),
|
||
"uievent" | "uievents" => Ok(DomRoot::upcast(UIEvent::new_uninitialized(
|
||
&self.window,
|
||
can_gc,
|
||
))),
|
||
_ => Err(Error::NotSupported),
|
||
}
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#dom-document-lastmodified
|
||
fn LastModified(&self) -> DOMString {
|
||
DOMString::from(self.last_modified.as_ref().cloned().unwrap_or_else(|| {
|
||
// Ideally this would get the local time using `time`, but `time` always fails to get the local
|
||
// timezone on Unix unless the application is single threaded unless the library is explicitly
|
||
// set to "unsound" mode. Maybe that's fine, but it needs more investigation. see
|
||
// https://nvd.nist.gov/vuln/detail/CVE-2020-26235
|
||
// When `time` supports a thread-safe way of getting the local time zone we could use it here.
|
||
Local::now().format("%m/%d/%Y %H:%M:%S").to_string()
|
||
}))
|
||
}
|
||
|
||
// https://dom.spec.whatwg.org/#dom-document-createrange
|
||
fn CreateRange(&self, can_gc: CanGc) -> DomRoot<Range> {
|
||
Range::new_with_doc(self, None, can_gc)
|
||
}
|
||
|
||
// https://dom.spec.whatwg.org/#dom-document-createnodeiteratorroot-whattoshow-filter
|
||
fn CreateNodeIterator(
|
||
&self,
|
||
root: &Node,
|
||
what_to_show: u32,
|
||
filter: Option<Rc<NodeFilter>>,
|
||
can_gc: CanGc,
|
||
) -> DomRoot<NodeIterator> {
|
||
NodeIterator::new(self, root, what_to_show, filter, can_gc)
|
||
}
|
||
|
||
// https://dom.spec.whatwg.org/#dom-document-createtreewalker
|
||
fn CreateTreeWalker(
|
||
&self,
|
||
root: &Node,
|
||
what_to_show: u32,
|
||
filter: Option<Rc<NodeFilter>>,
|
||
) -> DomRoot<TreeWalker> {
|
||
TreeWalker::new(self, root, what_to_show, filter)
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#document.title
|
||
fn Title(&self) -> DOMString {
|
||
self.title().unwrap_or_else(|| DOMString::from(""))
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#document.title
|
||
fn SetTitle(&self, title: DOMString, can_gc: CanGc) {
|
||
let root = match self.GetDocumentElement() {
|
||
Some(root) => root,
|
||
None => return,
|
||
};
|
||
|
||
let node = if root.namespace() == &ns!(svg) && root.local_name() == &local_name!("svg") {
|
||
let elem = root.upcast::<Node>().child_elements().find(|node| {
|
||
node.namespace() == &ns!(svg) && node.local_name() == &local_name!("title")
|
||
});
|
||
match elem {
|
||
Some(elem) => DomRoot::upcast::<Node>(elem),
|
||
None => {
|
||
let name = QualName::new(None, ns!(svg), local_name!("title"));
|
||
let elem = Element::create(
|
||
name,
|
||
None,
|
||
self,
|
||
ElementCreator::ScriptCreated,
|
||
CustomElementCreationMode::Synchronous,
|
||
None,
|
||
can_gc,
|
||
);
|
||
let parent = root.upcast::<Node>();
|
||
let child = elem.upcast::<Node>();
|
||
parent
|
||
.InsertBefore(child, parent.GetFirstChild().as_deref(), can_gc)
|
||
.unwrap()
|
||
},
|
||
}
|
||
} else if root.namespace() == &ns!(html) {
|
||
let elem = root
|
||
.upcast::<Node>()
|
||
.traverse_preorder(ShadowIncluding::No)
|
||
.find(|node| node.is::<HTMLTitleElement>());
|
||
match elem {
|
||
Some(elem) => elem,
|
||
None => match self.GetHead() {
|
||
Some(head) => {
|
||
let name = QualName::new(None, ns!(html), local_name!("title"));
|
||
let elem = Element::create(
|
||
name,
|
||
None,
|
||
self,
|
||
ElementCreator::ScriptCreated,
|
||
CustomElementCreationMode::Synchronous,
|
||
None,
|
||
can_gc,
|
||
);
|
||
head.upcast::<Node>()
|
||
.AppendChild(elem.upcast(), can_gc)
|
||
.unwrap()
|
||
},
|
||
None => return,
|
||
},
|
||
}
|
||
} else {
|
||
return;
|
||
};
|
||
|
||
node.set_text_content_for_element(Some(title), can_gc);
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#dom-document-head
|
||
fn GetHead(&self) -> Option<DomRoot<HTMLHeadElement>> {
|
||
self.get_html_element().and_then(|root| {
|
||
root.upcast::<Node>()
|
||
.children()
|
||
.filter_map(DomRoot::downcast)
|
||
.next()
|
||
})
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#dom-document-currentscript
|
||
fn GetCurrentScript(&self) -> Option<DomRoot<HTMLScriptElement>> {
|
||
self.current_script.get()
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#dom-document-body
|
||
fn GetBody(&self) -> Option<DomRoot<HTMLElement>> {
|
||
self.get_html_element().and_then(|root| {
|
||
let node = root.upcast::<Node>();
|
||
node.children()
|
||
.find(|child| {
|
||
matches!(
|
||
child.type_id(),
|
||
NodeTypeId::Element(ElementTypeId::HTMLElement(
|
||
HTMLElementTypeId::HTMLBodyElement,
|
||
)) | NodeTypeId::Element(ElementTypeId::HTMLElement(
|
||
HTMLElementTypeId::HTMLFrameSetElement,
|
||
))
|
||
)
|
||
})
|
||
.map(|node| DomRoot::downcast(node).unwrap())
|
||
})
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#dom-document-body
|
||
fn SetBody(&self, new_body: Option<&HTMLElement>, can_gc: CanGc) -> ErrorResult {
|
||
// Step 1.
|
||
let new_body = match new_body {
|
||
Some(new_body) => new_body,
|
||
None => return Err(Error::HierarchyRequest),
|
||
};
|
||
|
||
let node = new_body.upcast::<Node>();
|
||
match node.type_id() {
|
||
NodeTypeId::Element(ElementTypeId::HTMLElement(HTMLElementTypeId::HTMLBodyElement)) |
|
||
NodeTypeId::Element(ElementTypeId::HTMLElement(
|
||
HTMLElementTypeId::HTMLFrameSetElement,
|
||
)) => {},
|
||
_ => return Err(Error::HierarchyRequest),
|
||
}
|
||
|
||
// Step 2.
|
||
let old_body = self.GetBody();
|
||
if old_body.as_deref() == Some(new_body) {
|
||
return Ok(());
|
||
}
|
||
|
||
match (self.GetDocumentElement(), &old_body) {
|
||
// Step 3.
|
||
(Some(ref root), Some(child)) => {
|
||
let root = root.upcast::<Node>();
|
||
root.ReplaceChild(new_body.upcast(), child.upcast(), can_gc)
|
||
.unwrap();
|
||
},
|
||
|
||
// Step 4.
|
||
(None, _) => return Err(Error::HierarchyRequest),
|
||
|
||
// Step 5.
|
||
(Some(ref root), &None) => {
|
||
let root = root.upcast::<Node>();
|
||
root.AppendChild(new_body.upcast(), can_gc).unwrap();
|
||
},
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#dom-document-getelementsbyname
|
||
fn GetElementsByName(&self, name: DOMString, can_gc: CanGc) -> DomRoot<NodeList> {
|
||
NodeList::new_elements_by_name_list(self.window(), self, name, can_gc)
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#dom-document-images
|
||
fn Images(&self, can_gc: CanGc) -> DomRoot<HTMLCollection> {
|
||
self.images.or_init(|| {
|
||
HTMLCollection::new_with_filter_fn(
|
||
&self.window,
|
||
self.upcast(),
|
||
|element, _| element.is::<HTMLImageElement>(),
|
||
can_gc,
|
||
)
|
||
})
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#dom-document-embeds
|
||
fn Embeds(&self, can_gc: CanGc) -> DomRoot<HTMLCollection> {
|
||
self.embeds.or_init(|| {
|
||
HTMLCollection::new_with_filter_fn(
|
||
&self.window,
|
||
self.upcast(),
|
||
|element, _| element.is::<HTMLEmbedElement>(),
|
||
can_gc,
|
||
)
|
||
})
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#dom-document-plugins
|
||
fn Plugins(&self, can_gc: CanGc) -> DomRoot<HTMLCollection> {
|
||
self.Embeds(can_gc)
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#dom-document-links
|
||
fn Links(&self, can_gc: CanGc) -> DomRoot<HTMLCollection> {
|
||
self.links.or_init(|| {
|
||
HTMLCollection::new_with_filter_fn(
|
||
&self.window,
|
||
self.upcast(),
|
||
|element, _| {
|
||
(element.is::<HTMLAnchorElement>() || element.is::<HTMLAreaElement>()) &&
|
||
element.has_attribute(&local_name!("href"))
|
||
},
|
||
can_gc,
|
||
)
|
||
})
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#dom-document-forms
|
||
fn Forms(&self, can_gc: CanGc) -> DomRoot<HTMLCollection> {
|
||
self.forms.or_init(|| {
|
||
HTMLCollection::new_with_filter_fn(
|
||
&self.window,
|
||
self.upcast(),
|
||
|element, _| element.is::<HTMLFormElement>(),
|
||
can_gc,
|
||
)
|
||
})
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#dom-document-scripts
|
||
fn Scripts(&self, can_gc: CanGc) -> DomRoot<HTMLCollection> {
|
||
self.scripts.or_init(|| {
|
||
HTMLCollection::new_with_filter_fn(
|
||
&self.window,
|
||
self.upcast(),
|
||
|element, _| element.is::<HTMLScriptElement>(),
|
||
can_gc,
|
||
)
|
||
})
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#dom-document-anchors
|
||
fn Anchors(&self, can_gc: CanGc) -> DomRoot<HTMLCollection> {
|
||
self.anchors.or_init(|| {
|
||
HTMLCollection::new_with_filter_fn(
|
||
&self.window,
|
||
self.upcast(),
|
||
|element, _| {
|
||
element.is::<HTMLAnchorElement>() && element.has_attribute(&local_name!("href"))
|
||
},
|
||
can_gc,
|
||
)
|
||
})
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#dom-document-applets
|
||
fn Applets(&self, can_gc: CanGc) -> DomRoot<HTMLCollection> {
|
||
self.applets
|
||
.or_init(|| HTMLCollection::always_empty(&self.window, self.upcast(), can_gc))
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#dom-document-location
|
||
fn GetLocation(&self) -> Option<DomRoot<Location>> {
|
||
if self.is_fully_active() {
|
||
Some(self.window.Location())
|
||
} else {
|
||
None
|
||
}
|
||
}
|
||
|
||
// https://dom.spec.whatwg.org/#dom-parentnode-children
|
||
fn Children(&self, can_gc: CanGc) -> DomRoot<HTMLCollection> {
|
||
HTMLCollection::children(&self.window, self.upcast(), can_gc)
|
||
}
|
||
|
||
// https://dom.spec.whatwg.org/#dom-parentnode-firstelementchild
|
||
fn GetFirstElementChild(&self) -> Option<DomRoot<Element>> {
|
||
self.upcast::<Node>().child_elements().next()
|
||
}
|
||
|
||
// https://dom.spec.whatwg.org/#dom-parentnode-lastelementchild
|
||
fn GetLastElementChild(&self) -> Option<DomRoot<Element>> {
|
||
self.upcast::<Node>()
|
||
.rev_children()
|
||
.filter_map(DomRoot::downcast)
|
||
.next()
|
||
}
|
||
|
||
// https://dom.spec.whatwg.org/#dom-parentnode-childelementcount
|
||
fn ChildElementCount(&self) -> u32 {
|
||
self.upcast::<Node>().child_elements().count() as u32
|
||
}
|
||
|
||
// https://dom.spec.whatwg.org/#dom-parentnode-prepend
|
||
fn Prepend(&self, nodes: Vec<NodeOrString>, can_gc: CanGc) -> ErrorResult {
|
||
self.upcast::<Node>().prepend(nodes, can_gc)
|
||
}
|
||
|
||
// https://dom.spec.whatwg.org/#dom-parentnode-append
|
||
fn Append(&self, nodes: Vec<NodeOrString>, can_gc: CanGc) -> ErrorResult {
|
||
self.upcast::<Node>().append(nodes, can_gc)
|
||
}
|
||
|
||
// https://dom.spec.whatwg.org/#dom-parentnode-replacechildren
|
||
fn ReplaceChildren(&self, nodes: Vec<NodeOrString>, can_gc: CanGc) -> ErrorResult {
|
||
self.upcast::<Node>().replace_children(nodes, can_gc)
|
||
}
|
||
|
||
// https://dom.spec.whatwg.org/#dom-parentnode-queryselector
|
||
fn QuerySelector(&self, selectors: DOMString) -> Fallible<Option<DomRoot<Element>>> {
|
||
let root = self.upcast::<Node>();
|
||
root.query_selector(selectors)
|
||
}
|
||
|
||
// https://dom.spec.whatwg.org/#dom-parentnode-queryselectorall
|
||
fn QuerySelectorAll(&self, selectors: DOMString) -> Fallible<DomRoot<NodeList>> {
|
||
let root = self.upcast::<Node>();
|
||
root.query_selector_all(selectors)
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#dom-document-readystate
|
||
fn ReadyState(&self) -> DocumentReadyState {
|
||
self.ready_state.get()
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#dom-document-defaultview
|
||
fn GetDefaultView(&self) -> Option<DomRoot<Window>> {
|
||
if self.has_browsing_context {
|
||
Some(DomRoot::from_ref(&*self.window))
|
||
} else {
|
||
None
|
||
}
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#dom-document-cookie
|
||
fn GetCookie(&self) -> Fallible<DOMString> {
|
||
if self.is_cookie_averse() {
|
||
return Ok(DOMString::new());
|
||
}
|
||
|
||
if !self.origin.is_tuple() {
|
||
return Err(Error::Security);
|
||
}
|
||
|
||
let url = self.url();
|
||
let (tx, rx) = profile_ipc::channel(self.global().time_profiler_chan().clone()).unwrap();
|
||
let _ = self
|
||
.window
|
||
.as_global_scope()
|
||
.resource_threads()
|
||
.send(GetCookiesForUrl(url, tx, NonHTTP));
|
||
let cookies = rx.recv().unwrap();
|
||
Ok(cookies.map_or(DOMString::new(), DOMString::from))
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#dom-document-cookie
|
||
fn SetCookie(&self, cookie: DOMString) -> ErrorResult {
|
||
if self.is_cookie_averse() {
|
||
return Ok(());
|
||
}
|
||
|
||
if !self.origin.is_tuple() {
|
||
return Err(Error::Security);
|
||
}
|
||
|
||
let cookies = if let Some(cookie) = Cookie::parse(cookie.to_string()).ok().map(Serde) {
|
||
vec![cookie]
|
||
} else {
|
||
vec![]
|
||
};
|
||
|
||
let _ = self
|
||
.window
|
||
.as_global_scope()
|
||
.resource_threads()
|
||
.send(SetCookiesForUrl(self.url(), cookies, NonHTTP));
|
||
Ok(())
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#dom-document-bgcolor
|
||
fn BgColor(&self) -> DOMString {
|
||
self.get_body_attribute(&local_name!("bgcolor"))
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#dom-document-bgcolor
|
||
fn SetBgColor(&self, value: DOMString, can_gc: CanGc) {
|
||
self.set_body_attribute(&local_name!("bgcolor"), value, can_gc)
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#dom-document-fgcolor
|
||
fn FgColor(&self) -> DOMString {
|
||
self.get_body_attribute(&local_name!("text"))
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#dom-document-fgcolor
|
||
fn SetFgColor(&self, value: DOMString, can_gc: CanGc) {
|
||
self.set_body_attribute(&local_name!("text"), value, can_gc)
|
||
}
|
||
|
||
/// <https://html.spec.whatwg.org/multipage/#dom-tree-accessors:dom-document-nameditem-filter>
|
||
fn NamedGetter(&self, name: DOMString, can_gc: CanGc) -> Option<NamedPropertyValue> {
|
||
if name.is_empty() {
|
||
return None;
|
||
}
|
||
let name = Atom::from(name);
|
||
|
||
// Step 1. Let elements be the list of named elements with the name name that are in a document tree
|
||
// with the Document as their root.
|
||
let elements_with_name = self.get_elements_with_name(&name);
|
||
let name_iter = elements_with_name
|
||
.iter()
|
||
.filter(|elem| is_named_element_with_name_attribute(elem));
|
||
let elements_with_id = self.get_elements_with_id(&name);
|
||
let id_iter = elements_with_id
|
||
.iter()
|
||
.filter(|elem| is_named_element_with_id_attribute(elem));
|
||
let mut elements = name_iter.chain(id_iter);
|
||
|
||
// Step 2. If elements has only one element, and that element is an iframe element,
|
||
// and that iframe element's content navigable is not null, then return the active
|
||
// WindowProxy of the element's content navigable.
|
||
|
||
// NOTE: We have to check if all remaining elements are equal to the first, since
|
||
// the same element may appear in both lists.
|
||
let first = elements.next()?;
|
||
if elements.all(|other| first == other) {
|
||
if let Some(nested_window_proxy) = first
|
||
.downcast::<HTMLIFrameElement>()
|
||
.and_then(|iframe| iframe.GetContentWindow())
|
||
{
|
||
return Some(NamedPropertyValue::WindowProxy(nested_window_proxy));
|
||
}
|
||
|
||
// Step 3. Otherwise, if elements has only one element, return that element.
|
||
return Some(NamedPropertyValue::Element(DomRoot::from_ref(first)));
|
||
}
|
||
|
||
// Step 4. Otherwise, return an HTMLCollection rooted at the Document node,
|
||
// whose filter matches only named elements with the name name.
|
||
#[derive(JSTraceable, MallocSizeOf)]
|
||
struct DocumentNamedGetter {
|
||
#[no_trace]
|
||
name: Atom,
|
||
}
|
||
impl CollectionFilter for DocumentNamedGetter {
|
||
fn filter(&self, elem: &Element, _root: &Node) -> bool {
|
||
let type_ = match elem.upcast::<Node>().type_id() {
|
||
NodeTypeId::Element(ElementTypeId::HTMLElement(type_)) => type_,
|
||
_ => return false,
|
||
};
|
||
match type_ {
|
||
HTMLElementTypeId::HTMLFormElement | HTMLElementTypeId::HTMLIFrameElement => {
|
||
elem.get_name().as_ref() == Some(&self.name)
|
||
},
|
||
HTMLElementTypeId::HTMLImageElement => elem.get_name().is_some_and(|name| {
|
||
name == *self.name ||
|
||
!name.is_empty() && elem.get_id().as_ref() == Some(&self.name)
|
||
}),
|
||
// TODO handle <embed> and <object>; these depend on whether the element is
|
||
// “exposed”, a concept that doesn’t fully make sense until embed/object
|
||
// behaviour is actually implemented
|
||
_ => false,
|
||
}
|
||
}
|
||
}
|
||
let collection = HTMLCollection::create(
|
||
self.window(),
|
||
self.upcast(),
|
||
Box::new(DocumentNamedGetter { name }),
|
||
can_gc,
|
||
);
|
||
Some(NamedPropertyValue::HTMLCollection(collection))
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#dom-tree-accessors:supported-property-names
|
||
fn SupportedPropertyNames(&self) -> Vec<DOMString> {
|
||
let mut names_with_first_named_element_map: HashMap<&Atom, &Element> = HashMap::new();
|
||
|
||
let name_map = self.name_map.borrow();
|
||
for (name, elements) in &(name_map).0 {
|
||
if name.is_empty() {
|
||
continue;
|
||
}
|
||
let mut name_iter = elements
|
||
.iter()
|
||
.filter(|elem| is_named_element_with_name_attribute(elem));
|
||
if let Some(first) = name_iter.next() {
|
||
names_with_first_named_element_map.insert(name, first);
|
||
}
|
||
}
|
||
let id_map = self.id_map.borrow();
|
||
for (id, elements) in &(id_map).0 {
|
||
if id.is_empty() {
|
||
continue;
|
||
}
|
||
let mut id_iter = elements
|
||
.iter()
|
||
.filter(|elem| is_named_element_with_id_attribute(elem));
|
||
if let Some(first) = id_iter.next() {
|
||
match names_with_first_named_element_map.entry(id) {
|
||
Vacant(entry) => drop(entry.insert(first)),
|
||
Occupied(mut entry) => {
|
||
if first.upcast::<Node>().is_before(entry.get().upcast()) {
|
||
*entry.get_mut() = first;
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
let mut names_with_first_named_element_vec: Vec<(&Atom, &Element)> =
|
||
names_with_first_named_element_map
|
||
.iter()
|
||
.map(|(k, v)| (*k, *v))
|
||
.collect();
|
||
names_with_first_named_element_vec.sort_unstable_by(|a, b| {
|
||
if a.1 == b.1 {
|
||
// This can happen if an img has an id different from its name,
|
||
// spec does not say which string to put first.
|
||
a.0.cmp(b.0)
|
||
} else if a.1.upcast::<Node>().is_before(b.1.upcast::<Node>()) {
|
||
Ordering::Less
|
||
} else {
|
||
Ordering::Greater
|
||
}
|
||
});
|
||
|
||
names_with_first_named_element_vec
|
||
.iter()
|
||
.map(|(k, _v)| DOMString::from(&***k))
|
||
.collect()
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#dom-document-clear
|
||
fn Clear(&self) {
|
||
// This method intentionally does nothing
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#dom-document-captureevents
|
||
fn CaptureEvents(&self) {
|
||
// This method intentionally does nothing
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#dom-document-releaseevents
|
||
fn ReleaseEvents(&self) {
|
||
// This method intentionally does nothing
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#globaleventhandlers
|
||
global_event_handlers!();
|
||
|
||
// https://html.spec.whatwg.org/multipage/#handler-onreadystatechange
|
||
event_handler!(
|
||
readystatechange,
|
||
GetOnreadystatechange,
|
||
SetOnreadystatechange
|
||
);
|
||
|
||
// https://drafts.csswg.org/cssom-view/#dom-document-elementfrompoint
|
||
fn ElementFromPoint(&self, x: Finite<f64>, y: Finite<f64>) -> Option<DomRoot<Element>> {
|
||
self.document_or_shadow_root.element_from_point(
|
||
x,
|
||
y,
|
||
self.GetDocumentElement(),
|
||
self.has_browsing_context,
|
||
)
|
||
}
|
||
|
||
// https://drafts.csswg.org/cssom-view/#dom-document-elementsfrompoint
|
||
fn ElementsFromPoint(&self, x: Finite<f64>, y: Finite<f64>) -> Vec<DomRoot<Element>> {
|
||
self.document_or_shadow_root.elements_from_point(
|
||
x,
|
||
y,
|
||
self.GetDocumentElement(),
|
||
self.has_browsing_context,
|
||
)
|
||
}
|
||
|
||
/// <https://drafts.csswg.org/cssom-view/#dom-document-scrollingelement>
|
||
fn GetScrollingElement(&self) -> Option<DomRoot<Element>> {
|
||
// Step 1. If the Document is in quirks mode, follow these steps:
|
||
if self.quirks_mode() == QuirksMode::Quirks {
|
||
// Step 1.1. If the body element exists,
|
||
if let Some(ref body) = self.GetBody() {
|
||
let e = body.upcast::<Element>();
|
||
// and it is not potentially scrollable, return the body element and abort these steps.
|
||
// For this purpose, a value of overflow:clip on the the body element’s parent element
|
||
// must be treated as overflow:hidden.
|
||
if !e.is_potentially_scrollable_body_for_scrolling_element() {
|
||
return Some(DomRoot::from_ref(e));
|
||
}
|
||
}
|
||
|
||
// Step 1.2. Return null and abort these steps.
|
||
return None;
|
||
}
|
||
|
||
// Step 2. If there is a root element, return the root element and abort these steps.
|
||
// Step 3. Return null.
|
||
self.GetDocumentElement()
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#dom-document-open
|
||
fn Open(
|
||
&self,
|
||
_unused1: Option<DOMString>,
|
||
_unused2: Option<DOMString>,
|
||
can_gc: CanGc,
|
||
) -> Fallible<DomRoot<Document>> {
|
||
// Step 1
|
||
if !self.is_html_document() {
|
||
return Err(Error::InvalidState);
|
||
}
|
||
|
||
// Step 2
|
||
if self.throw_on_dynamic_markup_insertion_counter.get() > 0 {
|
||
return Err(Error::InvalidState);
|
||
}
|
||
|
||
// Step 3
|
||
let entry_responsible_document = GlobalScope::entry().as_window().Document();
|
||
|
||
// Step 4
|
||
if !self.origin.same_origin(&entry_responsible_document.origin) {
|
||
return Err(Error::Security);
|
||
}
|
||
|
||
// Step 5
|
||
if self
|
||
.get_current_parser()
|
||
.is_some_and(|parser| parser.is_active())
|
||
{
|
||
return Ok(DomRoot::from_ref(self));
|
||
}
|
||
|
||
// Step 6
|
||
if self.is_prompting_or_unloading() {
|
||
return Ok(DomRoot::from_ref(self));
|
||
}
|
||
|
||
// Step 7
|
||
if self.active_parser_was_aborted.get() {
|
||
return Ok(DomRoot::from_ref(self));
|
||
}
|
||
|
||
// TODO: prompt to unload.
|
||
// TODO: set unload_event_start and unload_event_end
|
||
|
||
self.window().set_navigation_start();
|
||
|
||
// Step 8
|
||
// TODO: https://github.com/servo/servo/issues/21937
|
||
if self.has_browsing_context() {
|
||
// spec says "stop document loading",
|
||
// which is a process that does more than just abort
|
||
self.abort(can_gc);
|
||
}
|
||
|
||
// Step 9
|
||
for node in self
|
||
.upcast::<Node>()
|
||
.traverse_preorder(ShadowIncluding::Yes)
|
||
{
|
||
node.upcast::<EventTarget>().remove_all_listeners();
|
||
}
|
||
|
||
// Step 10
|
||
if self.window.Document() == DomRoot::from_ref(self) {
|
||
self.window.upcast::<EventTarget>().remove_all_listeners();
|
||
}
|
||
|
||
// Step 11. Replace all with null within document.
|
||
Node::replace_all(None, self.upcast::<Node>(), can_gc);
|
||
|
||
// Specs and tests are in a state of flux about whether
|
||
// we want to clear the selection when we remove the contents;
|
||
// WPT selection/Document-open.html wants us to not clear it
|
||
// as of Feb 1 2020
|
||
|
||
// Step 12. If document is fully active, then:
|
||
if self.is_fully_active() {
|
||
// Step 12.1. Let newURL be a copy of entryDocument's URL.
|
||
let mut new_url = entry_responsible_document.url();
|
||
|
||
// Step 12.2. If entryDocument is not document, then set newURL's fragment to null.
|
||
if entry_responsible_document != DomRoot::from_ref(self) {
|
||
new_url.set_fragment(None);
|
||
}
|
||
|
||
// Step 12.3. Run the URL and history update steps with document and newURL.
|
||
// TODO: https://github.com/servo/servo/issues/21939
|
||
self.set_url(new_url);
|
||
}
|
||
|
||
// Step 13. Set document's is initial about:blank to false.
|
||
self.is_initial_about_blank.set(false);
|
||
|
||
// Step 14. If document's iframe load in progress flag is set, then set document's mute
|
||
// iframe load flag.
|
||
// TODO: https://github.com/servo/servo/issues/21938
|
||
|
||
// Step 15: Set document to no-quirks mode.
|
||
self.set_quirks_mode(QuirksMode::NoQuirks);
|
||
|
||
// Step 16. Create a new HTML parser and associate it with document. This is a
|
||
// script-created parser (meaning that it can be closed by the document.open() and
|
||
// document.close() methods, and that the tokenizer will wait for an explicit call to
|
||
// document.close() before emitting an end-of-file token). The encoding confidence is
|
||
// irrelevant.
|
||
let resource_threads = self.window.as_global_scope().resource_threads().clone();
|
||
*self.loader.borrow_mut() =
|
||
DocumentLoader::new_with_threads(resource_threads, Some(self.url()));
|
||
ServoParser::parse_html_script_input(self, self.url());
|
||
|
||
// Step 17. Set the insertion point to point at just before the end of the input stream
|
||
// (which at this point will be empty).
|
||
// Handled when creating the parser in step 16
|
||
|
||
// Step 18. Update the current document readiness of document to "loading".
|
||
self.ready_state.set(DocumentReadyState::Loading);
|
||
|
||
// Step 19. Return document.
|
||
Ok(DomRoot::from_ref(self))
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#dom-document-open-window
|
||
fn Open_(
|
||
&self,
|
||
url: USVString,
|
||
target: DOMString,
|
||
features: DOMString,
|
||
can_gc: CanGc,
|
||
) -> Fallible<Option<DomRoot<WindowProxy>>> {
|
||
self.browsing_context()
|
||
.ok_or(Error::InvalidAccess)?
|
||
.open(url, target, features, can_gc)
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#dom-document-write
|
||
fn Write(&self, text: Vec<TrustedHTMLOrString>, can_gc: CanGc) -> ErrorResult {
|
||
// The document.write(...text) method steps are to run the document write steps
|
||
// with this, text, false, and "Document write".
|
||
self.write(text, false, "Document", "write", can_gc)
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#dom-document-writeln
|
||
fn Writeln(&self, text: Vec<TrustedHTMLOrString>, can_gc: CanGc) -> ErrorResult {
|
||
// The document.writeln(...text) method steps are to run the document write steps
|
||
// with this, text, true, and "Document writeln".
|
||
self.write(text, true, "Document", "writeln", can_gc)
|
||
}
|
||
|
||
// https://html.spec.whatwg.org/multipage/#dom-document-close
|
||
fn Close(&self, can_gc: CanGc) -> ErrorResult {
|
||
if !self.is_html_document() {
|
||
// Step 1.
|
||
return Err(Error::InvalidState);
|
||
}
|
||
|
||
// Step 2.
|
||
if self.throw_on_dynamic_markup_insertion_counter.get() > 0 {
|
||
return Err(Error::InvalidState);
|
||
}
|
||
|
||
let parser = match self.get_current_parser() {
|
||
Some(ref parser) if parser.is_script_created() => DomRoot::from_ref(&**parser),
|
||
_ => {
|
||
// Step 3.
|
||
return Ok(());
|
||
},
|
||
};
|
||
|
||
// Step 4-6.
|
||
parser.close(can_gc);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
// https://fullscreen.spec.whatwg.org/#handler-document-onfullscreenerror
|
||
event_handler!(fullscreenerror, GetOnfullscreenerror, SetOnfullscreenerror);
|
||
|
||
// https://fullscreen.spec.whatwg.org/#handler-document-onfullscreenchange
|
||
event_handler!(
|
||
fullscreenchange,
|
||
GetOnfullscreenchange,
|
||
SetOnfullscreenchange
|
||
);
|
||
|
||
// https://fullscreen.spec.whatwg.org/#dom-document-fullscreenenabled
|
||
fn FullscreenEnabled(&self) -> bool {
|
||
self.get_allow_fullscreen()
|
||
}
|
||
|
||
// https://fullscreen.spec.whatwg.org/#dom-document-fullscreen
|
||
fn Fullscreen(&self) -> bool {
|
||
self.fullscreen_element.get().is_some()
|
||
}
|
||
|
||
// https://fullscreen.spec.whatwg.org/#dom-document-fullscreenelement
|
||
fn GetFullscreenElement(&self) -> Option<DomRoot<Element>> {
|
||
// TODO ShadowRoot
|
||
self.fullscreen_element.get()
|
||
}
|
||
|
||
// https://fullscreen.spec.whatwg.org/#dom-document-exitfullscreen
|
||
fn ExitFullscreen(&self, can_gc: CanGc) -> Rc<Promise> {
|
||
self.exit_fullscreen(can_gc)
|
||
}
|
||
|
||
// check-tidy: no specs after this line
|
||
// Servo only API to get an instance of the controls of a specific
|
||
// media element matching the given id.
|
||
fn ServoGetMediaControls(&self, id: DOMString) -> Fallible<DomRoot<ShadowRoot>> {
|
||
match self.media_controls.borrow().get(id.str()) {
|
||
Some(m) => Ok(DomRoot::from_ref(m)),
|
||
None => Err(Error::InvalidAccess),
|
||
}
|
||
}
|
||
|
||
// https://w3c.github.io/selection-api/#dom-document-getselection
|
||
fn GetSelection(&self, can_gc: CanGc) -> Option<DomRoot<Selection>> {
|
||
if self.has_browsing_context {
|
||
Some(self.selection.or_init(|| Selection::new(self, can_gc)))
|
||
} else {
|
||
None
|
||
}
|
||
}
|
||
|
||
// https://drafts.csswg.org/css-font-loading/#font-face-source
|
||
fn Fonts(&self, can_gc: CanGc) -> DomRoot<FontFaceSet> {
|
||
self.fonts
|
||
.or_init(|| FontFaceSet::new(&self.global(), None, can_gc))
|
||
}
|
||
|
||
/// <https://html.spec.whatwg.org/multipage/#dom-document-hidden>
|
||
fn Hidden(&self) -> bool {
|
||
self.visibility_state.get() == DocumentVisibilityState::Hidden
|
||
}
|
||
|
||
/// <https://html.spec.whatwg.org/multipage/#dom-document-visibilitystate>
|
||
fn VisibilityState(&self) -> DocumentVisibilityState {
|
||
self.visibility_state.get()
|
||
}
|
||
|
||
fn CreateExpression(
|
||
&self,
|
||
expression: DOMString,
|
||
resolver: Option<Rc<XPathNSResolver>>,
|
||
can_gc: CanGc,
|
||
) -> Fallible<DomRoot<super::types::XPathExpression>> {
|
||
let global = self.global();
|
||
let window = global.as_window();
|
||
let evaluator = XPathEvaluator::new(window, None, can_gc);
|
||
XPathEvaluatorMethods::<crate::DomTypeHolder>::CreateExpression(
|
||
&*evaluator,
|
||
expression,
|
||
resolver,
|
||
can_gc,
|
||
)
|
||
}
|
||
|
||
fn CreateNSResolver(&self, node_resolver: &Node, can_gc: CanGc) -> DomRoot<Node> {
|
||
let global = self.global();
|
||
let window = global.as_window();
|
||
let evaluator = XPathEvaluator::new(window, None, can_gc);
|
||
XPathEvaluatorMethods::<crate::DomTypeHolder>::CreateNSResolver(&*evaluator, node_resolver)
|
||
}
|
||
|
||
fn Evaluate(
|
||
&self,
|
||
expression: DOMString,
|
||
context_node: &Node,
|
||
resolver: Option<Rc<XPathNSResolver>>,
|
||
type_: u16,
|
||
result: Option<&super::types::XPathResult>,
|
||
can_gc: CanGc,
|
||
) -> Fallible<DomRoot<super::types::XPathResult>> {
|
||
let global = self.global();
|
||
let window = global.as_window();
|
||
let evaluator = XPathEvaluator::new(window, None, can_gc);
|
||
XPathEvaluatorMethods::<crate::DomTypeHolder>::Evaluate(
|
||
&*evaluator,
|
||
expression,
|
||
context_node,
|
||
resolver,
|
||
type_,
|
||
result,
|
||
can_gc,
|
||
)
|
||
}
|
||
|
||
/// <https://drafts.csswg.org/cssom/#dom-documentorshadowroot-adoptedstylesheets>
|
||
fn AdoptedStyleSheets(&self, context: JSContext, can_gc: CanGc, retval: MutableHandleValue) {
|
||
self.adopted_stylesheets_frozen_types.get_or_init(
|
||
|| {
|
||
self.adopted_stylesheets
|
||
.borrow()
|
||
.clone()
|
||
.iter()
|
||
.map(|sheet| sheet.as_rooted())
|
||
.collect()
|
||
},
|
||
context,
|
||
retval,
|
||
can_gc,
|
||
);
|
||
}
|
||
|
||
/// <https://drafts.csswg.org/cssom/#dom-documentorshadowroot-adoptedstylesheets>
|
||
fn SetAdoptedStyleSheets(&self, context: JSContext, val: HandleValue) -> ErrorResult {
|
||
let result = DocumentOrShadowRoot::set_adopted_stylesheet_from_jsval(
|
||
context,
|
||
self.adopted_stylesheets.borrow_mut().as_mut(),
|
||
val,
|
||
&StyleSheetListOwner::Document(Dom::from_ref(self)),
|
||
);
|
||
|
||
// If update is successful, clear the FrozenArray cache.
|
||
if result.is_ok() {
|
||
self.adopted_stylesheets_frozen_types.clear()
|
||
}
|
||
|
||
result
|
||
}
|
||
}
|
||
|
||
fn update_with_current_instant(marker: &Cell<Option<CrossProcessInstant>>) {
|
||
if marker.get().is_none() {
|
||
marker.set(Some(CrossProcessInstant::now()))
|
||
}
|
||
}
|
||
|
||
/// <https://w3c.github.io/webappsec-referrer-policy/#determine-policy-for-token>
|
||
pub(crate) fn determine_policy_for_token(token: &str) -> ReferrerPolicy {
|
||
match_ignore_ascii_case! { token,
|
||
"never" | "no-referrer" => ReferrerPolicy::NoReferrer,
|
||
"no-referrer-when-downgrade" => ReferrerPolicy::NoReferrerWhenDowngrade,
|
||
"origin" => ReferrerPolicy::Origin,
|
||
"same-origin" => ReferrerPolicy::SameOrigin,
|
||
"strict-origin" => ReferrerPolicy::StrictOrigin,
|
||
"default" | "strict-origin-when-cross-origin" => ReferrerPolicy::StrictOriginWhenCrossOrigin,
|
||
"origin-when-cross-origin" => ReferrerPolicy::OriginWhenCrossOrigin,
|
||
"always" | "unsafe-url" => ReferrerPolicy::UnsafeUrl,
|
||
_ => ReferrerPolicy::EmptyString,
|
||
}
|
||
}
|
||
|
||
/// Specifies the type of focus event that is sent to a pipeline
|
||
#[derive(Clone, Copy, PartialEq)]
|
||
pub(crate) enum FocusType {
|
||
Element, // The first focus message - focus the element itself
|
||
Parent, // Focusing a parent element (an iframe)
|
||
}
|
||
|
||
/// Specifies the initiator of a focus operation.
|
||
#[derive(Clone, Copy, PartialEq)]
|
||
pub enum FocusInitiator {
|
||
/// The operation is initiated by this document and to be broadcasted
|
||
/// through the constellation.
|
||
Local,
|
||
/// The operation is initiated somewhere else, and we are updating our
|
||
/// internal state accordingly.
|
||
Remote,
|
||
}
|
||
|
||
/// Focus events
|
||
pub(crate) enum FocusEventType {
|
||
Focus, // Element gained focus. Doesn't bubble.
|
||
Blur, // Element lost focus. Doesn't bubble.
|
||
}
|
||
|
||
#[derive(JSTraceable, MallocSizeOf)]
|
||
pub(crate) enum AnimationFrameCallback {
|
||
DevtoolsFramerateTick {
|
||
actor_name: String,
|
||
},
|
||
FrameRequestCallback {
|
||
#[ignore_malloc_size_of = "Rc is hard"]
|
||
callback: Rc<FrameRequestCallback>,
|
||
},
|
||
}
|
||
|
||
impl AnimationFrameCallback {
|
||
fn call(&self, document: &Document, now: f64, can_gc: CanGc) {
|
||
match *self {
|
||
AnimationFrameCallback::DevtoolsFramerateTick { ref actor_name } => {
|
||
let msg = ScriptToDevtoolsControlMsg::FramerateTick(actor_name.clone(), now);
|
||
let devtools_sender = document.window().as_global_scope().devtools_chan().unwrap();
|
||
devtools_sender.send(msg).unwrap();
|
||
},
|
||
AnimationFrameCallback::FrameRequestCallback { ref callback } => {
|
||
// TODO(jdm): The spec says that any exceptions should be suppressed:
|
||
// https://github.com/servo/servo/issues/6928
|
||
let _ = callback.Call__(Finite::wrap(now), ExceptionHandling::Report, can_gc);
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
#[derive(Default, JSTraceable, MallocSizeOf)]
|
||
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
|
||
struct PendingInOrderScriptVec {
|
||
scripts: DomRefCell<VecDeque<PendingScript>>,
|
||
}
|
||
|
||
impl PendingInOrderScriptVec {
|
||
fn is_empty(&self) -> bool {
|
||
self.scripts.borrow().is_empty()
|
||
}
|
||
|
||
fn push(&self, element: &HTMLScriptElement) {
|
||
self.scripts
|
||
.borrow_mut()
|
||
.push_back(PendingScript::new(element));
|
||
}
|
||
|
||
fn loaded(&self, element: &HTMLScriptElement, result: ScriptResult) {
|
||
let mut scripts = self.scripts.borrow_mut();
|
||
let entry = scripts
|
||
.iter_mut()
|
||
.find(|entry| &*entry.element == element)
|
||
.unwrap();
|
||
entry.loaded(result);
|
||
}
|
||
|
||
fn take_next_ready_to_be_executed(&self) -> Option<(DomRoot<HTMLScriptElement>, ScriptResult)> {
|
||
let mut scripts = self.scripts.borrow_mut();
|
||
let pair = scripts.front_mut()?.take_result()?;
|
||
scripts.pop_front();
|
||
Some(pair)
|
||
}
|
||
|
||
fn clear(&self) {
|
||
*self.scripts.borrow_mut() = Default::default();
|
||
}
|
||
}
|
||
|
||
#[derive(JSTraceable, MallocSizeOf)]
|
||
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
|
||
struct PendingScript {
|
||
element: Dom<HTMLScriptElement>,
|
||
// TODO(sagudev): could this be all no_trace?
|
||
load: Option<ScriptResult>,
|
||
}
|
||
|
||
impl PendingScript {
|
||
fn new(element: &HTMLScriptElement) -> Self {
|
||
Self {
|
||
element: Dom::from_ref(element),
|
||
load: None,
|
||
}
|
||
}
|
||
|
||
fn new_with_load(element: &HTMLScriptElement, load: Option<ScriptResult>) -> Self {
|
||
Self {
|
||
element: Dom::from_ref(element),
|
||
load,
|
||
}
|
||
}
|
||
|
||
fn loaded(&mut self, result: ScriptResult) {
|
||
assert!(self.load.is_none());
|
||
self.load = Some(result);
|
||
}
|
||
|
||
fn take_result(&mut self) -> Option<(DomRoot<HTMLScriptElement>, ScriptResult)> {
|
||
self.load
|
||
.take()
|
||
.map(|result| (DomRoot::from_ref(&*self.element), result))
|
||
}
|
||
}
|
||
|
||
fn is_named_element_with_name_attribute(elem: &Element) -> bool {
|
||
let type_ = match elem.upcast::<Node>().type_id() {
|
||
NodeTypeId::Element(ElementTypeId::HTMLElement(type_)) => type_,
|
||
_ => return false,
|
||
};
|
||
match type_ {
|
||
HTMLElementTypeId::HTMLFormElement |
|
||
HTMLElementTypeId::HTMLIFrameElement |
|
||
HTMLElementTypeId::HTMLImageElement => true,
|
||
// TODO handle <embed> and <object>; these depend on whether the element is
|
||
// “exposed”, a concept that doesn’t fully make sense until embed/object
|
||
// behaviour is actually implemented
|
||
_ => false,
|
||
}
|
||
}
|
||
|
||
fn is_named_element_with_id_attribute(elem: &Element) -> bool {
|
||
// TODO handle <embed> and <object>; these depend on whether the element is
|
||
// “exposed”, a concept that doesn’t fully make sense until embed/object
|
||
// behaviour is actually implemented
|
||
elem.is::<HTMLImageElement>() && elem.get_name().is_some_and(|name| !name.is_empty())
|
||
}
|
||
|
||
impl DocumentHelpers for Document {
|
||
fn ensure_safe_to_run_script_or_layout(&self) {
|
||
Document::ensure_safe_to_run_script_or_layout(self)
|
||
}
|
||
}
|