servo/components/script/dom/htmlstyleelement.rs
Jo Steven Novaryo f523445fc3
script: Implement DocumentOrShadowDOM.adoptedStylesheet with FrozenArray (#38163)
Spec:
https://drafts.csswg.org/cssom/#dom-documentorshadowroot-adoptedstylesheets

Implement `DocumentOrShadowDOM.adoptedStylesheet`. Due to
`ObservableArray` being a massive issue on its own, it will be as it was
a `FrozenArray` at first. This approach is similar to how Gecko
implement adopted stylesheet. See
https://phabricator.services.mozilla.com/D144547#change-IXyOzxxFn8sU.

All of the changes will be gated behind a preference
`dom_adoptedstylesheet_enabled`.

Adopted stylesheet is implemented by adding the setter and getter of it.
While the getter works like a normal attribute getter, the setter need
to consider the inner working of document and shadow root StylesheetSet,
specifically the ordering and the invalidations. Particularly for
setter, we will clear all of the adopted stylesheet within the
StylesheetSet and readd them. Possible optimization exist, but the focus
should be directed to implementing `ObservableArray`.

More context about the implementations
https://hackmd.io/vtJAn4UyS_O0Idvk5dCO_w.

Testing: Existing WPT Coverage
Fixes: https://github.com/servo/servo/issues/37561

---------

Signed-off-by: Jo Steven Novaryo <jo.steven.novaryo@huawei.com>
2025-07-23 08:16:01 +00:00

359 lines
13 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;
use dom_struct::dom_struct;
use html5ever::{LocalName, Prefix};
use js::rust::HandleObject;
use net_traits::ReferrerPolicy;
use script_bindings::root::Dom;
use servo_arc::Arc;
use style::media_queries::MediaList as StyleMediaList;
use style::stylesheets::{AllowImportRules, Origin, Stylesheet, UrlExtraData};
use crate::dom::attr::Attr;
use crate::dom::bindings::cell::DomRefCell;
use crate::dom::bindings::codegen::Bindings::HTMLStyleElementBinding::HTMLStyleElementMethods;
use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods;
use crate::dom::bindings::inheritance::Castable;
use crate::dom::bindings::root::{DomRoot, MutNullableDom};
use crate::dom::bindings::str::DOMString;
use crate::dom::csp::{CspReporting, InlineCheckType};
use crate::dom::cssstylesheet::CSSStyleSheet;
use crate::dom::document::Document;
use crate::dom::documentorshadowroot::StylesheetSource;
use crate::dom::element::{AttributeMutation, Element, ElementCreator};
use crate::dom::htmlelement::HTMLElement;
use crate::dom::medialist::MediaList;
use crate::dom::node::{BindContext, ChildrenMutation, Node, NodeTraits, UnbindContext};
use crate::dom::stylesheet::StyleSheet as DOMStyleSheet;
use crate::dom::virtualmethods::VirtualMethods;
use crate::script_runtime::CanGc;
use crate::stylesheet_loader::{StylesheetLoader, StylesheetOwner};
#[dom_struct]
pub(crate) struct HTMLStyleElement {
htmlelement: HTMLElement,
#[conditional_malloc_size_of]
#[no_trace]
stylesheet: DomRefCell<Option<Arc<Stylesheet>>>,
cssom_stylesheet: MutNullableDom<CSSStyleSheet>,
/// <https://html.spec.whatwg.org/multipage/#a-style-sheet-that-is-blocking-scripts>
parser_inserted: Cell<bool>,
in_stack_of_open_elements: Cell<bool>,
pending_loads: Cell<u32>,
any_failed_load: Cell<bool>,
}
impl HTMLStyleElement {
fn new_inherited(
local_name: LocalName,
prefix: Option<Prefix>,
document: &Document,
creator: ElementCreator,
) -> HTMLStyleElement {
HTMLStyleElement {
htmlelement: HTMLElement::new_inherited(local_name, prefix, document),
stylesheet: DomRefCell::new(None),
cssom_stylesheet: MutNullableDom::new(None),
parser_inserted: Cell::new(creator.is_parser_created()),
in_stack_of_open_elements: Cell::new(creator.is_parser_created()),
pending_loads: Cell::new(0),
any_failed_load: Cell::new(false),
}
}
#[cfg_attr(crown, allow(crown::unrooted_must_root))]
pub(crate) fn new(
local_name: LocalName,
prefix: Option<Prefix>,
document: &Document,
proto: Option<HandleObject>,
creator: ElementCreator,
can_gc: CanGc,
) -> DomRoot<HTMLStyleElement> {
Node::reflect_node_with_proto(
Box::new(HTMLStyleElement::new_inherited(
local_name, prefix, document, creator,
)),
document,
proto,
can_gc,
)
}
#[inline]
fn create_media_list(&self, mq_str: &str) -> StyleMediaList {
MediaList::parse_media_list(mq_str, &self.owner_window())
}
pub(crate) fn parse_own_css(&self) {
let node = self.upcast::<Node>();
assert!(node.is_connected());
// Step 4. of <https://html.spec.whatwg.org/multipage/#the-style-element%3Aupdate-a-style-block>
let mut type_attribute = self.Type();
type_attribute.make_ascii_lowercase();
if !type_attribute.is_empty() && type_attribute != "text/css" {
return;
}
let doc = self.owner_document();
let global = &self.owner_global();
// Step 5: If the Should element's inline behavior be blocked by Content Security Policy? algorithm
// returns "Blocked" when executed upon the style element, "style",
// and the style element's child text content, then return. [CSP]
if global
.get_csp_list()
.should_elements_inline_type_behavior_be_blocked(
global,
self.upcast(),
InlineCheckType::Style,
&node.child_text_content(),
)
{
return;
}
let window = node.owner_window();
let data = node
.GetTextContent()
.expect("Element.textContent must be a string");
let shared_lock = node.owner_doc().style_shared_lock().clone();
let mq = Arc::new(shared_lock.wrap(self.create_media_list(&self.Media())));
let loader = StylesheetLoader::for_element(self.upcast());
let sheet = Stylesheet::from_str(
&data,
UrlExtraData(window.get_url().get_arc()),
Origin::Author,
mq,
shared_lock,
Some(&loader),
window.css_error_reporter(),
doc.quirks_mode(),
AllowImportRules::Yes,
);
let sheet = Arc::new(sheet);
// No subresource loads were triggered, queue load event
if self.pending_loads.get() == 0 {
self.owner_global()
.task_manager()
.dom_manipulation_task_source()
.queue_simple_event(self.upcast(), atom!("load"));
}
self.set_stylesheet(sheet);
}
// FIXME(emilio): This is duplicated with HTMLLinkElement::set_stylesheet.
#[cfg_attr(crown, allow(crown::unrooted_must_root))]
pub(crate) fn set_stylesheet(&self, s: Arc<Stylesheet>) {
let stylesheets_owner = self.stylesheet_list_owner();
if let Some(ref s) = *self.stylesheet.borrow() {
stylesheets_owner
.remove_stylesheet(StylesheetSource::Element(Dom::from_ref(self.upcast())), s)
}
*self.stylesheet.borrow_mut() = Some(s.clone());
self.clean_stylesheet_ownership();
stylesheets_owner.add_owned_stylesheet(self.upcast(), s);
}
pub(crate) fn get_stylesheet(&self) -> Option<Arc<Stylesheet>> {
self.stylesheet.borrow().clone()
}
pub(crate) fn get_cssom_stylesheet(&self) -> Option<DomRoot<CSSStyleSheet>> {
self.get_stylesheet().map(|sheet| {
self.cssom_stylesheet.or_init(|| {
CSSStyleSheet::new(
&self.owner_window(),
Some(self.upcast::<Element>()),
"text/css".into(),
None, // todo handle location
None, // todo handle title
sheet,
None, // constructor_document
CanGc::note(),
)
})
})
}
fn clean_stylesheet_ownership(&self) {
if let Some(cssom_stylesheet) = self.cssom_stylesheet.get() {
cssom_stylesheet.set_owner_node(None);
}
self.cssom_stylesheet.set(None);
}
fn remove_stylesheet(&self) {
if let Some(s) = self.stylesheet.borrow_mut().take() {
self.clean_stylesheet_ownership();
self.stylesheet_list_owner()
.remove_stylesheet(StylesheetSource::Element(Dom::from_ref(self.upcast())), &s)
}
}
}
impl VirtualMethods for HTMLStyleElement {
fn super_type(&self) -> Option<&dyn VirtualMethods> {
Some(self.upcast::<HTMLElement>() as &dyn VirtualMethods)
}
fn children_changed(&self, mutation: &ChildrenMutation) {
self.super_type().unwrap().children_changed(mutation);
// https://html.spec.whatwg.org/multipage/#update-a-style-block
// Handles the case when:
// "The element is not on the stack of open elements of an HTML parser or XML parser,
// and one of its child nodes is modified by a script."
// TODO: Handle Text child contents being mutated.
let node = self.upcast::<Node>();
if (node.is_in_a_document_tree() || node.is_in_a_shadow_tree()) &&
!self.in_stack_of_open_elements.get()
{
self.parse_own_css();
}
}
fn bind_to_tree(&self, context: &BindContext, can_gc: CanGc) {
self.super_type().unwrap().bind_to_tree(context, can_gc);
// https://html.spec.whatwg.org/multipage/#update-a-style-block
// Handles the case when:
// "The element is not on the stack of open elements of an HTML parser or XML parser,
// and it becomes connected or disconnected."
if context.tree_connected && !self.in_stack_of_open_elements.get() {
self.parse_own_css();
}
}
fn pop(&self) {
self.super_type().unwrap().pop();
// https://html.spec.whatwg.org/multipage/#update-a-style-block
// Handles the case when:
// "The element is popped off the stack of open elements of an HTML parser or XML parser."
self.in_stack_of_open_elements.set(false);
if self.upcast::<Node>().is_in_a_document_tree() {
self.parse_own_css();
}
}
fn unbind_from_tree(&self, context: &UnbindContext, can_gc: CanGc) {
if let Some(s) = self.super_type() {
s.unbind_from_tree(context, can_gc);
}
if context.tree_connected {
self.remove_stylesheet();
}
}
fn attribute_mutated(&self, attr: &Attr, mutation: AttributeMutation, can_gc: CanGc) {
if let Some(s) = self.super_type() {
s.attribute_mutated(attr, mutation, can_gc);
}
let node = self.upcast::<Node>();
if !(node.is_in_a_document_tree() || node.is_in_a_shadow_tree()) ||
self.in_stack_of_open_elements.get()
{
return;
}
if attr.name() == "type" {
if let AttributeMutation::Set(Some(old_value)) = mutation {
if **old_value == **attr.value() {
return;
}
}
self.remove_stylesheet();
self.parse_own_css();
} else if attr.name() == "media" {
if let Some(ref stylesheet) = *self.stylesheet.borrow_mut() {
let shared_lock = node.owner_doc().style_shared_lock().clone();
let mut guard = shared_lock.write();
let media = stylesheet.media.write_with(&mut guard);
match mutation {
AttributeMutation::Set(_) => *media = self.create_media_list(&attr.value()),
AttributeMutation::Removed => *media = StyleMediaList::empty(),
};
self.owner_document().invalidate_stylesheets();
}
}
}
}
impl StylesheetOwner for HTMLStyleElement {
fn increment_pending_loads_count(&self) {
self.pending_loads.set(self.pending_loads.get() + 1)
}
fn load_finished(&self, succeeded: bool) -> Option<bool> {
assert!(self.pending_loads.get() > 0, "What finished?");
if !succeeded {
self.any_failed_load.set(true);
}
self.pending_loads.set(self.pending_loads.get() - 1);
if self.pending_loads.get() != 0 {
return None;
}
let any_failed = self.any_failed_load.get();
self.any_failed_load.set(false);
Some(any_failed)
}
fn parser_inserted(&self) -> bool {
self.parser_inserted.get()
}
fn referrer_policy(&self) -> ReferrerPolicy {
ReferrerPolicy::EmptyString
}
fn set_origin_clean(&self, origin_clean: bool) {
if let Some(stylesheet) = self.get_cssom_stylesheet() {
stylesheet.set_origin_clean(origin_clean);
}
}
}
impl HTMLStyleElementMethods<crate::DomTypeHolder> for HTMLStyleElement {
/// <https://drafts.csswg.org/cssom/#dom-linkstyle-sheet>
fn GetSheet(&self) -> Option<DomRoot<DOMStyleSheet>> {
self.get_cssom_stylesheet().map(DomRoot::upcast)
}
/// <https://html.spec.whatwg.org/multipage/#dom-style-disabled>
fn Disabled(&self) -> bool {
self.get_cssom_stylesheet()
.is_some_and(|sheet| sheet.disabled())
}
/// <https://html.spec.whatwg.org/multipage/#dom-style-disabled>
fn SetDisabled(&self, value: bool) {
if let Some(sheet) = self.get_cssom_stylesheet() {
sheet.set_disabled(value);
}
}
// <https://html.spec.whatwg.org/multipage/#HTMLStyleElement-partial>
make_getter!(Type, "type");
// <https://html.spec.whatwg.org/multipage/#HTMLStyleElement-partial>
make_setter!(SetType, "type");
// <https://html.spec.whatwg.org/multipage/#attr-style-media>
make_getter!(Media, "media");
// <https://html.spec.whatwg.org/multipage/#attr-style-media>
make_setter!(SetMedia, "media");
}