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