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