style/stylesheets/
document_rule.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! [@document rules](https://www.w3.org/TR/2012/WD-css3-conditional-20120911/#at-document)
6//! initially in CSS Conditional Rules Module Level 3, @document has been postponed to the level 4.
7//! We implement the prefixed `@-moz-document`.
8
9use crate::media_queries::Device;
10use crate::parser::{Parse, ParserContext};
11use crate::shared_lock::{DeepCloneWithLock, Locked};
12use crate::shared_lock::{SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard};
13use crate::str::CssStringWriter;
14use crate::stylesheets::CssRules;
15use crate::values::CssUrl;
16use cssparser::{BasicParseErrorKind, Parser, SourceLocation};
17#[cfg(feature = "gecko")]
18use malloc_size_of::{MallocSizeOfOps, MallocUnconditionalShallowSizeOf};
19use servo_arc::Arc;
20use std::fmt::{self, Write};
21use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
22
23#[derive(Debug, ToShmem)]
24/// A @-moz-document rule
25pub struct DocumentRule {
26    /// The parsed condition
27    pub condition: DocumentCondition,
28    /// Child rules
29    pub rules: Arc<Locked<CssRules>>,
30    /// The line and column of the rule's source code.
31    pub source_location: SourceLocation,
32}
33
34impl DocumentRule {
35    /// Measure heap usage.
36    #[cfg(feature = "gecko")]
37    pub fn size_of(&self, guard: &SharedRwLockReadGuard, ops: &mut MallocSizeOfOps) -> usize {
38        // Measurement of other fields may be added later.
39        self.rules.unconditional_shallow_size_of(ops) +
40            self.rules.read_with(guard).size_of(guard, ops)
41    }
42}
43
44impl ToCssWithGuard for DocumentRule {
45    fn to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result {
46        dest.write_str("@-moz-document ")?;
47        self.condition.to_css(&mut CssWriter::new(dest))?;
48        dest.write_str(" {")?;
49        for rule in self.rules.read_with(guard).0.iter() {
50            dest.write_char(' ')?;
51            rule.to_css(guard, dest)?;
52        }
53        dest.write_str(" }")
54    }
55}
56
57impl DeepCloneWithLock for DocumentRule {
58    /// Deep clones this DocumentRule.
59    fn deep_clone_with_lock(
60        &self,
61        lock: &SharedRwLock,
62        guard: &SharedRwLockReadGuard,
63    ) -> Self {
64        let rules = self.rules.read_with(guard);
65        DocumentRule {
66            condition: self.condition.clone(),
67            rules: Arc::new(lock.wrap(rules.deep_clone_with_lock(lock, guard))),
68            source_location: self.source_location.clone(),
69        }
70    }
71}
72
73/// The kind of media document that the rule will match.
74#[derive(Clone, Copy, Debug, Parse, PartialEq, ToCss, ToShmem)]
75#[allow(missing_docs)]
76pub enum MediaDocumentKind {
77    All,
78    Image,
79    Video,
80}
81
82/// A matching function for a `@document` rule's condition.
83#[derive(Clone, Debug, ToCss, ToShmem)]
84pub enum DocumentMatchingFunction {
85    /// Exact URL matching function. It evaluates to true whenever the
86    /// URL of the document being styled is exactly the URL given.
87    Url(CssUrl),
88    /// URL prefix matching function. It evaluates to true whenever the
89    /// URL of the document being styled has the argument to the
90    /// function as an initial substring (which is true when the two
91    /// strings are equal). When the argument is the empty string,
92    /// it evaluates to true for all documents.
93    #[css(function)]
94    UrlPrefix(String),
95    /// Domain matching function. It evaluates to true whenever the URL
96    /// of the document being styled has a host subcomponent and that
97    /// host subcomponent is exactly the argument to the ‘domain()’
98    /// function or a final substring of the host component is a
99    /// period (U+002E) immediately followed by the argument to the
100    /// ‘domain()’ function.
101    #[css(function)]
102    Domain(String),
103    /// Regular expression matching function. It evaluates to true
104    /// whenever the regular expression matches the entirety of the URL
105    /// of the document being styled.
106    #[css(function)]
107    Regexp(String),
108    /// Matching function for a media document.
109    #[css(function)]
110    MediaDocument(MediaDocumentKind),
111    /// Matching function for a plain-text document.
112    #[css(function)]
113    PlainTextDocument(()),
114    /// Matching function for a document that can be observed by other content
115    /// documents.
116    #[css(function)]
117    UnobservableDocument(()),
118}
119
120macro_rules! parse_quoted_or_unquoted_string {
121    ($input:ident, $url_matching_function:expr) => {
122        $input.parse_nested_block(|input| {
123            let start = input.position();
124            input
125                .parse_entirely(|input| {
126                    let string = input.expect_string()?;
127                    Ok($url_matching_function(string.as_ref().to_owned()))
128                })
129                .or_else(|_: ParseError| {
130                    while let Ok(_) = input.next() {}
131                    Ok($url_matching_function(input.slice_from(start).to_string()))
132                })
133        })
134    };
135}
136
137impl DocumentMatchingFunction {
138    /// Parse a URL matching function for a`@document` rule's condition.
139    pub fn parse<'i, 't>(
140        context: &ParserContext,
141        input: &mut Parser<'i, 't>,
142    ) -> Result<Self, ParseError<'i>> {
143        if let Ok(url) = input.try_parse(|input| CssUrl::parse(context, input)) {
144            return Ok(DocumentMatchingFunction::Url(url));
145        }
146
147        let location = input.current_source_location();
148        let function = input.expect_function()?.clone();
149        match_ignore_ascii_case! { &function,
150            "url-prefix" => {
151                parse_quoted_or_unquoted_string!(input, DocumentMatchingFunction::UrlPrefix)
152            },
153            "domain" => {
154                parse_quoted_or_unquoted_string!(input, DocumentMatchingFunction::Domain)
155            },
156            "regexp" => {
157                input.parse_nested_block(|input| {
158                    Ok(DocumentMatchingFunction::Regexp(
159                        input.expect_string()?.as_ref().to_owned(),
160                    ))
161                })
162            },
163            "media-document" => {
164                input.parse_nested_block(|input| {
165                    let kind = MediaDocumentKind::parse(input)?;
166                    Ok(DocumentMatchingFunction::MediaDocument(kind))
167                })
168            },
169
170            "plain-text-document" => {
171                input.parse_nested_block(|input| {
172                    input.expect_exhausted()?;
173                    Ok(DocumentMatchingFunction::PlainTextDocument(()))
174                })
175            },
176
177            "unobservable-document" => {
178                input.parse_nested_block(|input| {
179                    input.expect_exhausted()?;
180                    Ok(DocumentMatchingFunction::UnobservableDocument(()))
181                })
182            },
183
184            _ => {
185                Err(location.new_custom_error(
186                    StyleParseErrorKind::UnexpectedFunction(function.clone())
187                ))
188            },
189        }
190    }
191
192    #[cfg(feature = "gecko")]
193    /// Evaluate a URL matching function.
194    pub fn evaluate(&self, device: &Device) -> bool {
195        use crate::gecko_bindings::bindings::Gecko_DocumentRule_UseForPresentation;
196        use crate::gecko_bindings::structs::DocumentMatchingFunction as GeckoDocumentMatchingFunction;
197        use nsstring::nsCStr;
198
199        let func = match *self {
200            DocumentMatchingFunction::Url(_) => GeckoDocumentMatchingFunction::URL,
201            DocumentMatchingFunction::UrlPrefix(_) => GeckoDocumentMatchingFunction::URLPrefix,
202            DocumentMatchingFunction::Domain(_) => GeckoDocumentMatchingFunction::Domain,
203            DocumentMatchingFunction::Regexp(_) => GeckoDocumentMatchingFunction::RegExp,
204            DocumentMatchingFunction::MediaDocument(_) => {
205                GeckoDocumentMatchingFunction::MediaDocument
206            },
207            DocumentMatchingFunction::PlainTextDocument(..) => {
208                GeckoDocumentMatchingFunction::PlainTextDocument
209            },
210            DocumentMatchingFunction::UnobservableDocument(..) => {
211                GeckoDocumentMatchingFunction::UnobservableDocument
212            },
213        };
214
215        let pattern = nsCStr::from(match *self {
216            DocumentMatchingFunction::Url(ref url) => url.as_str(),
217            DocumentMatchingFunction::UrlPrefix(ref pat) |
218            DocumentMatchingFunction::Domain(ref pat) |
219            DocumentMatchingFunction::Regexp(ref pat) => pat,
220            DocumentMatchingFunction::MediaDocument(kind) => match kind {
221                MediaDocumentKind::All => "all",
222                MediaDocumentKind::Image => "image",
223                MediaDocumentKind::Video => "video",
224            },
225            DocumentMatchingFunction::PlainTextDocument(()) |
226            DocumentMatchingFunction::UnobservableDocument(()) => "",
227        });
228        unsafe { Gecko_DocumentRule_UseForPresentation(device.document(), &*pattern, func) }
229    }
230
231    #[cfg(not(feature = "gecko"))]
232    /// Evaluate a URL matching function.
233    pub fn evaluate(&self, _: &Device) -> bool {
234        false
235    }
236}
237
238/// A `@document` rule's condition.
239///
240/// <https://www.w3.org/TR/2012/WD-css3-conditional-20120911/#at-document>
241///
242/// The `@document` rule's condition is written as a comma-separated list of
243/// URL matching functions, and the condition evaluates to true whenever any
244/// one of those functions evaluates to true.
245#[derive(Clone, Debug, ToCss, ToShmem)]
246#[css(comma)]
247pub struct DocumentCondition(#[css(iterable)] Vec<DocumentMatchingFunction>);
248
249impl DocumentCondition {
250    /// Parse a document condition.
251    pub fn parse<'i, 't>(
252        context: &ParserContext,
253        input: &mut Parser<'i, 't>,
254    ) -> Result<Self, ParseError<'i>> {
255        let conditions =
256            input.parse_comma_separated(|input| DocumentMatchingFunction::parse(context, input))?;
257
258        let condition = DocumentCondition(conditions);
259        if !condition.allowed_in(context) {
260            return Err(input.new_error(BasicParseErrorKind::AtRuleInvalid("-moz-document".into())));
261        }
262        Ok(condition)
263    }
264
265    /// Evaluate a document condition.
266    pub fn evaluate(&self, device: &Device) -> bool {
267        self.0
268            .iter()
269            .any(|url_matching_function| url_matching_function.evaluate(device))
270    }
271
272    #[cfg(feature = "servo")]
273    fn allowed_in(&self, _: &ParserContext) -> bool {
274        false
275    }
276
277    #[cfg(feature = "gecko")]
278    fn allowed_in(&self, context: &ParserContext) -> bool {
279        if context.chrome_rules_enabled() {
280            return true;
281        }
282
283        // Allow a single url-prefix() for compatibility.
284        //
285        // See bug 1446470 and dependencies.
286        if self.0.len() != 1 {
287            return false;
288        }
289
290        // NOTE(emilio): This technically allows url-prefix("") too, but...
291        match self.0[0] {
292            DocumentMatchingFunction::UrlPrefix(ref prefix) => prefix.is_empty(),
293            _ => false,
294        }
295    }
296}