Skip to main content

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::derives::*;
10use crate::device::Device;
11use crate::parser::{Parse, ParserContext};
12use crate::shared_lock::{DeepCloneWithLock, Locked};
13use crate::shared_lock::{SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard};
14use crate::stylesheets::CssRules;
15use crate::values::CssUrl;
16use cssparser::{match_ignore_ascii_case, 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::{CssStringWriter, 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,
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}
111
112macro_rules! parse_quoted_or_unquoted_string {
113    ($input:ident, $url_matching_function:expr) => {
114        $input.parse_nested_block(|input| {
115            let start = input.position();
116            input
117                .parse_entirely(|input| {
118                    let string = input.expect_string()?;
119                    Ok($url_matching_function(string.as_ref().to_owned()))
120                })
121                .or_else(|_: ParseError| {
122                    while let Ok(_) = input.next() {}
123                    Ok($url_matching_function(input.slice_from(start).to_string()))
124                })
125        })
126    };
127}
128
129impl DocumentMatchingFunction {
130    /// Parse a URL matching function for a`@document` rule's condition.
131    pub fn parse(context: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
132        if let Ok(url) = input.try_parse(|input| CssUrl::parse(context, input)) {
133            return Ok(DocumentMatchingFunction::Url(url));
134        }
135
136        let function = input.expect_function()?.clone();
137        match_ignore_ascii_case! { &function,
138            "url-prefix" => {
139                parse_quoted_or_unquoted_string!(input, DocumentMatchingFunction::UrlPrefix)
140            },
141            "domain" => {
142                parse_quoted_or_unquoted_string!(input, DocumentMatchingFunction::Domain)
143            },
144            "regexp" => {
145                input.parse_nested_block(|input| {
146                    Ok(DocumentMatchingFunction::Regexp(
147                        input.expect_string()?.as_ref().to_owned(),
148                    ))
149                })
150            },
151            "media-document" => {
152                input.parse_nested_block(|input| {
153                    let kind = MediaDocumentKind::parse(input)?;
154                    Ok(DocumentMatchingFunction::MediaDocument(kind))
155                })
156            },
157
158            "plain-text-document" => {
159                input.parse_nested_block(|input| {
160                    input.expect_exhausted()?;
161                    Ok(DocumentMatchingFunction::PlainTextDocument(()))
162                })
163            },
164
165            _ => {
166                Err(ParseError::custom(
167                    StyleParseErrorKind::UnexpectedFunction
168                ))
169            },
170        }
171    }
172
173    #[cfg(feature = "gecko")]
174    /// Evaluate a URL matching function.
175    pub fn evaluate(&self, device: &Device) -> bool {
176        use crate::gecko_bindings::bindings::Gecko_DocumentRule_UseForPresentation;
177        use crate::gecko_bindings::structs::DocumentMatchingFunction as GeckoDocumentMatchingFunction;
178        use nsstring::nsCStr;
179
180        let func = match *self {
181            DocumentMatchingFunction::Url(_) => GeckoDocumentMatchingFunction::URL,
182            DocumentMatchingFunction::UrlPrefix(_) => GeckoDocumentMatchingFunction::URLPrefix,
183            DocumentMatchingFunction::Domain(_) => GeckoDocumentMatchingFunction::Domain,
184            DocumentMatchingFunction::Regexp(_) => GeckoDocumentMatchingFunction::RegExp,
185            DocumentMatchingFunction::MediaDocument(_) => {
186                GeckoDocumentMatchingFunction::MediaDocument
187            },
188            DocumentMatchingFunction::PlainTextDocument(..) => {
189                GeckoDocumentMatchingFunction::PlainTextDocument
190            },
191        };
192
193        let pattern = nsCStr::from(match *self {
194            DocumentMatchingFunction::Url(ref url) => url.as_str(),
195            DocumentMatchingFunction::UrlPrefix(ref pat)
196            | DocumentMatchingFunction::Domain(ref pat)
197            | DocumentMatchingFunction::Regexp(ref pat) => pat,
198            DocumentMatchingFunction::MediaDocument(kind) => match kind {
199                MediaDocumentKind::All => "all",
200                MediaDocumentKind::Image => "image",
201                MediaDocumentKind::Video => "video",
202            },
203            DocumentMatchingFunction::PlainTextDocument(()) => "",
204        });
205        unsafe { Gecko_DocumentRule_UseForPresentation(device.document(), &*pattern, func) }
206    }
207
208    #[cfg(not(feature = "gecko"))]
209    /// Evaluate a URL matching function.
210    pub fn evaluate(&self, _: &Device) -> bool {
211        false
212    }
213}
214
215/// A `@document` rule's condition.
216///
217/// <https://www.w3.org/TR/2012/WD-css3-conditional-20120911/#at-document>
218///
219/// The `@document` rule's condition is written as a comma-separated list of
220/// URL matching functions, and the condition evaluates to true whenever any
221/// one of those functions evaluates to true.
222#[derive(Clone, Debug, ToCss, ToShmem)]
223#[css(comma)]
224pub struct DocumentCondition(#[css(iterable)] Vec<DocumentMatchingFunction>);
225
226impl DocumentCondition {
227    /// Parse a document condition.
228    pub fn parse(context: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
229        let conditions =
230            input.parse_comma_separated(|input| DocumentMatchingFunction::parse(context, input))?;
231
232        let condition = DocumentCondition(conditions);
233        if !condition.allowed_in(context) {
234            return Err(ParseError::from_basic_kind(
235                BasicParseErrorKind::AtRuleInvalid,
236            ));
237        }
238        Ok(condition)
239    }
240
241    /// Evaluate a document condition.
242    pub fn evaluate(&self, device: &Device) -> bool {
243        self.0
244            .iter()
245            .any(|url_matching_function| url_matching_function.evaluate(device))
246    }
247
248    #[cfg(feature = "servo")]
249    fn allowed_in(&self, _: &ParserContext) -> bool {
250        false
251    }
252
253    #[cfg(feature = "gecko")]
254    fn allowed_in(&self, context: &ParserContext) -> bool {
255        if context.chrome_rules_enabled() {
256            return true;
257        }
258
259        // Allow a single url-prefix() for compatibility.
260        //
261        // See bug 1446470 and dependencies.
262        if self.0.len() != 1 {
263            return false;
264        }
265
266        // NOTE(emilio): This technically allows url-prefix("") too, but...
267        match self.0[0] {
268            DocumentMatchingFunction::UrlPrefix(ref prefix) => prefix.is_empty(),
269            _ => false,
270        }
271    }
272}