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