/* 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/. */ //! [@document rules](https://www.w3.org/TR/2012/WD-css3-conditional-20120911/#at-document) //! initially in CSS Conditional Rules Module Level 3, @document has been postponed to the level 4. //! We implement the prefixed `@-moz-document`. use crate::media_queries::Device; use crate::parser::{Parse, ParserContext}; use crate::shared_lock::{DeepCloneParams, DeepCloneWithLock, Locked}; use crate::shared_lock::{SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard}; use crate::str::CssStringWriter; use crate::stylesheets::CssRules; use crate::values::CssUrl; use cssparser::{BasicParseErrorKind, Parser, SourceLocation}; #[cfg(feature = "gecko")] use malloc_size_of::{MallocSizeOfOps, MallocUnconditionalShallowSizeOf}; use servo_arc::Arc; use std::fmt::{self, Write}; use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; #[derive(Debug, ToShmem)] /// A @-moz-document rule pub struct DocumentRule { /// The parsed condition pub condition: DocumentCondition, /// Child rules pub rules: Arc>, /// The line and column of the rule's source code. pub source_location: SourceLocation, } impl DocumentRule { /// Measure heap usage. #[cfg(feature = "gecko")] pub fn size_of(&self, guard: &SharedRwLockReadGuard, ops: &mut MallocSizeOfOps) -> usize { // Measurement of other fields may be added later. self.rules.unconditional_shallow_size_of(ops) + self.rules.read_with(guard).size_of(guard, ops) } } impl ToCssWithGuard for DocumentRule { fn to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result { dest.write_str("@-moz-document ")?; self.condition.to_css(&mut CssWriter::new(dest))?; dest.write_str(" {")?; for rule in self.rules.read_with(guard).0.iter() { dest.write_char(' ')?; rule.to_css(guard, dest)?; } dest.write_str(" }") } } impl DeepCloneWithLock for DocumentRule { /// Deep clones this DocumentRule. fn deep_clone_with_lock( &self, lock: &SharedRwLock, guard: &SharedRwLockReadGuard, params: &DeepCloneParams, ) -> Self { let rules = self.rules.read_with(guard); DocumentRule { condition: self.condition.clone(), rules: Arc::new(lock.wrap(rules.deep_clone_with_lock(lock, guard, params))), source_location: self.source_location.clone(), } } } /// The kind of media document that the rule will match. #[derive(Clone, Copy, Debug, Parse, PartialEq, ToCss, ToShmem)] #[allow(missing_docs)] pub enum MediaDocumentKind { All, Plugin, Image, Video, } /// A matching function for a `@document` rule's condition. #[derive(Clone, Debug, ToCss, ToShmem)] pub enum DocumentMatchingFunction { /// Exact URL matching function. It evaluates to true whenever the /// URL of the document being styled is exactly the URL given. Url(CssUrl), /// URL prefix matching function. It evaluates to true whenever the /// URL of the document being styled has the argument to the /// function as an initial substring (which is true when the two /// strings are equal). When the argument is the empty string, /// it evaluates to true for all documents. #[css(function)] UrlPrefix(String), /// Domain matching function. It evaluates to true whenever the URL /// of the document being styled has a host subcomponent and that /// host subcomponent is exactly the argument to the ‘domain()’ /// function or a final substring of the host component is a /// period (U+002E) immediately followed by the argument to the /// ‘domain()’ function. #[css(function)] Domain(String), /// Regular expression matching function. It evaluates to true /// whenever the regular expression matches the entirety of the URL /// of the document being styled. #[css(function)] Regexp(String), /// Matching function for a media document. #[css(function)] MediaDocument(MediaDocumentKind), /// Matching function for a plain-text document. #[css(function)] PlainTextDocument(()), /// Matching function for a document that can be observed by other content /// documents. #[css(function)] UnobservableDocument(()), } macro_rules! parse_quoted_or_unquoted_string { ($input:ident, $url_matching_function:expr) => { $input.parse_nested_block(|input| { let start = input.position(); input .parse_entirely(|input| { let string = input.expect_string()?; Ok($url_matching_function(string.as_ref().to_owned())) }) .or_else(|_: ParseError| { while let Ok(_) = input.next() {} Ok($url_matching_function(input.slice_from(start).to_string())) }) }) }; } impl DocumentMatchingFunction { /// Parse a URL matching function for a`@document` rule's condition. pub fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { if let Ok(url) = input.try_parse(|input| CssUrl::parse(context, input)) { return Ok(DocumentMatchingFunction::Url(url)); } let location = input.current_source_location(); let function = input.expect_function()?.clone(); match_ignore_ascii_case! { &function, "url-prefix" => { parse_quoted_or_unquoted_string!(input, DocumentMatchingFunction::UrlPrefix) }, "domain" => { parse_quoted_or_unquoted_string!(input, DocumentMatchingFunction::Domain) }, "regexp" => { input.parse_nested_block(|input| { Ok(DocumentMatchingFunction::Regexp( input.expect_string()?.as_ref().to_owned(), )) }) }, "media-document" => { input.parse_nested_block(|input| { let kind = MediaDocumentKind::parse(input)?; Ok(DocumentMatchingFunction::MediaDocument(kind)) }) }, "plain-text-document" => { input.parse_nested_block(|input| { input.expect_exhausted()?; Ok(DocumentMatchingFunction::PlainTextDocument(())) }) }, "unobservable-document" => { input.parse_nested_block(|input| { input.expect_exhausted()?; Ok(DocumentMatchingFunction::UnobservableDocument(())) }) }, _ => { Err(location.new_custom_error( StyleParseErrorKind::UnexpectedFunction(function.clone()) )) }, } } #[cfg(feature = "gecko")] /// Evaluate a URL matching function. pub fn evaluate(&self, device: &Device) -> bool { use crate::gecko_bindings::bindings::Gecko_DocumentRule_UseForPresentation; use crate::gecko_bindings::structs::DocumentMatchingFunction as GeckoDocumentMatchingFunction; use nsstring::nsCStr; let func = match *self { DocumentMatchingFunction::Url(_) => GeckoDocumentMatchingFunction::URL, DocumentMatchingFunction::UrlPrefix(_) => GeckoDocumentMatchingFunction::URLPrefix, DocumentMatchingFunction::Domain(_) => GeckoDocumentMatchingFunction::Domain, DocumentMatchingFunction::Regexp(_) => GeckoDocumentMatchingFunction::RegExp, DocumentMatchingFunction::MediaDocument(_) => { GeckoDocumentMatchingFunction::MediaDocument }, DocumentMatchingFunction::PlainTextDocument(..) => { GeckoDocumentMatchingFunction::PlainTextDocument }, DocumentMatchingFunction::UnobservableDocument(..) => { GeckoDocumentMatchingFunction::UnobservableDocument }, }; let pattern = nsCStr::from(match *self { DocumentMatchingFunction::Url(ref url) => url.as_str(), DocumentMatchingFunction::UrlPrefix(ref pat) | DocumentMatchingFunction::Domain(ref pat) | DocumentMatchingFunction::Regexp(ref pat) => pat, DocumentMatchingFunction::MediaDocument(kind) => match kind { MediaDocumentKind::All => "all", MediaDocumentKind::Image => "image", MediaDocumentKind::Plugin => "plugin", MediaDocumentKind::Video => "video", }, DocumentMatchingFunction::PlainTextDocument(()) | DocumentMatchingFunction::UnobservableDocument(()) => "", }); unsafe { Gecko_DocumentRule_UseForPresentation(device.document(), &*pattern, func) } } #[cfg(not(feature = "gecko"))] /// Evaluate a URL matching function. pub fn evaluate(&self, _: &Device) -> bool { false } } /// A `@document` rule's condition. /// /// /// /// The `@document` rule's condition is written as a comma-separated list of /// URL matching functions, and the condition evaluates to true whenever any /// one of those functions evaluates to true. #[derive(Clone, Debug, ToCss, ToShmem)] #[css(comma)] pub struct DocumentCondition(#[css(iterable)] Vec); impl DocumentCondition { /// Parse a document condition. pub fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { let conditions = input.parse_comma_separated(|input| DocumentMatchingFunction::parse(context, input))?; let condition = DocumentCondition(conditions); if !condition.allowed_in(context) { return Err(input.new_error(BasicParseErrorKind::AtRuleInvalid("-moz-document".into()))); } Ok(condition) } /// Evaluate a document condition. pub fn evaluate(&self, device: &Device) -> bool { self.0 .iter() .any(|url_matching_function| url_matching_function.evaluate(device)) } #[cfg(feature = "servo")] fn allowed_in(&self, _: &ParserContext) -> bool { false } #[cfg(feature = "gecko")] fn allowed_in(&self, context: &ParserContext) -> bool { if context.chrome_rules_enabled() { return true; } // Allow a single url-prefix() for compatibility. // // See bug 1446470 and dependencies. if self.0.len() != 1 { return false; } // NOTE(emilio): This technically allows url-prefix("") too, but... match self.0[0] { DocumentMatchingFunction::UrlPrefix(ref prefix) => prefix.is_empty(), _ => false, } } }