gecko-dev/servo/components/style/media_queries.rs
Fernando Jiménez Moreno efe49af334 servo: Merge #18341 - stylo: Error reporting for unknown media features (from ferjm:bug1384225.media.errors); r=jdm
- [X] `./mach build -d` does not report any errors
- [X] `./mach test-tidy` does not report any errors
- [X] These changes fix [bug 1384225](https://bugzilla.mozilla.org/show_bug.cgi?id=1384225)

Source-Repo: https://github.com/servo/servo
Source-Revision: fef2cfde8cc6688c63cde1966b081a4698a5d977

--HG--
extra : subtree_source : https%3A//hg.mozilla.org/projects/converted-servo-linear
extra : subtree_revision : f16c453db1e3cdccb516562ef92dc4ebce3e9cf1
2017-09-08 11:14:51 -05:00

348 lines
11 KiB
Rust

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
//! [Media queries][mq].
//!
//! [mq]: https://drafts.csswg.org/mediaqueries/
use Atom;
use context::QuirksMode;
use cssparser::{Delimiter, Parser};
use cssparser::{Token, ParserInput};
use error_reporting::{ContextualParseError, ParseErrorReporter};
use parser::{ParserContext, ParserErrorContext};
use selectors::parser::SelectorParseError;
use serialize_comma_separated_list;
use std::fmt;
use str::string_as_ascii_lowercase;
use style_traits::{ToCss, ParseError, StyleParseError};
use values::CustomIdent;
#[cfg(feature = "servo")]
pub use servo::media_queries::{Device, Expression};
#[cfg(feature = "gecko")]
pub use gecko::media_queries::{Device, Expression};
/// A type that encapsulates a media query list.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "servo", derive(HeapSizeOf))]
pub struct MediaList {
/// The list of media queries.
pub media_queries: Vec<MediaQuery>,
}
impl ToCss for MediaList {
fn to_css<W>(&self, dest: &mut W) -> fmt::Result
where W: fmt::Write
{
serialize_comma_separated_list(dest, &self.media_queries)
}
}
impl MediaList {
/// Create an empty MediaList.
pub fn empty() -> Self {
MediaList { media_queries: vec![] }
}
}
/// https://drafts.csswg.org/mediaqueries/#mq-prefix
#[cfg_attr(feature = "servo", derive(HeapSizeOf))]
#[derive(Clone, Copy, Debug, Eq, PartialEq, ToCss)]
pub enum Qualifier {
/// Hide a media query from legacy UAs:
/// https://drafts.csswg.org/mediaqueries/#mq-only
Only,
/// Negate a media query:
/// https://drafts.csswg.org/mediaqueries/#mq-not
Not,
}
/// A [media query][mq].
///
/// [mq]: https://drafts.csswg.org/mediaqueries/
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "servo", derive(HeapSizeOf))]
pub struct MediaQuery {
/// The qualifier for this query.
pub qualifier: Option<Qualifier>,
/// The media type for this query, that can be known, unknown, or "all".
pub media_type: MediaQueryType,
/// The set of expressions that this media query contains.
pub expressions: Vec<Expression>,
}
impl MediaQuery {
/// Return a media query that never matches, used for when we fail to parse
/// a given media query.
fn never_matching() -> Self {
Self::new(Some(Qualifier::Not), MediaQueryType::All, vec![])
}
/// Trivially constructs a new media query.
pub fn new(qualifier: Option<Qualifier>,
media_type: MediaQueryType,
expressions: Vec<Expression>) -> MediaQuery {
MediaQuery {
qualifier: qualifier,
media_type: media_type,
expressions: expressions,
}
}
}
impl ToCss for MediaQuery {
fn to_css<W>(&self, dest: &mut W) -> fmt::Result
where W: fmt::Write,
{
if let Some(qual) = self.qualifier {
qual.to_css(dest)?;
dest.write_char(' ')?;
}
match self.media_type {
MediaQueryType::All => {
// We need to print "all" if there's a qualifier, or there's
// just an empty list of expressions.
//
// Otherwise, we'd serialize media queries like "(min-width:
// 40px)" in "all (min-width: 40px)", which is unexpected.
if self.qualifier.is_some() || self.expressions.is_empty() {
dest.write_str("all")?;
}
},
MediaQueryType::Concrete(MediaType(ref desc)) => desc.to_css(dest)?,
}
if self.expressions.is_empty() {
return Ok(());
}
if self.media_type != MediaQueryType::All || self.qualifier.is_some() {
dest.write_str(" and ")?;
}
self.expressions[0].to_css(dest)?;
for expr in self.expressions.iter().skip(1) {
dest.write_str(" and ")?;
expr.to_css(dest)?;
}
Ok(())
}
}
/// http://dev.w3.org/csswg/mediaqueries-3/#media0
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "servo", derive(HeapSizeOf))]
pub enum MediaQueryType {
/// A media type that matches every device.
All,
/// A specific media type.
Concrete(MediaType),
}
impl MediaQueryType {
fn parse(ident: &str) -> Result<Self, ()> {
match_ignore_ascii_case! { ident,
"all" => return Ok(MediaQueryType::All),
_ => (),
};
// If parseable, accept this type as a concrete type.
MediaType::parse(ident).map(MediaQueryType::Concrete)
}
fn matches(&self, other: MediaType) -> bool {
match *self {
MediaQueryType::All => true,
MediaQueryType::Concrete(ref known_type) => *known_type == other,
}
}
}
/// https://drafts.csswg.org/mediaqueries/#media-types
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "servo", derive(HeapSizeOf))]
pub struct MediaType(pub CustomIdent);
impl MediaType {
/// The `screen` media type.
pub fn screen() -> Self {
MediaType(CustomIdent(atom!("screen")))
}
/// The `print` media type.
pub fn print() -> Self {
MediaType(CustomIdent(atom!("print")))
}
fn parse(name: &str) -> Result<Self, ()> {
// From https://drafts.csswg.org/mediaqueries/#mq-syntax:
//
// The <media-type> production does not include the keywords not, or, and, and only.
//
// Here we also perform the to-ascii-lowercase part of the serialization
// algorithm: https://drafts.csswg.org/cssom/#serializing-media-queries
match_ignore_ascii_case! { name,
"not" | "or" | "and" | "only" => Err(()),
_ => Ok(MediaType(CustomIdent(Atom::from(string_as_ascii_lowercase(name))))),
}
}
}
impl MediaQuery {
/// Parse a media query given css input.
///
/// Returns an error if any of the expressions is unknown.
pub fn parse<'i, 't>(context: &ParserContext, input: &mut Parser<'i, 't>)
-> Result<MediaQuery, ParseError<'i>> {
let mut expressions = vec![];
let qualifier = if input.try(|input| input.expect_ident_matching("only")).is_ok() {
Some(Qualifier::Only)
} else if input.try(|input| input.expect_ident_matching("not")).is_ok() {
Some(Qualifier::Not)
} else {
None
};
let media_type = match input.try(|i| i.expect_ident_cloned()) {
Ok(ident) => {
let result: Result<_, ParseError> = MediaQueryType::parse(&*ident)
.map_err(|()| SelectorParseError::UnexpectedIdent(ident.clone()).into());
result?
}
Err(_) => {
// Media type is only optional if qualifier is not specified.
if qualifier.is_some() {
return Err(StyleParseError::UnspecifiedError.into())
}
// Without a media type, require at least one expression.
expressions.push(Expression::parse(context, input)?);
MediaQueryType::All
}
};
// Parse any subsequent expressions
loop {
if input.try(|input| input.expect_ident_matching("and")).is_err() {
return Ok(MediaQuery::new(qualifier, media_type, expressions))
}
expressions.push(Expression::parse(context, input)?)
}
}
}
/// Parse a media query list from CSS.
///
/// Always returns a media query list. If any invalid media query is found, the
/// media query list is only filled with the equivalent of "not all", see:
///
/// https://drafts.csswg.org/mediaqueries/#error-handling
pub fn parse_media_query_list<R>(
context: &ParserContext,
input: &mut Parser,
error_reporter: &R,
) -> MediaList
where
R: ParseErrorReporter,
{
if input.is_exhausted() {
return MediaList::empty()
}
let mut media_queries = vec![];
loop {
let start_position = input.position();
let start_location = input.current_source_location();
match input.parse_until_before(Delimiter::Comma, |i| MediaQuery::parse(context, i)) {
Ok(mq) => {
media_queries.push(mq);
},
Err(err) => {
media_queries.push(MediaQuery::never_matching());
let error = ContextualParseError::InvalidMediaRule(
input.slice_from(start_position), err);
let error_context = ParserErrorContext { error_reporter };
context.log_css_error(&error_context, start_location, error);
},
}
match input.next() {
Ok(&Token::Comma) => {},
Ok(_) => unreachable!(),
Err(_) => break,
}
}
MediaList {
media_queries: media_queries,
}
}
impl MediaList {
/// Evaluate a whole `MediaList` against `Device`.
pub fn evaluate(&self, device: &Device, quirks_mode: QuirksMode) -> bool {
// Check if it is an empty media query list or any queries match (OR condition)
// https://drafts.csswg.org/mediaqueries-4/#mq-list
self.media_queries.is_empty() || self.media_queries.iter().any(|mq| {
let media_match = mq.media_type.matches(device.media_type());
// Check if all conditions match (AND condition)
let query_match =
media_match &&
mq.expressions.iter()
.all(|expression| expression.matches(&device, quirks_mode));
// Apply the logical NOT qualifier to the result
match mq.qualifier {
Some(Qualifier::Not) => !query_match,
_ => query_match,
}
})
}
/// Whether this `MediaList` contains no media queries.
pub fn is_empty(&self) -> bool {
self.media_queries.is_empty()
}
/// Append a new media query item to the media list.
/// https://drafts.csswg.org/cssom/#dom-medialist-appendmedium
///
/// Returns true if added, false if fail to parse the medium string.
pub fn append_medium(&mut self, context: &ParserContext, new_medium: &str) -> bool {
let mut input = ParserInput::new(new_medium);
let mut parser = Parser::new(&mut input);
let new_query = match MediaQuery::parse(&context, &mut parser) {
Ok(query) => query,
Err(_) => { return false; }
};
// This algorithm doesn't actually matches the current spec,
// but it matches the behavior of Gecko and Edge.
// See https://github.com/w3c/csswg-drafts/issues/697
self.media_queries.retain(|query| query != &new_query);
self.media_queries.push(new_query);
true
}
/// Delete a media query from the media list.
/// https://drafts.csswg.org/cssom/#dom-medialist-deletemedium
///
/// Returns true if found and deleted, false otherwise.
pub fn delete_medium(&mut self, context: &ParserContext, old_medium: &str) -> bool {
let mut input = ParserInput::new(old_medium);
let mut parser = Parser::new(&mut input);
let old_query = match MediaQuery::parse(context, &mut parser) {
Ok(query) => query,
Err(_) => { return false; }
};
let old_len = self.media_queries.len();
self.media_queries.retain(|query| query != &old_query);
old_len != self.media_queries.len()
}
}