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 
9 use crate::media_queries::Device;
10 use crate::parser::{Parse, ParserContext};
11 use crate::shared_lock::{DeepCloneParams, DeepCloneWithLock, Locked};
12 use crate::shared_lock::{SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard};
13 use crate::str::CssStringWriter;
14 use crate::stylesheets::CssRules;
15 use crate::values::CssUrl;
16 use cssparser::{Parser, SourceLocation};
17 #[cfg(feature = "gecko")]
18 use malloc_size_of::{MallocSizeOfOps, MallocUnconditionalShallowSizeOf};
19 use servo_arc::Arc;
20 use std::fmt::{self, Write};
21 use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
22 
23 #[derive(Debug, ToShmem)]
24 /// A @-moz-document rule
25 pub 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 
34 impl DocumentRule {
35     /// Measure heap usage.
36     #[cfg(feature = "gecko")]
size_of(&self, guard: &SharedRwLockReadGuard, ops: &mut MallocSizeOfOps) -> usize37     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 
44 impl ToCssWithGuard for DocumentRule {
to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result45     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_str(" ")?;
51             rule.to_css(guard, dest)?;
52         }
53         dest.write_str(" }")
54     }
55 }
56 
57 impl DeepCloneWithLock for DocumentRule {
58     /// Deep clones this DocumentRule.
deep_clone_with_lock( &self, lock: &SharedRwLock, guard: &SharedRwLockReadGuard, params: &DeepCloneParams, ) -> Self59     fn deep_clone_with_lock(
60         &self,
61         lock: &SharedRwLock,
62         guard: &SharedRwLockReadGuard,
63         params: &DeepCloneParams,
64     ) -> Self {
65         let rules = self.rules.read_with(guard);
66         DocumentRule {
67             condition: self.condition.clone(),
68             rules: Arc::new(lock.wrap(rules.deep_clone_with_lock(lock, guard, params))),
69             source_location: self.source_location.clone(),
70         }
71     }
72 }
73 
74 /// The kind of media document that the rule will match.
75 #[derive(Clone, Copy, Debug, Parse, PartialEq, ToCss, ToShmem)]
76 #[allow(missing_docs)]
77 pub enum MediaDocumentKind {
78     All,
79     Plugin,
80     Image,
81     Video,
82 }
83 
84 /// A matching function for a `@document` rule's condition.
85 #[derive(Clone, Debug, ToCss, ToShmem)]
86 pub enum DocumentMatchingFunction {
87     /// Exact URL matching function. It evaluates to true whenever the
88     /// URL of the document being styled is exactly the URL given.
89     Url(CssUrl),
90     /// URL prefix matching function. It evaluates to true whenever the
91     /// URL of the document being styled has the argument to the
92     /// function as an initial substring (which is true when the two
93     /// strings are equal). When the argument is the empty string,
94     /// it evaluates to true for all documents.
95     #[css(function)]
96     UrlPrefix(String),
97     /// Domain matching function. It evaluates to true whenever the URL
98     /// of the document being styled has a host subcomponent and that
99     /// host subcomponent is exactly the argument to the ‘domain()’
100     /// function or a final substring of the host component is a
101     /// period (U+002E) immediately followed by the argument to the
102     /// ‘domain()’ function.
103     #[css(function)]
104     Domain(String),
105     /// Regular expression matching function. It evaluates to true
106     /// whenever the regular expression matches the entirety of the URL
107     /// of the document being styled.
108     #[css(function)]
109     Regexp(String),
110     /// Matching function for a media document.
111     #[css(function)]
112     MediaDocument(MediaDocumentKind),
113 }
114 
115 macro_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 
132 impl DocumentMatchingFunction {
133     /// Parse a URL matching function for a`@document` rule's condition.
parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result<Self, ParseError<'i>>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(|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                 Err(location.new_custom_error(
166                     StyleParseErrorKind::UnexpectedFunction(function.clone())
167                 ))
168             },
169         }
170     }
171 
172     #[cfg(feature = "gecko")]
173     /// Evaluate a URL matching function.
evaluate(&self, device: &Device) -> bool174     pub fn evaluate(&self, device: &Device) -> bool {
175         use crate::gecko_bindings::bindings::Gecko_DocumentRule_UseForPresentation;
176         use crate::gecko_bindings::structs::DocumentMatchingFunction as GeckoDocumentMatchingFunction;
177         use nsstring::nsCStr;
178 
179         let func = match *self {
180             DocumentMatchingFunction::Url(_) => GeckoDocumentMatchingFunction::URL,
181             DocumentMatchingFunction::UrlPrefix(_) => GeckoDocumentMatchingFunction::URLPrefix,
182             DocumentMatchingFunction::Domain(_) => GeckoDocumentMatchingFunction::Domain,
183             DocumentMatchingFunction::Regexp(_) => GeckoDocumentMatchingFunction::RegExp,
184             DocumentMatchingFunction::MediaDocument(_) => {
185                 GeckoDocumentMatchingFunction::MediaDocument
186             },
187         };
188 
189         let pattern = nsCStr::from(match *self {
190             DocumentMatchingFunction::Url(ref url) => url.as_str(),
191             DocumentMatchingFunction::UrlPrefix(ref pat) |
192             DocumentMatchingFunction::Domain(ref pat) |
193             DocumentMatchingFunction::Regexp(ref pat) => pat,
194             DocumentMatchingFunction::MediaDocument(kind) => match kind {
195                 MediaDocumentKind::All => "all",
196                 MediaDocumentKind::Image => "image",
197                 MediaDocumentKind::Plugin => "plugin",
198                 MediaDocumentKind::Video => "video",
199             },
200         });
201         unsafe { Gecko_DocumentRule_UseForPresentation(device.document(), &*pattern, func) }
202     }
203 
204     #[cfg(not(feature = "gecko"))]
205     /// Evaluate a URL matching function.
evaluate(&self, _: &Device) -> bool206     pub fn evaluate(&self, _: &Device) -> bool {
207         false
208     }
209 }
210 
211 /// A `@document` rule's condition.
212 ///
213 /// <https://www.w3.org/TR/2012/WD-css3-conditional-20120911/#at-document>
214 ///
215 /// The `@document` rule's condition is written as a comma-separated list of
216 /// URL matching functions, and the condition evaluates to true whenever any
217 /// one of those functions evaluates to true.
218 #[css(comma)]
219 #[derive(Clone, Debug, ToCss, ToShmem)]
220 pub struct DocumentCondition(#[css(iterable)] Vec<DocumentMatchingFunction>);
221 
222 impl DocumentCondition {
223     /// Parse a document condition.
parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result<Self, ParseError<'i>>224     pub fn parse<'i, 't>(
225         context: &ParserContext,
226         input: &mut Parser<'i, 't>,
227     ) -> Result<Self, ParseError<'i>> {
228         let conditions =
229             input.parse_comma_separated(|input| DocumentMatchingFunction::parse(context, input))?;
230 
231         let condition = DocumentCondition(conditions);
232         if !condition.allowed_in(context) {
233             return Err(
234                 input.new_custom_error(StyleParseErrorKind::UnsupportedAtRule(
235                     "-moz-document".into(),
236                 )),
237             );
238         }
239         Ok(condition)
240     }
241 
242     /// Evaluate a document condition.
evaluate(&self, device: &Device) -> bool243     pub fn evaluate(&self, device: &Device) -> bool {
244         self.0
245             .iter()
246             .any(|url_matching_function| url_matching_function.evaluate(device))
247     }
248 
249     #[cfg(feature = "servo")]
allowed_in(&self, _: &ParserContext) -> bool250     fn allowed_in(&self, _: &ParserContext) -> bool {
251         false
252     }
253 
254     #[cfg(feature = "gecko")]
allowed_in(&self, context: &ParserContext) -> bool255     fn allowed_in(&self, context: &ParserContext) -> bool {
256         use crate::stylesheets::Origin;
257         use static_prefs::pref;
258 
259         if context.stylesheet_origin != Origin::Author {
260             return true;
261         }
262 
263         if pref!("layout.css.moz-document.content.enabled") {
264             return true;
265         }
266 
267         // Allow a single url-prefix() for compatibility.
268         //
269         // See bug 1446470 and dependencies.
270         if self.0.len() != 1 {
271             return false;
272         }
273 
274         // NOTE(emilio): This technically allows url-prefix("") too, but...
275         match self.0[0] {
276             DocumentMatchingFunction::UrlPrefix(ref prefix) => prefix.is_empty(),
277             _ => false,
278         }
279     }
280 }
281