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 http://mozilla.org/MPL/2.0/. */
4 
5 //! [Media queries][mq].
6 //!
7 //! [mq]: https://drafts.csswg.org/mediaqueries/
8 
9 use Atom;
10 use context::QuirksMode;
11 use cssparser::{Delimiter, Parser};
12 use cssparser::{Token, ParserInput};
13 use error_reporting::{ContextualParseError, ParseErrorReporter};
14 use parser::{ParserContext, ParserErrorContext};
15 use selectors::parser::SelectorParseErrorKind;
16 use std::fmt::{self, Write};
17 use str::string_as_ascii_lowercase;
18 use style_traits::{CssWriter, ToCss, ParseError, StyleParseErrorKind};
19 use values::CustomIdent;
20 
21 #[cfg(feature = "servo")]
22 pub use servo::media_queries::{Device, Expression};
23 #[cfg(feature = "gecko")]
24 pub use gecko::media_queries::{Device, Expression};
25 
26 /// A type that encapsulates a media query list.
27 #[cfg_attr(feature = "servo", derive(MallocSizeOf))]
28 #[css(comma)]
29 #[derive(Clone, Debug, ToCss)]
30 pub struct MediaList {
31     /// The list of media queries.
32     #[css(iterable)]
33     pub media_queries: Vec<MediaQuery>,
34 }
35 
36 impl MediaList {
37     /// Create an empty MediaList.
empty() -> Self38     pub fn empty() -> Self {
39         MediaList { media_queries: vec![] }
40     }
41 }
42 
43 /// <https://drafts.csswg.org/mediaqueries/#mq-prefix>
44 #[cfg_attr(feature = "servo", derive(MallocSizeOf))]
45 #[derive(Clone, Copy, Debug, Eq, PartialEq, ToCss)]
46 pub enum Qualifier {
47     /// Hide a media query from legacy UAs:
48     /// <https://drafts.csswg.org/mediaqueries/#mq-only>
49     Only,
50     /// Negate a media query:
51     /// <https://drafts.csswg.org/mediaqueries/#mq-not>
52     Not,
53 }
54 
55 /// A [media query][mq].
56 ///
57 /// [mq]: https://drafts.csswg.org/mediaqueries/
58 #[derive(Clone, Debug, PartialEq)]
59 #[cfg_attr(feature = "servo", derive(MallocSizeOf))]
60 pub struct MediaQuery {
61     /// The qualifier for this query.
62     pub qualifier: Option<Qualifier>,
63     /// The media type for this query, that can be known, unknown, or "all".
64     pub media_type: MediaQueryType,
65     /// The set of expressions that this media query contains.
66     pub expressions: Vec<Expression>,
67 }
68 
69 impl MediaQuery {
70     /// Return a media query that never matches, used for when we fail to parse
71     /// a given media query.
never_matching() -> Self72     fn never_matching() -> Self {
73         Self {
74             qualifier: Some(Qualifier::Not),
75             media_type: MediaQueryType::All,
76             expressions: vec![],
77         }
78     }
79 }
80 
81 impl ToCss for MediaQuery {
to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: Write,82     fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
83     where
84         W: Write,
85     {
86         if let Some(qual) = self.qualifier {
87             qual.to_css(dest)?;
88             dest.write_char(' ')?;
89         }
90 
91         match self.media_type {
92             MediaQueryType::All => {
93                 // We need to print "all" if there's a qualifier, or there's
94                 // just an empty list of expressions.
95                 //
96                 // Otherwise, we'd serialize media queries like "(min-width:
97                 // 40px)" in "all (min-width: 40px)", which is unexpected.
98                 if self.qualifier.is_some() || self.expressions.is_empty() {
99                     dest.write_str("all")?;
100                 }
101             },
102             MediaQueryType::Concrete(MediaType(ref desc)) => desc.to_css(dest)?,
103         }
104 
105         if self.expressions.is_empty() {
106             return Ok(());
107         }
108 
109         if self.media_type != MediaQueryType::All || self.qualifier.is_some() {
110             dest.write_str(" and ")?;
111         }
112 
113         self.expressions[0].to_css(dest)?;
114 
115         for expr in self.expressions.iter().skip(1) {
116             dest.write_str(" and ")?;
117             expr.to_css(dest)?;
118         }
119         Ok(())
120     }
121 }
122 
123 /// <http://dev.w3.org/csswg/mediaqueries-3/#media0>
124 #[derive(Clone, Debug, Eq, PartialEq)]
125 #[cfg_attr(feature = "servo", derive(MallocSizeOf))]
126 pub enum MediaQueryType {
127     /// A media type that matches every device.
128     All,
129     /// A specific media type.
130     Concrete(MediaType),
131 }
132 
133 impl MediaQueryType {
parse(ident: &str) -> Result<Self, ()>134     fn parse(ident: &str) -> Result<Self, ()> {
135         match_ignore_ascii_case! { ident,
136             "all" => return Ok(MediaQueryType::All),
137             _ => (),
138         };
139 
140         // If parseable, accept this type as a concrete type.
141         MediaType::parse(ident).map(MediaQueryType::Concrete)
142     }
143 
matches(&self, other: MediaType) -> bool144     fn matches(&self, other: MediaType) -> bool {
145         match *self {
146             MediaQueryType::All => true,
147             MediaQueryType::Concrete(ref known_type) => *known_type == other,
148         }
149     }
150 }
151 
152 /// <https://drafts.csswg.org/mediaqueries/#media-types>
153 #[derive(Clone, Debug, Eq, PartialEq)]
154 #[cfg_attr(feature = "servo", derive(MallocSizeOf))]
155 pub struct MediaType(pub CustomIdent);
156 
157 impl MediaType {
158     /// The `screen` media type.
screen() -> Self159     pub fn screen() -> Self {
160         MediaType(CustomIdent(atom!("screen")))
161     }
162 
163     /// The `print` media type.
print() -> Self164     pub fn print() -> Self {
165         MediaType(CustomIdent(atom!("print")))
166     }
167 
parse(name: &str) -> Result<Self, ()>168     fn parse(name: &str) -> Result<Self, ()> {
169         // From https://drafts.csswg.org/mediaqueries/#mq-syntax:
170         //
171         //   The <media-type> production does not include the keywords not, or, and, and only.
172         //
173         // Here we also perform the to-ascii-lowercase part of the serialization
174         // algorithm: https://drafts.csswg.org/cssom/#serializing-media-queries
175         match_ignore_ascii_case! { name,
176             "not" | "or" | "and" | "only" => Err(()),
177             _ => Ok(MediaType(CustomIdent(Atom::from(string_as_ascii_lowercase(name))))),
178         }
179     }
180 }
181 impl MediaQuery {
182     /// Parse a media query given css input.
183     ///
184     /// Returns an error if any of the expressions is unknown.
parse<'i, 't>(context: &ParserContext, input: &mut Parser<'i, 't>) -> Result<MediaQuery, ParseError<'i>>185     pub fn parse<'i, 't>(context: &ParserContext, input: &mut Parser<'i, 't>)
186                          -> Result<MediaQuery, ParseError<'i>> {
187         let mut expressions = vec![];
188 
189         let qualifier = if input.try(|input| input.expect_ident_matching("only")).is_ok() {
190             Some(Qualifier::Only)
191         } else if input.try(|input| input.expect_ident_matching("not")).is_ok() {
192             Some(Qualifier::Not)
193         } else {
194             None
195         };
196 
197         let media_type = match input.try(|i| i.expect_ident_cloned()) {
198             Ok(ident) => {
199                 MediaQueryType::parse(&*ident)
200                     .map_err(|()| {
201                         input.new_custom_error(
202                             SelectorParseErrorKind::UnexpectedIdent(ident.clone())
203                         )
204                     })?
205             }
206             Err(_) => {
207                 // Media type is only optional if qualifier is not specified.
208                 if qualifier.is_some() {
209                     return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
210                 }
211 
212                 // Without a media type, require at least one expression.
213                 expressions.push(Expression::parse(context, input)?);
214 
215                 MediaQueryType::All
216             }
217         };
218 
219         // Parse any subsequent expressions
220         loop {
221             if input.try(|input| input.expect_ident_matching("and")).is_err() {
222                 return Ok(MediaQuery { qualifier, media_type, expressions })
223             }
224             expressions.push(Expression::parse(context, input)?)
225         }
226     }
227 }
228 
229 /// Parse a media query list from CSS.
230 ///
231 /// Always returns a media query list. If any invalid media query is found, the
232 /// media query list is only filled with the equivalent of "not all", see:
233 ///
234 /// <https://drafts.csswg.org/mediaqueries/#error-handling>
parse_media_query_list<R>( context: &ParserContext, input: &mut Parser, error_reporter: &R, ) -> MediaList where R: ParseErrorReporter,235 pub fn parse_media_query_list<R>(
236     context: &ParserContext,
237     input: &mut Parser,
238     error_reporter: &R,
239 ) -> MediaList
240 where
241     R: ParseErrorReporter,
242 {
243     if input.is_exhausted() {
244         return MediaList::empty()
245     }
246 
247     let mut media_queries = vec![];
248     loop {
249         let start_position = input.position();
250         match input.parse_until_before(Delimiter::Comma, |i| MediaQuery::parse(context, i)) {
251             Ok(mq) => {
252                 media_queries.push(mq);
253             },
254             Err(err) => {
255                 media_queries.push(MediaQuery::never_matching());
256                 let location = err.location;
257                 let error = ContextualParseError::InvalidMediaRule(
258                     input.slice_from(start_position), err);
259                 let error_context = ParserErrorContext { error_reporter };
260                 context.log_css_error(&error_context, location, error);
261             },
262         }
263 
264         match input.next() {
265             Ok(&Token::Comma) => {},
266             Ok(_) => unreachable!(),
267             Err(_) => break,
268         }
269     }
270 
271     MediaList {
272         media_queries: media_queries,
273     }
274 }
275 
276 impl MediaList {
277     /// Evaluate a whole `MediaList` against `Device`.
evaluate(&self, device: &Device, quirks_mode: QuirksMode) -> bool278     pub fn evaluate(&self, device: &Device, quirks_mode: QuirksMode) -> bool {
279         // Check if it is an empty media query list or any queries match (OR condition)
280         // https://drafts.csswg.org/mediaqueries-4/#mq-list
281         self.media_queries.is_empty() || self.media_queries.iter().any(|mq| {
282             let media_match = mq.media_type.matches(device.media_type());
283 
284             // Check if all conditions match (AND condition)
285             let query_match =
286                 media_match &&
287                 mq.expressions.iter()
288                     .all(|expression| expression.matches(&device, quirks_mode));
289 
290             // Apply the logical NOT qualifier to the result
291             match mq.qualifier {
292                 Some(Qualifier::Not) => !query_match,
293                 _ => query_match,
294             }
295         })
296     }
297 
298     /// Whether this `MediaList` contains no media queries.
is_empty(&self) -> bool299     pub fn is_empty(&self) -> bool {
300         self.media_queries.is_empty()
301     }
302 
303     /// Append a new media query item to the media list.
304     /// <https://drafts.csswg.org/cssom/#dom-medialist-appendmedium>
305     ///
306     /// Returns true if added, false if fail to parse the medium string.
append_medium(&mut self, context: &ParserContext, new_medium: &str) -> bool307     pub fn append_medium(&mut self, context: &ParserContext, new_medium: &str) -> bool {
308         let mut input = ParserInput::new(new_medium);
309         let mut parser = Parser::new(&mut input);
310         let new_query = match MediaQuery::parse(&context, &mut parser) {
311             Ok(query) => query,
312             Err(_) => { return false; }
313         };
314         // This algorithm doesn't actually matches the current spec,
315         // but it matches the behavior of Gecko and Edge.
316         // See https://github.com/w3c/csswg-drafts/issues/697
317         self.media_queries.retain(|query| query != &new_query);
318         self.media_queries.push(new_query);
319         true
320     }
321 
322     /// Delete a media query from the media list.
323     /// <https://drafts.csswg.org/cssom/#dom-medialist-deletemedium>
324     ///
325     /// Returns true if found and deleted, false otherwise.
delete_medium(&mut self, context: &ParserContext, old_medium: &str) -> bool326     pub fn delete_medium(&mut self, context: &ParserContext, old_medium: &str) -> bool {
327         let mut input = ParserInput::new(old_medium);
328         let mut parser = Parser::new(&mut input);
329         let old_query = match MediaQuery::parse(context, &mut parser) {
330             Ok(query) => query,
331             Err(_) => { return false; }
332         };
333         let old_len = self.media_queries.len();
334         self.media_queries.retain(|query| query != &old_query);
335         old_len != self.media_queries.len()
336     }
337 }
338