//! Media queries. use crate::compat::Feature; use crate::error::{ErrorWithLocation, MinifyError, MinifyErrorKind, ParserError, PrinterError}; use crate::macros::enum_property; use crate::printer::Printer; use crate::properties::custom::EnvironmentVariable; use crate::rules::custom_media::CustomMediaRule; use crate::rules::Location; use crate::stylesheet::ParserOptions; use crate::targets::Browsers; use crate::traits::{Parse, ToCss}; use crate::values::ident::Ident; use crate::values::number::CSSNumber; use crate::values::string::CowArcStr; use crate::values::{length::Length, ratio::Ratio, resolution::Resolution}; #[cfg(feature = "visitor")] use crate::visitor::Visit; use cssparser::*; use std::collections::{HashMap, HashSet}; #[cfg(feature = "serde")] use crate::serialization::ValueWrapper; /// A [media query list](https://drafts.csswg.org/mediaqueries/#mq-list). #[derive(Clone, Debug, PartialEq, Default)] #[cfg_attr(feature = "visitor", derive(Visit), visit(visit_media_list, MEDIA_QUERIES))] #[cfg_attr(feature = "into_owned", derive(lightningcss_derive::IntoOwned))] #[cfg_attr( feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(rename_all = "camelCase") )] #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] pub struct MediaList<'i> { /// The list of media queries. #[cfg_attr(feature = "serde", serde(borrow))] pub media_queries: Vec>, } impl<'i> MediaList<'i> { /// Creates an empty media query list. pub fn new() -> Self { MediaList { media_queries: vec![] } } /// Parse a media query list from CSS. pub fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { let mut media_queries = vec![]; loop { match input.parse_until_before(Delimiter::Comma, |i| MediaQuery::parse(i)) { Ok(mq) => { media_queries.push(mq); } Err(err) => match err.kind { ParseErrorKind::Basic(BasicParseErrorKind::EndOfInput) => break, _ => return Err(err), }, } match input.next() { Ok(&Token::Comma) => {} Ok(_) => unreachable!(), Err(_) => break, } } Ok(MediaList { media_queries }) } pub(crate) fn transform_custom_media( &mut self, loc: Location, custom_media: &HashMap, CustomMediaRule<'i>>, ) -> Result<(), MinifyError> { for query in self.media_queries.iter_mut() { query.transform_custom_media(loc, custom_media)?; } Ok(()) } /// Returns whether the media query list always matches. pub fn always_matches(&self) -> bool { // If the media list is empty, it always matches. self.media_queries.is_empty() || self.media_queries.iter().all(|mq| mq.always_matches()) } /// Returns whether the media query list never matches. pub fn never_matches(&self) -> bool { !self.media_queries.is_empty() && self.media_queries.iter().all(|mq| mq.never_matches()) } /// Attempts to combine the given media query list into this one. The resulting media query /// list matches if both the original media query lists would have matched. /// /// Returns an error if the boolean logic is not possible. pub fn and(&mut self, b: &MediaList<'i>) -> Result<(), ()> { if self.media_queries.is_empty() { self.media_queries.extend(b.media_queries.iter().cloned()); return Ok(()); } for b in &b.media_queries { if self.media_queries.contains(&b) { continue; } for a in &mut self.media_queries { a.and(&b)?; } } Ok(()) } /// Combines the given media query list into this one. The resulting media query list /// matches if either of the original media query lists would have matched. pub fn or(&mut self, b: &MediaList<'i>) { for mq in &b.media_queries { if !self.media_queries.contains(&mq) { self.media_queries.push(mq.clone()) } } } } impl<'i> ToCss for MediaList<'i> { fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { if self.media_queries.is_empty() { dest.write_str("not all")?; return Ok(()); } let mut first = true; for query in &self.media_queries { if !first { dest.delim(',', false)?; } first = false; query.to_css(dest)?; } Ok(()) } } enum_property! { /// A [media query qualifier](https://drafts.csswg.org/mediaqueries/#mq-prefix). pub enum Qualifier { /// Prevents older browsers from matching the media query. Only, /// Negates a media query. Not, } } /// A [media type](https://drafts.csswg.org/mediaqueries/#media-types) within a media query. #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr(feature = "into_owned", derive(lightningcss_derive::IntoOwned))] #[cfg_attr( feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(rename_all = "kebab-case", into = "CowArcStr", from = "CowArcStr") )] pub enum MediaType<'i> { /// Matches all devices. All, /// Matches printers, and devices intended to reproduce a printed /// display, such as a web browser showing a document in “Print Preview”. Print, /// Matches all devices that aren’t matched by print. Screen, /// An unknown media type. #[cfg_attr(feature = "serde", serde(borrow))] Custom(CowArcStr<'i>), } impl<'i> From> for MediaType<'i> { fn from(name: CowArcStr<'i>) -> Self { match_ignore_ascii_case! { &*name, "all" => MediaType::All, "print" => MediaType::Print, "screen" => MediaType::Screen, _ => MediaType::Custom(name) } } } impl<'i> Into> for MediaType<'i> { fn into(self) -> CowArcStr<'i> { match self { MediaType::All => "all".into(), MediaType::Print => "print".into(), MediaType::Screen => "screen".into(), MediaType::Custom(desc) => desc, } } } impl<'i> Parse<'i> for MediaType<'i> { fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { let name: CowArcStr = input.expect_ident()?.into(); Ok(Self::from(name)) } } #[cfg(feature = "jsonschema")] #[cfg_attr(docsrs, doc(cfg(feature = "jsonschema")))] impl<'a> schemars::JsonSchema for MediaType<'a> { fn is_referenceable() -> bool { true } fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { str::json_schema(gen) } fn schema_name() -> String { "MediaType".into() } } /// A [media query](https://drafts.csswg.org/mediaqueries/#media). #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr(feature = "into_owned", derive(lightningcss_derive::IntoOwned))] #[cfg_attr(feature = "visitor", visit(visit_media_query, MEDIA_QUERIES))] #[cfg_attr( feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(rename_all = "camelCase") )] #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] pub struct MediaQuery<'i> { /// The qualifier for this query. pub qualifier: Option, /// The media type for this query, that can be known, unknown, or "all". #[cfg_attr(feature = "serde", serde(borrow))] pub media_type: MediaType<'i>, /// The condition that this media query contains. This cannot have `or` /// in the first level. pub condition: Option>, } impl<'i> MediaQuery<'i> { /// Parse a media query given css input. /// /// Returns an error if any of the expressions is unknown. pub fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { let (qualifier, explicit_media_type) = input .try_parse(|input| -> Result<_, ParseError<'i, ParserError<'i>>> { let qualifier = input.try_parse(Qualifier::parse).ok(); let media_type = MediaType::parse(input)?; Ok((qualifier, Some(media_type))) }) .unwrap_or_default(); let condition = if explicit_media_type.is_none() { Some(MediaCondition::parse(input, true)?) } else if input.try_parse(|i| i.expect_ident_matching("and")).is_ok() { Some(MediaCondition::parse(input, false)?) } else { None }; let media_type = explicit_media_type.unwrap_or(MediaType::All); Ok(Self { qualifier, media_type, condition, }) } fn transform_custom_media( &mut self, loc: Location, custom_media: &HashMap, CustomMediaRule<'i>>, ) -> Result<(), MinifyError> { if let Some(condition) = &mut self.condition { let used = process_condition( loc, custom_media, &mut self.media_type, &mut self.qualifier, condition, &mut HashSet::new(), )?; if !used { self.condition = None; } } Ok(()) } /// Returns whether the media query is guaranteed to always match. pub fn always_matches(&self) -> bool { self.qualifier == None && self.media_type == MediaType::All && self.condition == None } /// Returns whether the media query is guaranteed to never match. pub fn never_matches(&self) -> bool { self.qualifier == Some(Qualifier::Not) && self.media_type == MediaType::All && self.condition == None } /// Attempts to combine the given media query into this one. The resulting media query /// matches if both of the original media queries would have matched. /// /// Returns an error if the boolean logic is not possible. pub fn and<'a>(&mut self, b: &MediaQuery<'i>) -> Result<(), ()> { let at = (&self.qualifier, &self.media_type); let bt = (&b.qualifier, &b.media_type); let (qualifier, media_type) = match (at, bt) { // `not all and screen` => not all // `screen and not all` => not all ((&Some(Qualifier::Not), &MediaType::All), _) | (_, (&Some(Qualifier::Not), &MediaType::All)) => (Some(Qualifier::Not), MediaType::All), // `not screen and not print` => ERROR // `not screen and not screen` => not screen ((&Some(Qualifier::Not), a), (&Some(Qualifier::Not), b)) => { if a == b { (Some(Qualifier::Not), a.clone()) } else { return Err(()) } }, // `all and print` => print // `print and all` => print // `all and not print` => not print ((_, MediaType::All), (q, t)) | ((q, t), (_, MediaType::All)) | // `not screen and print` => print // `print and not screen` => print ((&Some(Qualifier::Not), _), (q, t)) | ((q, t), (&Some(Qualifier::Not), _)) => (q.clone(), t.clone()), // `print and screen` => not all ((_, a), (_, b)) if a != b => (Some(Qualifier::Not), MediaType::All), ((_, a), _) => (None, a.clone()) }; self.qualifier = qualifier; self.media_type = media_type; if let Some(cond) = &b.condition { self.condition = if let Some(condition) = &self.condition { if condition != cond { Some(MediaCondition::Operation { conditions: vec![condition.clone(), cond.clone()], operator: Operator::And, }) } else { Some(condition.clone()) } } else { Some(cond.clone()) } } Ok(()) } } impl<'i> ToCss for MediaQuery<'i> { fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { if let Some(qual) = self.qualifier { qual.to_css(dest)?; dest.write_char(' ')?; } match self.media_type { MediaType::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")?; } } MediaType::Print => dest.write_str("print")?, MediaType::Screen => dest.write_str("screen")?, MediaType::Custom(ref desc) => dest.write_str(desc)?, } let condition = match self.condition { Some(ref c) => c, None => return Ok(()), }; let needs_parens = if self.media_type != MediaType::All || self.qualifier.is_some() { dest.write_str(" and ")?; matches!(condition, MediaCondition::Operation { operator, .. } if *operator != Operator::And) } else { false }; condition.to_css_with_parens_if_needed(dest, needs_parens) } } enum_property! { /// A binary `and` or `or` operator. pub enum Operator { /// The `and` operator. And, /// The `or` operator. Or, } } /// Represents a media condition. #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr(feature = "into_owned", derive(lightningcss_derive::IntoOwned))] #[cfg_attr( feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(tag = "type", rename_all = "kebab-case") )] #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] pub enum MediaCondition<'i> { /// A media feature, implicitly parenthesized. #[cfg_attr(feature = "serde", serde(borrow, with = "ValueWrapper::"))] Feature(MediaFeature<'i>), /// A negation of a condition. #[cfg_attr(feature = "visitor", skip_type)] #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::>"))] Not(Box>), /// A set of joint operations. #[cfg_attr(feature = "visitor", skip_type)] Operation { /// The operator for the conditions. operator: Operator, /// The conditions for the operator. conditions: Vec>, }, } impl<'i> MediaCondition<'i> { /// Parse a single media condition. pub fn parse<'t>(input: &mut Parser<'i, 't>, allow_or: bool) -> Result>> { let location = input.current_source_location(); let is_negation = match *input.next()? { Token::ParenthesisBlock => false, Token::Ident(ref ident) if ident.eq_ignore_ascii_case("not") => true, ref t => return Err(location.new_unexpected_token_error(t.clone())), }; if is_negation { let inner_condition = Self::parse_in_parens(input)?; return Ok(MediaCondition::Not(Box::new(inner_condition))); } // ParenthesisBlock. let first_condition = Self::parse_paren_block(input)?; let operator = match input.try_parse(Operator::parse) { Ok(op) => op, Err(..) => return Ok(first_condition), }; if !allow_or && operator == Operator::Or { return Err(location.new_custom_error(ParserError::InvalidMediaQuery)); } let mut conditions = vec![]; conditions.push(first_condition); conditions.push(Self::parse_in_parens(input)?); let delim = match operator { Operator::And => "and", Operator::Or => "or", }; loop { if input.try_parse(|i| i.expect_ident_matching(delim)).is_err() { return Ok(MediaCondition::Operation { conditions, operator }); } conditions.push(Self::parse_in_parens(input)?); } } /// Parse a media condition in parentheses. pub fn parse_in_parens<'t>(input: &mut Parser<'i, 't>) -> Result>> { input.expect_parenthesis_block()?; Self::parse_paren_block(input) } fn parse_paren_block<'t>(input: &mut Parser<'i, 't>) -> Result>> { input.parse_nested_block(|input| { if let Ok(inner) = input.try_parse(|i| Self::parse(i, true)) { return Ok(inner); } let feature = MediaFeature::parse(input)?; Ok(MediaCondition::Feature(feature)) }) } fn needs_parens(&self, parent_operator: Option, targets: &Option) -> bool { match self { MediaCondition::Not(_) => true, MediaCondition::Operation { operator, .. } => Some(*operator) != parent_operator, MediaCondition::Feature(f) => { parent_operator != Some(Operator::And) && targets.is_some() && matches!(f, MediaFeature::Interval { .. }) && !Feature::MediaIntervalSyntax.is_compatible(targets.unwrap()) } } } fn to_css_with_parens_if_needed(&self, dest: &mut Printer, needs_parens: bool) -> Result<(), PrinterError> where W: std::fmt::Write, { if needs_parens { dest.write_char('(')?; } self.to_css(dest)?; if needs_parens { dest.write_char(')')?; } Ok(()) } } impl<'i> ToCss for MediaCondition<'i> { fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { match *self { MediaCondition::Feature(ref f) => f.to_css(dest), MediaCondition::Not(ref c) => { dest.write_str("not ")?; c.to_css_with_parens_if_needed(dest, c.needs_parens(None, &dest.targets)) } MediaCondition::Operation { ref conditions, operator, } => { let mut iter = conditions.iter(); let first = iter.next().unwrap(); first.to_css_with_parens_if_needed(dest, first.needs_parens(Some(operator), &dest.targets))?; for item in iter { dest.write_char(' ')?; operator.to_css(dest)?; dest.write_char(' ')?; item.to_css_with_parens_if_needed(dest, item.needs_parens(Some(operator), &dest.targets))?; } Ok(()) } } } } /// A [comparator](https://drafts.csswg.org/mediaqueries/#typedef-mf-comparison) within a media query. #[derive(Clone, Copy, Debug, Eq, PartialEq)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr( feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(rename_all = "kebab-case") )] #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] pub enum MediaFeatureComparison { /// `=` Equal, /// `>` GreaterThan, /// `>=` GreaterThanEqual, /// `<` LessThan, /// `<=` LessThanEqual, } impl ToCss for MediaFeatureComparison { fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { use MediaFeatureComparison::*; match self { Equal => dest.delim('=', true), GreaterThan => dest.delim('>', true), GreaterThanEqual => { dest.whitespace()?; dest.write_str(">=")?; dest.whitespace() } LessThan => dest.delim('<', true), LessThanEqual => { dest.whitespace()?; dest.write_str("<=")?; dest.whitespace() } } } } impl MediaFeatureComparison { fn opposite(&self) -> MediaFeatureComparison { match self { MediaFeatureComparison::GreaterThan => MediaFeatureComparison::LessThan, MediaFeatureComparison::GreaterThanEqual => MediaFeatureComparison::LessThanEqual, MediaFeatureComparison::LessThan => MediaFeatureComparison::GreaterThan, MediaFeatureComparison::LessThanEqual => MediaFeatureComparison::GreaterThanEqual, MediaFeatureComparison::Equal => MediaFeatureComparison::Equal, } } } /// A [media feature](https://drafts.csswg.org/mediaqueries/#typedef-media-feature) #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "visitor", derive(Visit), visit(visit_media_feature, MEDIA_QUERIES))] #[cfg_attr(feature = "into_owned", derive(lightningcss_derive::IntoOwned))] #[cfg_attr( feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(tag = "type", rename_all = "kebab-case") )] #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] pub enum MediaFeature<'i> { /// A plain media feature, e.g. `(min-width: 240px)`. Plain { /// The name of the feature. #[cfg_attr(feature = "serde", serde(borrow))] name: Ident<'i>, /// The feature value. value: MediaFeatureValue<'i>, }, /// A boolean feature, e.g. `(hover)`. Boolean { /// The name of the feature. name: Ident<'i>, }, /// A range, e.g. `(width > 240px)`. Range { /// The name of the feature. name: Ident<'i>, /// A comparator. operator: MediaFeatureComparison, /// The feature value. value: MediaFeatureValue<'i>, }, /// An interval, e.g. `(120px < width < 240px)`. #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] Interval { /// The name of the feature. name: Ident<'i>, /// A start value. start: MediaFeatureValue<'i>, /// A comparator for the start value. start_operator: MediaFeatureComparison, /// The end value. end: MediaFeatureValue<'i>, /// A comparator for the end value. end_operator: MediaFeatureComparison, }, } impl<'i> Parse<'i> for MediaFeature<'i> { fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { if let Ok(res) = input.try_parse(Self::parse_name_first) { return Ok(res); } Self::parse_value_first(input) } } impl<'i> MediaFeature<'i> { fn parse_name_first<'t>(input: &mut Parser<'i, 't>) -> Result>> { let name = Ident::parse(input)?; let operator = input.try_parse(|input| consume_operation_or_colon(input, true)); let operator = match operator { Err(..) => return Ok(MediaFeature::Boolean { name }), Ok(operator) => operator, }; let value = MediaFeatureValue::parse(input)?; if let Some(operator) = operator { Ok(MediaFeature::Range { name, operator, value }) } else { Ok(MediaFeature::Plain { name, value }) } } fn parse_value_first<'t>(input: &mut Parser<'i, 't>) -> Result>> { let value = MediaFeatureValue::parse(input)?; let operator = consume_operation_or_colon(input, false)?; let name = Ident::parse(input)?; if let Ok(end_operator) = input.try_parse(|input| consume_operation_or_colon(input, false)) { let start_operator = operator.unwrap(); let end_operator = end_operator.unwrap(); // Start and end operators must be matching. match (start_operator, end_operator) { (MediaFeatureComparison::GreaterThan, MediaFeatureComparison::GreaterThan) | (MediaFeatureComparison::GreaterThan, MediaFeatureComparison::GreaterThanEqual) | (MediaFeatureComparison::GreaterThanEqual, MediaFeatureComparison::GreaterThanEqual) | (MediaFeatureComparison::GreaterThanEqual, MediaFeatureComparison::GreaterThan) | (MediaFeatureComparison::LessThan, MediaFeatureComparison::LessThan) | (MediaFeatureComparison::LessThan, MediaFeatureComparison::LessThanEqual) | (MediaFeatureComparison::LessThanEqual, MediaFeatureComparison::LessThanEqual) | (MediaFeatureComparison::LessThanEqual, MediaFeatureComparison::LessThan) => {} _ => return Err(input.new_custom_error(ParserError::InvalidMediaQuery)), }; let end_value = MediaFeatureValue::parse(input)?; Ok(MediaFeature::Interval { name, start: value, start_operator, end: end_value, end_operator, }) } else { let operator = operator.unwrap().opposite(); Ok(MediaFeature::Range { name, operator, value }) } } } impl<'i> ToCss for MediaFeature<'i> { fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { dest.write_char('(')?; match self { MediaFeature::Boolean { name } => { name.to_css(dest)?; } MediaFeature::Plain { name, value } => { name.to_css(dest)?; dest.delim(':', false)?; value.to_css(dest)?; } MediaFeature::Range { name, operator, value } => { // If range syntax is unsupported, use min/max prefix if possible. if let Some(targets) = dest.targets { if !Feature::MediaRangeSyntax.is_compatible(targets) { return write_min_max(operator, name, value, dest); } } name.to_css(dest)?; operator.to_css(dest)?; value.to_css(dest)?; } MediaFeature::Interval { name, start, start_operator, end, end_operator, } => { if let Some(targets) = dest.targets { if !Feature::MediaIntervalSyntax.is_compatible(targets) { write_min_max(&start_operator.opposite(), name, start, dest)?; dest.write_str(" and (")?; return write_min_max(end_operator, name, end, dest); } } start.to_css(dest)?; start_operator.to_css(dest)?; name.to_css(dest)?; end_operator.to_css(dest)?; end.to_css(dest)?; } } dest.write_char(')') } } #[inline] fn write_min_max( operator: &MediaFeatureComparison, name: &Ident, value: &MediaFeatureValue, dest: &mut Printer, ) -> Result<(), PrinterError> where W: std::fmt::Write, { let prefix = match operator { MediaFeatureComparison::GreaterThan | MediaFeatureComparison::GreaterThanEqual => Some("min-"), MediaFeatureComparison::LessThan | MediaFeatureComparison::LessThanEqual => Some("max-"), MediaFeatureComparison::Equal => None, }; if let Some(prefix) = prefix { dest.write_str(prefix)?; } name.to_css(dest)?; dest.delim(':', false)?; let adjusted = match operator { MediaFeatureComparison::GreaterThan => Some(value.clone() + 0.001), MediaFeatureComparison::LessThan => Some(value.clone() + -0.001), _ => None, }; if let Some(value) = adjusted { value.to_css(dest)?; } else { value.to_css(dest)?; } dest.write_char(')')?; Ok(()) } /// [media feature value](https://drafts.csswg.org/mediaqueries/#typedef-mf-value) within a media query. /// /// See [MediaFeature](MediaFeature). #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "visitor", derive(Visit), visit(visit_media_feature_value, MEDIA_QUERIES))] #[cfg_attr(feature = "into_owned", derive(lightningcss_derive::IntoOwned))] #[cfg_attr( feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(tag = "type", content = "value", rename_all = "kebab-case") )] #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] pub enum MediaFeatureValue<'i> { /// A length value. Length(Length), /// A number value. Number(CSSNumber), /// A resolution. Resolution(Resolution), /// A ratio. Ratio(Ratio), /// An indentifier. #[cfg_attr(feature = "serde", serde(borrow))] Ident(Ident<'i>), /// An environment variable reference. Env(EnvironmentVariable<'i>), } impl<'i> Parse<'i> for MediaFeatureValue<'i> { fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { // Ratios are ambigous with numbers because the second param is optional (e.g. 2/1 == 2). // We require the / delimeter when parsing ratios so that 2/1 ends up as a ratio and 2 is // parsed as a number. if let Ok(ratio) = input.try_parse(Ratio::parse_required) { return Ok(MediaFeatureValue::Ratio(ratio)); } // Parse number next so that unitless values are not parsed as lengths. if let Ok(num) = input.try_parse(CSSNumber::parse) { return Ok(MediaFeatureValue::Number(num)); } if let Ok(length) = input.try_parse(Length::parse) { return Ok(MediaFeatureValue::Length(length)); } if let Ok(res) = input.try_parse(Resolution::parse) { return Ok(MediaFeatureValue::Resolution(res)); } if let Ok(env) = input.try_parse(|input| EnvironmentVariable::parse(input, &ParserOptions::default(), 0)) { return Ok(MediaFeatureValue::Env(env)); } let ident = Ident::parse(input)?; Ok(MediaFeatureValue::Ident(ident)) } } impl<'i> ToCss for MediaFeatureValue<'i> { fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { match self { MediaFeatureValue::Length(len) => len.to_css(dest), MediaFeatureValue::Number(num) => num.to_css(dest), MediaFeatureValue::Resolution(res) => res.to_css(dest), MediaFeatureValue::Ratio(ratio) => ratio.to_css(dest), MediaFeatureValue::Ident(id) => { id.to_css(dest)?; Ok(()) } MediaFeatureValue::Env(env) => env.to_css(dest, false), } } } impl<'i> std::ops::Add for MediaFeatureValue<'i> { type Output = Self; fn add(self, other: f32) -> Self { match self { MediaFeatureValue::Length(len) => MediaFeatureValue::Length(len + Length::px(other)), MediaFeatureValue::Number(num) => MediaFeatureValue::Number(num + other), MediaFeatureValue::Resolution(res) => MediaFeatureValue::Resolution(res + other), MediaFeatureValue::Ratio(ratio) => MediaFeatureValue::Ratio(ratio + other), MediaFeatureValue::Ident(id) => MediaFeatureValue::Ident(id), MediaFeatureValue::Env(env) => MediaFeatureValue::Env(env), } } } /// Consumes an operation or a colon, or returns an error. fn consume_operation_or_colon<'i, 't>( input: &mut Parser<'i, 't>, allow_colon: bool, ) -> Result, ParseError<'i, ParserError<'i>>> { let location = input.current_source_location(); let first_delim = { let location = input.current_source_location(); let next_token = input.next()?; match next_token { Token::Colon if allow_colon => return Ok(None), Token::Delim(oper) => oper, t => return Err(location.new_unexpected_token_error(t.clone())), } }; Ok(Some(match first_delim { '=' => MediaFeatureComparison::Equal, '>' => { if input.try_parse(|i| i.expect_delim('=')).is_ok() { MediaFeatureComparison::GreaterThanEqual } else { MediaFeatureComparison::GreaterThan } } '<' => { if input.try_parse(|i| i.expect_delim('=')).is_ok() { MediaFeatureComparison::LessThanEqual } else { MediaFeatureComparison::LessThan } } d => return Err(location.new_unexpected_token_error(Token::Delim(*d))), })) } fn process_condition<'i>( loc: Location, custom_media: &HashMap, CustomMediaRule<'i>>, media_type: &mut MediaType<'i>, qualifier: &mut Option, condition: &mut MediaCondition<'i>, seen: &mut HashSet>, ) -> Result { match condition { MediaCondition::Not(cond) => { let used = process_condition(loc, custom_media, media_type, qualifier, &mut *cond, seen)?; if !used { // If unused, only a media type remains so apply a not qualifier. // If it is already not, then it cancels out. *qualifier = if *qualifier == Some(Qualifier::Not) { None } else { Some(Qualifier::Not) }; return Ok(false); } // Unwrap nested nots match &**cond { MediaCondition::Not(cond) => { *condition = (**cond).clone(); } _ => {} } } MediaCondition::Operation { conditions, .. } => { let mut res = Ok(true); conditions.retain_mut(|condition| { let r = process_condition(loc, custom_media, media_type, qualifier, condition, seen); if let Ok(used) = r { used } else { res = r; false } }); return res; } MediaCondition::Feature(MediaFeature::Boolean { name }) => { if !name.starts_with("--") { return Ok(true); } if seen.contains(name) { return Err(ErrorWithLocation { kind: MinifyErrorKind::CircularCustomMedia { name: name.to_string() }, loc, }); } let rule = custom_media.get(&name.0).ok_or_else(|| ErrorWithLocation { kind: MinifyErrorKind::CustomMediaNotDefined { name: name.to_string() }, loc, })?; seen.insert(name.clone()); let mut res = Ok(true); let mut conditions: Vec = rule .query .media_queries .iter() .filter_map(|query| { if query.media_type != MediaType::All || query.qualifier != None { if *media_type == MediaType::All { // `not all` will never match. if *qualifier == Some(Qualifier::Not) { res = Ok(false); return None; } // Propagate media type and qualifier to @media rule. *media_type = query.media_type.clone(); *qualifier = query.qualifier.clone(); } else if query.media_type != *media_type || query.qualifier != *qualifier { // Boolean logic with media types is hard to emulate, so we error for now. res = Err(ErrorWithLocation { kind: MinifyErrorKind::UnsupportedCustomMediaBooleanLogic { custom_media_loc: rule.loc, }, loc, }); return None; } } if let Some(condition) = &query.condition { let mut condition = condition.clone(); let r = process_condition(loc, custom_media, media_type, qualifier, &mut condition, seen); if r.is_err() { res = r; } // Parentheses are required around the condition unless there is a single media feature. match condition { MediaCondition::Feature(..) => Some(condition), _ => Some(condition), } } else { None } }) .collect(); seen.remove(name); if res.is_err() { return res; } if conditions.is_empty() { return Ok(false); } if conditions.len() == 1 { *condition = conditions.pop().unwrap(); } else { *condition = MediaCondition::Operation { conditions, operator: Operator::Or, }; } } _ => {} } Ok(true) } #[cfg(test)] mod tests { use super::*; use crate::{stylesheet::PrinterOptions, targets::Browsers}; fn parse(s: &str) -> MediaQuery { let mut input = ParserInput::new(&s); let mut parser = Parser::new(&mut input); MediaQuery::parse(&mut parser).unwrap() } fn and(a: &str, b: &str) -> String { let mut a = parse(a); let b = parse(b); a.and(&b).unwrap(); a.to_css_string(PrinterOptions::default()).unwrap() } #[test] fn test_and() { assert_eq!(and("(min-width: 250px)", "(color)"), "(min-width: 250px) and (color)"); assert_eq!( and("(min-width: 250px) or (color)", "(orientation: landscape)"), "((min-width: 250px) or (color)) and (orientation: landscape)" ); assert_eq!( and("(min-width: 250px) and (color)", "(orientation: landscape)"), "(min-width: 250px) and (color) and (orientation: landscape)" ); assert_eq!(and("all", "print"), "print"); assert_eq!(and("print", "all"), "print"); assert_eq!(and("all", "not print"), "not print"); assert_eq!(and("not print", "all"), "not print"); assert_eq!(and("not all", "print"), "not all"); assert_eq!(and("print", "not all"), "not all"); assert_eq!(and("print", "screen"), "not all"); assert_eq!(and("not print", "screen"), "screen"); assert_eq!(and("print", "not screen"), "print"); assert_eq!(and("not screen", "print"), "print"); assert_eq!(and("not screen", "not all"), "not all"); assert_eq!(and("print", "(min-width: 250px)"), "print and (min-width: 250px)"); assert_eq!(and("(min-width: 250px)", "print"), "print and (min-width: 250px)"); assert_eq!( and("print and (min-width: 250px)", "(color)"), "print and (min-width: 250px) and (color)" ); assert_eq!(and("all", "only screen"), "only screen"); assert_eq!(and("only screen", "all"), "only screen"); assert_eq!(and("print", "print"), "print"); } #[test] fn test_negated_interval_parens() { let media_query = parse("screen and not (200px <= width < 500px)"); let printer_options = PrinterOptions { targets: Some(Browsers { chrome: Some(95 << 16), ..Default::default() }), ..Default::default() }; assert_eq!( media_query.to_css_string(printer_options).unwrap(), "screen and not ((min-width: 200px) and (max-width: 499.999px))" ); } }