| /* 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 std::sync::atomic::{AtomicBool, Ordering}; |
| |
| 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::shared_lock::DeepCloneWithLock; |
| use style::stylesheets::{AllowImportRules, Origin, Stylesheet, StylesheetContents, 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::html::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::stylesheetcontentscache::{StylesheetContentsCache, StylesheetContentsCacheKey}; |
| 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>>>, |
| #[no_trace] |
| stylesheetcontents_cache_key: DomRefCell<Option<StylesheetContentsCacheKey>>, |
| 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), |
| stylesheetcontents_cache_key: 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 stylesheetcontents_create_callback = || { |
| #[cfg(feature = "tracing")] |
| let _span = tracing::trace_span!("ParseStylesheet", servo_profiling = true).entered(); |
| StylesheetContents::from_str( |
| &data, |
| UrlExtraData(window.get_url().get_arc()), |
| Origin::Author, |
| &shared_lock, |
| Some(&loader), |
| window.css_error_reporter(), |
| doc.quirks_mode(), |
| AllowImportRules::Yes, |
| /* sanitized_output = */ None, |
| ) |
| }; |
| |
| // For duplicate style sheets with identical content, `StylesheetContents` can be reused |
| // to avoid reedundant parsing of the style sheets. Additionally, the cache hit rate of |
| // stylo's `CascadeDataCache` can now be significantly improved. When shared `StylesheetContents` |
| // is modified, copy-on-write will occur, see `CSSStyleSheet::will_modify`. |
| let (cache_key, contents) = StylesheetContentsCache::get_or_insert_with( |
| &data, |
| &shared_lock, |
| UrlExtraData(window.get_url().get_arc()), |
| doc.quirks_mode(), |
| stylesheetcontents_create_callback, |
| ); |
| |
| let sheet = Arc::new(Stylesheet { |
| contents, |
| shared_lock, |
| media: mq, |
| disabled: AtomicBool::new(false), |
| }); |
| |
| // 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, cache_key, true); |
| } |
| |
| // FIXME(emilio): This is duplicated with HTMLLinkElement::set_stylesheet. |
| // |
| // With the reuse of `StylesheetContent` for same stylesheet string content, |
| // this function has a bit difference with `HTMLLinkElement::set_stylesheet` now. |
| #[cfg_attr(crown, allow(crown::unrooted_must_root))] |
| pub(crate) fn set_stylesheet( |
| &self, |
| s: Arc<Stylesheet>, |
| cache_key: Option<StylesheetContentsCacheKey>, |
| need_clean_cssom: bool, |
| ) { |
| 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); |
| } |
| |
| if need_clean_cssom { |
| self.clean_stylesheet_ownership(); |
| } else if let Some(cssom_stylesheet) = self.cssom_stylesheet.get() { |
| let guard = s.shared_lock.read(); |
| cssom_stylesheet.update_style_stylesheet(&s, &guard); |
| } |
| |
| *self.stylesheet.borrow_mut() = Some(s.clone()); |
| *self.stylesheetcontents_cache_key.borrow_mut() = cache_key; |
| stylesheets_owner.add_owned_stylesheet(self.upcast(), s); |
| } |
| |
| pub(crate) fn will_modify_stylesheet(&self) { |
| if let Some(stylesheet_with_owned_contents) = self.create_owned_contents_stylesheet() { |
| self.set_stylesheet(stylesheet_with_owned_contents, None, false); |
| } |
| } |
| |
| 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 create_owned_contents_stylesheet(&self) -> Option<Arc<Stylesheet>> { |
| let cache_key = self.stylesheetcontents_cache_key.borrow_mut().take()?; |
| if cache_key.is_uniquely_owned() { |
| StylesheetContentsCache::remove(cache_key); |
| return None; |
| } |
| |
| let stylesheet_with_shared_contents = self.stylesheet.borrow().clone()?; |
| let lock = stylesheet_with_shared_contents.shared_lock.clone(); |
| let guard = stylesheet_with_shared_contents.shared_lock.read(); |
| let stylesheet_with_owned_contents = Arc::new(Stylesheet { |
| contents: Arc::new( |
| stylesheet_with_shared_contents |
| .contents |
| .deep_clone_with_lock(&lock, &guard), |
| ), |
| shared_lock: lock, |
| media: stylesheet_with_shared_contents.media.clone(), |
| disabled: AtomicBool::new( |
| stylesheet_with_shared_contents |
| .disabled |
| .load(Ordering::SeqCst), |
| ), |
| }); |
| |
| Some(stylesheet_with_owned_contents) |
| } |
| |
| fn clean_stylesheet_ownership(&self) { |
| if let Some(cssom_stylesheet) = self.cssom_stylesheet.get() { |
| // If the CSSOMs change from having an owner node to being ownerless, they may still |
| // potentially modify shared stylesheets. Thus, create an new `Stylesheet` with owned |
| // `StylesheetContents` to ensure that the potentially modifications are only made on |
| // the owned `StylesheetContents`. |
| if let Some(stylesheet) = self.create_owned_contents_stylesheet() { |
| let guard = stylesheet.shared_lock.read(); |
| cssom_stylesheet.update_style_stylesheet(&stylesheet, &guard); |
| } |
| cssom_stylesheet.set_owner_node(None); |
| } |
| self.cssom_stylesheet.set(None); |
| } |
| |
| fn remove_stylesheet(&self) { |
| self.clean_stylesheet_ownership(); |
| if let Some(s) = self.stylesheet.borrow_mut().take() { |
| self.stylesheet_list_owner() |
| .remove_stylesheet(StylesheetSource::Element(Dom::from_ref(self.upcast())), &s); |
| let _ = self.stylesheetcontents_cache_key.borrow_mut().take(); |
| } |
| } |
| } |
| |
| 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"); |
| } |