diff options
Diffstat (limited to 'servo/components/style/media_queries')
-rw-r--r-- | servo/components/style/media_queries/media_list.rs | 150 | ||||
-rw-r--r-- | servo/components/style/media_queries/media_query.rs | 193 | ||||
-rw-r--r-- | servo/components/style/media_queries/mod.rs | 18 |
3 files changed, 361 insertions, 0 deletions
diff --git a/servo/components/style/media_queries/media_list.rs b/servo/components/style/media_queries/media_list.rs new file mode 100644 index 0000000000..3c2ba9ee5c --- /dev/null +++ b/servo/components/style/media_queries/media_list.rs @@ -0,0 +1,150 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +//! A media query list: +//! +//! https://drafts.csswg.org/mediaqueries/#typedef-media-query-list + +use super::{Device, MediaQuery, Qualifier}; +use crate::context::QuirksMode; +use crate::error_reporting::ContextualParseError; +use crate::parser::ParserContext; +use crate::queries::condition::KleeneValue; +use crate::values::computed; +use cssparser::{Delimiter, Parser}; +use cssparser::{ParserInput, Token}; + +/// A type that encapsulates a media query list. +#[derive(Clone, MallocSizeOf, ToCss, ToShmem)] +#[css(comma, derive_debug)] +pub struct MediaList { + /// The list of media queries. + #[css(iterable)] + pub media_queries: Vec<MediaQuery>, +} + +impl MediaList { + /// 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(context: &ParserContext, input: &mut Parser) -> Self { + if input.is_exhausted() { + return Self::empty(); + } + + let mut media_queries = vec![]; + loop { + let start_position = input.position(); + 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 location = err.location; + let error = ContextualParseError::InvalidMediaRule( + input.slice_from(start_position), + err, + ); + context.log_css_error(location, error); + }, + } + + match input.next() { + Ok(&Token::Comma) => {}, + Ok(_) => unreachable!(), + Err(_) => break, + } + } + + MediaList { media_queries } + } + + /// Create an empty MediaList. + pub fn empty() -> Self { + MediaList { + media_queries: vec![], + } + } + + /// 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. + // https://drafts.csswg.org/mediaqueries-4/#mq-list + if self.media_queries.is_empty() { + return true; + } + + computed::Context::for_media_query_evaluation(device, quirks_mode, |context| { + self.media_queries.iter().any(|mq| { + let mut query_match = if mq.media_type.matches(device.media_type()) { + mq.condition + .as_ref() + .map_or(KleeneValue::True, |c| c.matches(context)) + } else { + KleeneValue::False + }; + + // Apply the logical NOT qualifier to the result + if matches!(mq.qualifier, Some(Qualifier::Not)) { + query_match = !query_match; + } + query_match.to_bool(/* unknown = */ false) + }) + }) + } + + /// Whether this `MediaList` contains no media queries. + pub fn is_empty(&self) -> bool { + self.media_queries.is_empty() + } + + /// Whether this `MediaList` depends on the viewport size. + pub fn is_viewport_dependent(&self) -> bool { + self.media_queries.iter().any(|q| q.is_viewport_dependent()) + } + + /// 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() + } +} diff --git a/servo/components/style/media_queries/media_query.rs b/servo/components/style/media_queries/media_query.rs new file mode 100644 index 0000000000..c30a445393 --- /dev/null +++ b/servo/components/style/media_queries/media_query.rs @@ -0,0 +1,193 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +//! A media query: +//! +//! https://drafts.csswg.org/mediaqueries/#typedef-media-query + +use crate::parser::ParserContext; +use crate::queries::{FeatureFlags, FeatureType, QueryCondition}; +use crate::str::string_as_ascii_lowercase; +use crate::values::CustomIdent; +use crate::Atom; +use cssparser::Parser; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError, ToCss}; + +/// <https://drafts.csswg.org/mediaqueries/#mq-prefix> +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, ToCss, ToShmem)] +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, +} + +/// <https://drafts.csswg.org/mediaqueries/#media-types> +#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)] +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 only, not, and, or, and layer. + // + // 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" | "layer" => Err(()), + _ => Ok(MediaType(CustomIdent(Atom::from(string_as_ascii_lowercase(name))))), + } + } +} + +/// A [media query][mq]. +/// +/// [mq]: https://drafts.csswg.org/mediaqueries/ +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] +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 condition that this media query contains. This cannot have `or` + /// in the first level. + pub condition: Option<QueryCondition>, +} + +impl ToCss for MediaQuery { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: 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.condition.is_none() { + dest.write_str("all")?; + } + }, + MediaQueryType::Concrete(MediaType(ref desc)) => desc.to_css(dest)?, + } + + let condition = match self.condition { + Some(ref c) => c, + None => return Ok(()), + }; + + if self.media_type != MediaQueryType::All || self.qualifier.is_some() { + dest.write_str(" and ")?; + } + + condition.to_css(dest) + } +} + +impl MediaQuery { + /// Return a media query that never matches, used for when we fail to parse + /// a given media query. + pub fn never_matching() -> Self { + Self { + qualifier: Some(Qualifier::Not), + media_type: MediaQueryType::All, + condition: None, + } + } + + /// Returns whether this media query depends on the viewport. + pub fn is_viewport_dependent(&self) -> bool { + self.condition.as_ref().map_or(false, |c| { + return c + .cumulative_flags() + .contains(FeatureFlags::VIEWPORT_DEPENDENT); + }) + } + + /// 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<Self, ParseError<'i>> { + let (qualifier, explicit_media_type) = input + .try_parse(|input| -> Result<_, ()> { + let qualifier = input.try_parse(Qualifier::parse).ok(); + let ident = input.expect_ident().map_err(|_| ())?; + let media_type = MediaQueryType::parse(&ident)?; + Ok((qualifier, Some(media_type))) + }) + .unwrap_or_default(); + + let condition = if explicit_media_type.is_none() { + Some(QueryCondition::parse(context, input, FeatureType::Media)?) + } else if input.try_parse(|i| i.expect_ident_matching("and")).is_ok() { + Some(QueryCondition::parse_disallow_or( + context, + input, + FeatureType::Media, + )?) + } else { + None + }; + + let media_type = explicit_media_type.unwrap_or(MediaQueryType::All); + Ok(Self { + qualifier, + media_type, + condition, + }) + } +} + +/// <http://dev.w3.org/csswg/mediaqueries-3/#media0> +#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)] +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) + } + + /// Returns whether this media query type matches a MediaType. + pub fn matches(&self, other: MediaType) -> bool { + match *self { + MediaQueryType::All => true, + MediaQueryType::Concrete(ref known_type) => *known_type == other, + } + } +} diff --git a/servo/components/style/media_queries/mod.rs b/servo/components/style/media_queries/mod.rs new file mode 100644 index 0000000000..833f6f53cb --- /dev/null +++ b/servo/components/style/media_queries/mod.rs @@ -0,0 +1,18 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +//! [Media queries][mq]. +//! +//! [mq]: https://drafts.csswg.org/mediaqueries/ + +mod media_list; +mod media_query; + +pub use self::media_list::MediaList; +pub use self::media_query::{MediaQuery, MediaQueryType, MediaType, Qualifier}; + +#[cfg(feature = "gecko")] +pub use crate::gecko::media_queries::Device; +#[cfg(feature = "servo")] +pub use crate::servo::media_queries::Device; |