summaryrefslogtreecommitdiffstats
path: root/servo/components/style/stylesheets/document_rule.rs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--servo/components/style/stylesheets/document_rule.rs305
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,
+ }
+ }
+}