diff options
Diffstat (limited to '')
-rw-r--r-- | servo/components/style/stylesheets/document_rule.rs | 305 |
1 files changed, 305 insertions, 0 deletions
diff --git a/servo/components/style/stylesheets/document_rule.rs b/servo/components/style/stylesheets/document_rule.rs new file mode 100644 index 0000000000..75edab308d --- /dev/null +++ b/servo/components/style/stylesheets/document_rule.rs @@ -0,0 +1,305 @@ +/* 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<Locked<CssRules>>, + /// 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<Self, ParseError<'i>> { + 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. +/// +/// <https://www.w3.org/TR/2012/WD-css3-conditional-20120911/#at-document> +/// +/// 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<DocumentMatchingFunction>); + +impl DocumentCondition { + /// Parse a document condition. + pub fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + 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 { + use static_prefs::pref; + + if context.in_ua_or_chrome_sheet() { + return true; + } + + if pref!("layout.css.moz-document.content.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, + } + } +} |