summaryrefslogtreecommitdiffstats
path: root/servo/components/style/media_queries
diff options
context:
space:
mode:
Diffstat (limited to 'servo/components/style/media_queries')
-rw-r--r--servo/components/style/media_queries/media_list.rs150
-rw-r--r--servo/components/style/media_queries/media_query.rs193
-rw-r--r--servo/components/style/media_queries/mod.rs18
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;