//! Media queries. use crate::error::{ErrorWithLocation, MinifyError, MinifyErrorKind, ParserError, PrinterError}; use crate::macros::enum_property; use crate::parser::starts_with_ignore_ascii_case; use crate::printer::Printer; use crate::properties::custom::EnvironmentVariable; #[cfg(feature = "visitor")] use crate::rules::container::ContainerSizeFeatureId; use crate::rules::custom_media::CustomMediaRule; use crate::rules::Location; use crate::stylesheet::ParserOptions; use crate::targets::{should_compile, Targets}; use crate::traits::{Parse, ToCss}; use crate::values::ident::{DashedIdent, Ident}; use crate::values::number::{CSSInteger, CSSNumber}; use crate::values::string::CowArcStr; use crate::values::{length::Length, ratio::Ratio, resolution::Resolution}; use crate::vendor_prefix::VendorPrefix; #[cfg(feature = "visitor")] use crate::visitor::Visit; use bitflags::bitflags; use cssparser::*; use std::borrow::Cow; 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(()) } pub(crate) fn transform_resolution(&mut self, targets: Targets) { let mut i = 0; while i < self.media_queries.len() { let query = &self.media_queries[i]; let mut prefixes = query.get_necessary_prefixes(targets); prefixes.remove(VendorPrefix::None); if !prefixes.is_empty() { let query = query.clone(); for prefix in prefixes { let mut transformed = query.clone(); transformed.transform_resolution(prefix); if !self.media_queries.contains(&transformed) { self.media_queries.insert(i, transformed); } i += 1; } } i += 1; } } /// 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(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> Parse<'i> for MediaQuery<'i> { 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_with_flags(input, QueryConditionFlags::ALLOW_OR)?) } else if input.try_parse(|i| i.expect_ident_matching("and")).is_ok() { Some(MediaCondition::parse_with_flags(input, QueryConditionFlags::empty())?) } else { None }; let media_type = explicit_media_type.unwrap_or(MediaType::All); Ok(Self { qualifier, media_type, condition, }) } } impl<'i> MediaQuery<'i> { 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(()) } fn get_necessary_prefixes(&self, targets: Targets) -> VendorPrefix { if let Some(condition) = &self.condition { condition.get_necessary_prefixes(targets) } else { VendorPrefix::empty() } } fn transform_resolution(&mut self, prefix: VendorPrefix) { if let Some(condition) = &mut self.condition { condition.transform_resolution(prefix) } } /// 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 }; to_css_with_parens_if_needed(condition, dest, needs_parens) } } #[cfg(feature = "serde")] #[derive(serde::Deserialize)] #[serde(untagged)] #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] enum MediaQueryOrRaw<'i> { #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] MediaQuery { qualifier: Option, #[cfg_attr(feature = "serde", serde(borrow))] media_type: MediaType<'i>, condition: Option>, }, Raw { raw: CowArcStr<'i>, }, } #[cfg(feature = "serde")] impl<'i, 'de: 'i> serde::Deserialize<'de> for MediaQuery<'i> { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let mq = MediaQueryOrRaw::deserialize(deserializer)?; match mq { MediaQueryOrRaw::MediaQuery { qualifier, media_type, condition, } => Ok(MediaQuery { qualifier, media_type, condition, }), MediaQueryOrRaw::Raw { raw } => { let res = MediaQuery::parse_string(raw.as_ref()).map_err(|_| serde::de::Error::custom("Could not parse value"))?; Ok(res.into_owned()) } } } } 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>, }, } /// A trait for conditions such as media queries and container queries. pub(crate) trait QueryCondition<'i>: Sized { fn parse_feature<'t>(input: &mut Parser<'i, 't>) -> Result>>; fn create_negation(condition: Box) -> Self; fn create_operation(operator: Operator, conditions: Vec) -> Self; fn parse_style_query<'t>(input: &mut Parser<'i, 't>) -> Result>> { Err(input.new_error_for_next_token()) } fn needs_parens(&self, parent_operator: Option, targets: &Targets) -> bool; } impl<'i> QueryCondition<'i> for MediaCondition<'i> { #[inline] fn parse_feature<'t>(input: &mut Parser<'i, 't>) -> Result>> { let feature = MediaFeature::parse(input)?; Ok(Self::Feature(feature)) } #[inline] fn create_negation(condition: Box>) -> Self { Self::Not(condition) } #[inline] fn create_operation(operator: Operator, conditions: Vec>) -> Self { Self::Operation { operator, conditions } } fn needs_parens(&self, parent_operator: Option, targets: &Targets) -> bool { match self { MediaCondition::Not(_) => true, MediaCondition::Operation { operator, .. } => Some(*operator) != parent_operator, MediaCondition::Feature(f) => f.needs_parens(parent_operator, targets), } } } bitflags! { /// Flags for `parse_query_condition`. #[derive(PartialEq, Eq, Clone, Copy)] pub(crate) struct QueryConditionFlags: u8 { /// Whether to allow top-level "or" boolean logic. const ALLOW_OR = 1 << 0; /// Whether to allow style container queries. const ALLOW_STYLE = 1 << 1; } } impl<'i> MediaCondition<'i> { /// Parse a single media condition. fn parse_with_flags<'t>( input: &mut Parser<'i, 't>, flags: QueryConditionFlags, ) -> Result>> { parse_query_condition(input, flags) } fn get_necessary_prefixes(&self, targets: Targets) -> VendorPrefix { match self { MediaCondition::Feature(MediaFeature::Range { name: MediaFeatureName::Standard(MediaFeatureId::Resolution), .. }) => targets.prefixes(VendorPrefix::None, crate::prefixes::Feature::AtResolution), MediaCondition::Not(not) => not.get_necessary_prefixes(targets), MediaCondition::Operation { conditions, .. } => { let mut prefixes = VendorPrefix::empty(); for condition in conditions { prefixes |= condition.get_necessary_prefixes(targets); } prefixes } _ => VendorPrefix::empty(), } } fn transform_resolution(&mut self, prefix: VendorPrefix) { match self { MediaCondition::Feature(MediaFeature::Range { name: MediaFeatureName::Standard(MediaFeatureId::Resolution), operator, value: MediaFeatureValue::Resolution(value), }) => match prefix { VendorPrefix::WebKit | VendorPrefix::Moz => { *self = MediaCondition::Feature(MediaFeature::Range { name: MediaFeatureName::Standard(match prefix { VendorPrefix::WebKit => MediaFeatureId::WebKitDevicePixelRatio, VendorPrefix::Moz => MediaFeatureId::MozDevicePixelRatio, _ => unreachable!(), }), operator: *operator, value: MediaFeatureValue::Number(match value { Resolution::Dpi(dpi) => *dpi / 96.0, Resolution::Dpcm(dpcm) => *dpcm * 2.54 / 96.0, Resolution::Dppx(dppx) => *dppx, }), }); } _ => {} }, MediaCondition::Not(not) => not.transform_resolution(prefix), MediaCondition::Operation { conditions, .. } => { for condition in conditions { condition.transform_resolution(prefix); } } _ => {} } } } impl<'i> Parse<'i> for MediaCondition<'i> { fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { Self::parse_with_flags(input, QueryConditionFlags::ALLOW_OR) } } /// Parse a single query condition. pub(crate) fn parse_query_condition<'t, 'i, P: QueryCondition<'i>>( input: &mut Parser<'i, 't>, flags: QueryConditionFlags, ) -> Result>> { let location = input.current_source_location(); let (is_negation, is_style) = match *input.next()? { Token::ParenthesisBlock => (false, false), Token::Ident(ref ident) if ident.eq_ignore_ascii_case("not") => (true, false), Token::Function(ref f) if flags.contains(QueryConditionFlags::ALLOW_STYLE) && f.eq_ignore_ascii_case("style") => { (false, true) } ref t => return Err(location.new_unexpected_token_error(t.clone())), }; let first_condition = match (is_negation, is_style) { (true, false) => { let inner_condition = parse_parens_or_function(input, flags)?; return Ok(P::create_negation(Box::new(inner_condition))); } (true, true) => { let inner_condition = P::parse_style_query(input)?; return Ok(P::create_negation(Box::new(inner_condition))); } (false, false) => parse_paren_block(input, flags)?, (false, true) => P::parse_style_query(input)?, }; let operator = match input.try_parse(Operator::parse) { Ok(op) => op, Err(..) => return Ok(first_condition), }; if !flags.contains(QueryConditionFlags::ALLOW_OR) && operator == Operator::Or { return Err(location.new_unexpected_token_error(Token::Ident("or".into()))); } let mut conditions = vec![]; conditions.push(first_condition); conditions.push(parse_parens_or_function(input, flags)?); 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(P::create_operation(operator, conditions)); } conditions.push(parse_parens_or_function(input, flags)?); } } /// Parse a media condition in parentheses, or a style() function. fn parse_parens_or_function<'t, 'i, P: QueryCondition<'i>>( input: &mut Parser<'i, 't>, flags: QueryConditionFlags, ) -> Result>> { let location = input.current_source_location(); match *input.next()? { Token::ParenthesisBlock => parse_paren_block(input, flags), Token::Function(ref f) if flags.contains(QueryConditionFlags::ALLOW_STYLE) && f.eq_ignore_ascii_case("style") => { P::parse_style_query(input) } ref t => return Err(location.new_unexpected_token_error(t.clone())), } } fn parse_paren_block<'t, 'i, P: QueryCondition<'i>>( input: &mut Parser<'i, 't>, flags: QueryConditionFlags, ) -> Result>> { input.parse_nested_block(|input| { if let Ok(inner) = input.try_parse(|i| parse_query_condition(i, flags | QueryConditionFlags::ALLOW_OR)) { return Ok(inner); } P::parse_feature(input) }) } pub(crate) fn to_css_with_parens_if_needed( value: V, dest: &mut Printer, needs_parens: bool, ) -> Result<(), PrinterError> where W: std::fmt::Write, { if needs_parens { dest.write_char('(')?; } value.to_css(dest)?; if needs_parens { dest.write_char(')')?; } Ok(()) } pub(crate) fn operation_to_css<'i, V: ToCss + QueryCondition<'i>, W>( operator: Operator, conditions: &Vec, dest: &mut Printer, ) -> Result<(), PrinterError> where W: std::fmt::Write, { let mut iter = conditions.iter(); let first = iter.next().unwrap(); to_css_with_parens_if_needed(first, dest, first.needs_parens(Some(operator), &dest.targets))?; for item in iter { dest.write_char(' ')?; operator.to_css(dest)?; dest.write_char(' ')?; to_css_with_parens_if_needed(item, dest, item.needs_parens(Some(operator), &dest.targets))?; } 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 ")?; to_css_with_parens_if_needed(&**c, dest, c.needs_parens(None, &dest.targets)) } MediaCondition::Operation { ref conditions, operator, } => operation_to_css(operator, conditions, dest), } } } /// 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 generic media feature or container feature. #[derive(Clone, Debug, PartialEq)] #[cfg_attr( feature = "visitor", derive(Visit), visit(visit_media_feature, MEDIA_QUERIES, <'i, MediaFeatureId>), visit(<'i, ContainerSizeFeatureId>) )] #[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 QueryFeature<'i, FeatureId> { /// A plain media feature, e.g. `(min-width: 240px)`. Plain { /// The name of the feature. #[cfg_attr(feature = "serde", serde(borrow))] name: MediaFeatureName<'i, FeatureId>, /// The feature value. value: MediaFeatureValue<'i>, }, /// A boolean feature, e.g. `(hover)`. Boolean { /// The name of the feature. name: MediaFeatureName<'i, FeatureId>, }, /// A range, e.g. `(width > 240px)`. Range { /// The name of the feature. name: MediaFeatureName<'i, FeatureId>, /// 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: MediaFeatureName<'i, FeatureId>, /// 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, }, } /// A [media feature](https://drafts.csswg.org/mediaqueries/#typedef-media-feature) pub type MediaFeature<'i> = QueryFeature<'i, MediaFeatureId>; impl<'i, FeatureId> Parse<'i> for QueryFeature<'i, FeatureId> where FeatureId: for<'x> Parse<'x> + std::fmt::Debug + PartialEq + ValueType, { fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { match input.try_parse(Self::parse_name_first) { Ok(res) => Ok(res), Err( err @ ParseError { kind: ParseErrorKind::Custom(ParserError::InvalidMediaQuery), .. }, ) => Err(err), _ => Self::parse_value_first(input), } } } impl<'i, FeatureId> QueryFeature<'i, FeatureId> where FeatureId: for<'x> Parse<'x> + std::fmt::Debug + PartialEq + ValueType, { fn parse_name_first<'t>(input: &mut Parser<'i, 't>) -> Result>> { let (name, legacy_op) = MediaFeatureName::parse(input)?; let operator = input.try_parse(|input| consume_operation_or_colon(input, true)); let operator = match operator { Err(..) => return Ok(QueryFeature::Boolean { name }), Ok(operator) => operator, }; if operator.is_some() && legacy_op.is_some() { return Err(input.new_custom_error(ParserError::InvalidMediaQuery)); } let value = MediaFeatureValue::parse(input, name.value_type())?; if !value.check_type(name.value_type()) { return Err(input.new_custom_error(ParserError::InvalidMediaQuery)); } if let Some(operator) = operator.or(legacy_op) { if !name.value_type().allows_ranges() { return Err(input.new_custom_error(ParserError::InvalidMediaQuery)); } Ok(QueryFeature::Range { name, operator, value }) } else { Ok(QueryFeature::Plain { name, value }) } } fn parse_value_first<'t>(input: &mut Parser<'i, 't>) -> Result>> { // We need to find the feature name first so we know the type. let start = input.state(); let name = loop { if let Ok((name, legacy_op)) = MediaFeatureName::parse(input) { if legacy_op.is_some() { return Err(input.new_custom_error(ParserError::InvalidMediaQuery)); } break name; } if input.is_exhausted() { return Err(input.new_custom_error(ParserError::InvalidMediaQuery)); } }; input.reset(&start); // Now we can parse the first value. let value = MediaFeatureValue::parse(input, name.value_type())?; let operator = consume_operation_or_colon(input, false)?; // Skip over the feature name again. { let (feature_name, _) = MediaFeatureName::parse(input)?; debug_assert_eq!(name, feature_name); } if !name.value_type().allows_ranges() || !value.check_type(name.value_type()) { return Err(input.new_custom_error(ParserError::InvalidMediaQuery)); } 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, name.value_type())?; if !end_value.check_type(name.value_type()) { return Err(input.new_custom_error(ParserError::InvalidMediaQuery)); } Ok(QueryFeature::Interval { name, start: value, start_operator, end: end_value, end_operator, }) } else { let operator = operator.unwrap().opposite(); Ok(QueryFeature::Range { name, operator, value }) } } pub(crate) fn needs_parens(&self, parent_operator: Option, targets: &Targets) -> bool { parent_operator != Some(Operator::And) && matches!(self, QueryFeature::Interval { .. }) && should_compile!(targets, MediaIntervalSyntax) } } impl<'i, FeatureId: FeatureToCss> ToCss for QueryFeature<'i, FeatureId> { fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { dest.write_char('(')?; match self { QueryFeature::Boolean { name } => { name.to_css(dest)?; } QueryFeature::Plain { name, value } => { name.to_css(dest)?; dest.delim(':', false)?; value.to_css(dest)?; } QueryFeature::Range { name, operator, value } => { // If range syntax is unsupported, use min/max prefix if possible. if should_compile!(dest.targets, MediaRangeSyntax) { return write_min_max(operator, name, value, dest); } name.to_css(dest)?; operator.to_css(dest)?; value.to_css(dest)?; } QueryFeature::Interval { name, start, start_operator, end, end_operator, } => { if should_compile!(dest.targets, MediaIntervalSyntax) { 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(')') } } /// A media feature name. #[derive(Debug, Clone, 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(untagged))] #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] pub enum MediaFeatureName<'i, FeatureId> { /// A standard media query feature identifier. Standard(FeatureId), /// A custom author-defined environment variable. #[cfg_attr(feature = "serde", serde(borrow))] Custom(DashedIdent<'i>), /// An unknown environment variable. Unknown(Ident<'i>), } impl<'i, FeatureId: for<'x> Parse<'x>> MediaFeatureName<'i, FeatureId> { /// Parses a media feature name. pub fn parse<'t>( input: &mut Parser<'i, 't>, ) -> Result<(Self, Option), ParseError<'i, ParserError<'i>>> { let ident = input.expect_ident()?; if ident.starts_with("--") { return Ok((MediaFeatureName::Custom(DashedIdent(ident.into())), None)); } let mut name = ident.as_ref(); // Webkit places its prefixes before "min" and "max". Remove it first, and // re-add after removing min/max. let is_webkit = starts_with_ignore_ascii_case(&name, "-webkit-"); if is_webkit { name = &name[8..]; } let comparator = if starts_with_ignore_ascii_case(&name, "min-") { name = &name[4..]; Some(MediaFeatureComparison::GreaterThanEqual) } else if starts_with_ignore_ascii_case(&name, "max-") { name = &name[4..]; Some(MediaFeatureComparison::LessThanEqual) } else { None }; let name = if is_webkit { Cow::Owned(format!("-webkit-{}", name)) } else { Cow::Borrowed(name) }; if let Ok(standard) = FeatureId::parse_string(&name) { return Ok((MediaFeatureName::Standard(standard), comparator)); } Ok((MediaFeatureName::Unknown(Ident(ident.into())), None)) } } mod private { use super::*; /// A trait for feature ids which can get a value type. pub trait ValueType { /// Returns the value type for this feature id. fn value_type(&self) -> MediaFeatureType; } } pub(crate) use private::ValueType; impl<'i, FeatureId: ValueType> ValueType for MediaFeatureName<'i, FeatureId> { fn value_type(&self) -> MediaFeatureType { match self { Self::Standard(standard) => standard.value_type(), _ => MediaFeatureType::Unknown, } } } impl<'i, FeatureId: FeatureToCss> ToCss for MediaFeatureName<'i, FeatureId> { fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { match self { Self::Standard(v) => v.to_css(dest), Self::Custom(v) => v.to_css(dest), Self::Unknown(v) => v.to_css(dest), } } } impl<'i, FeatureId: FeatureToCss> FeatureToCss for MediaFeatureName<'i, FeatureId> { fn to_css_with_prefix(&self, prefix: &str, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { match self { Self::Standard(v) => v.to_css_with_prefix(prefix, dest), Self::Custom(v) => { dest.write_str(prefix)?; v.to_css(dest) } Self::Unknown(v) => { dest.write_str(prefix)?; v.to_css(dest) } } } } /// The type of a media feature. #[derive(PartialEq)] pub enum MediaFeatureType { /// A length value. Length, /// A number value. Number, /// An integer value. Integer, /// A boolean value, either 0 or 1. Boolean, /// A resolution. Resolution, /// A ratio. Ratio, /// An identifier. Ident, /// An unknown type. Unknown, } impl MediaFeatureType { fn allows_ranges(&self) -> bool { use MediaFeatureType::*; match self { Length => true, Number => true, Integer => true, Boolean => false, Resolution => true, Ratio => true, Ident => false, Unknown => true, } } } macro_rules! define_query_features { ( $(#[$outer:meta])* $vis:vis enum $name:ident { $( $(#[$meta: meta])* $str: literal: $id: ident = $ty: ident, )+ } ) => { crate::macros::enum_property! { $(#[$outer])* $vis enum $name { $( $(#[$meta])* $str: $id, )+ } } impl ValueType for $name { fn value_type(&self) -> MediaFeatureType { match self { $( Self::$id => MediaFeatureType::$ty, )+ } } } } } pub(crate) use define_query_features; define_query_features! { /// A media query feature identifier. pub enum MediaFeatureId { /// The [width](https://w3c.github.io/csswg-drafts/mediaqueries-5/#width) media feature. "width": Width = Length, /// The [height](https://w3c.github.io/csswg-drafts/mediaqueries-5/#height) media feature. "height": Height = Length, /// The [aspect-ratio](https://w3c.github.io/csswg-drafts/mediaqueries-5/#aspect-ratio) media feature. "aspect-ratio": AspectRatio = Ratio, /// The [orientation](https://w3c.github.io/csswg-drafts/mediaqueries-5/#orientation) media feature. "orientation": Orientation = Ident, /// The [overflow-block](https://w3c.github.io/csswg-drafts/mediaqueries-5/#overflow-block) media feature. "overflow-block": OverflowBlock = Ident, /// The [overflow-inline](https://w3c.github.io/csswg-drafts/mediaqueries-5/#overflow-inline) media feature. "overflow-inline": OverflowInline = Ident, /// The [horizontal-viewport-segments](https://w3c.github.io/csswg-drafts/mediaqueries-5/#horizontal-viewport-segments) media feature. "horizontal-viewport-segments": HorizontalViewportSegments = Integer, /// The [vertical-viewport-segments](https://w3c.github.io/csswg-drafts/mediaqueries-5/#vertical-viewport-segments) media feature. "vertical-viewport-segments": VerticalViewportSegments = Integer, /// The [display-mode](https://w3c.github.io/csswg-drafts/mediaqueries-5/#display-mode) media feature. "display-mode": DisplayMode = Ident, /// The [resolution](https://w3c.github.io/csswg-drafts/mediaqueries-5/#resolution) media feature. "resolution": Resolution = Resolution, // | infinite?? /// The [scan](https://w3c.github.io/csswg-drafts/mediaqueries-5/#scan) media feature. "scan": Scan = Ident, /// The [grid](https://w3c.github.io/csswg-drafts/mediaqueries-5/#grid) media feature. "grid": Grid = Boolean, /// The [update](https://w3c.github.io/csswg-drafts/mediaqueries-5/#update) media feature. "update": Update = Ident, /// The [environment-blending](https://w3c.github.io/csswg-drafts/mediaqueries-5/#environment-blending) media feature. "environment-blending": EnvironmentBlending = Ident, /// The [color](https://w3c.github.io/csswg-drafts/mediaqueries-5/#color) media feature. "color": Color = Integer, /// The [color-index](https://w3c.github.io/csswg-drafts/mediaqueries-5/#color-index) media feature. "color-index": ColorIndex = Integer, /// The [monochrome](https://w3c.github.io/csswg-drafts/mediaqueries-5/#monochrome) media feature. "monochrome": Monochrome = Integer, /// The [color-gamut](https://w3c.github.io/csswg-drafts/mediaqueries-5/#color-gamut) media feature. "color-gamut": ColorGamut = Ident, /// The [dynamic-range](https://w3c.github.io/csswg-drafts/mediaqueries-5/#dynamic-range) media feature. "dynamic-range": DynamicRange = Ident, /// The [inverted-colors](https://w3c.github.io/csswg-drafts/mediaqueries-5/#inverted-colors) media feature. "inverted-colors": InvertedColors = Ident, /// The [pointer](https://w3c.github.io/csswg-drafts/mediaqueries-5/#pointer) media feature. "pointer": Pointer = Ident, /// The [hover](https://w3c.github.io/csswg-drafts/mediaqueries-5/#hover) media feature. "hover": Hover = Ident, /// The [any-pointer](https://w3c.github.io/csswg-drafts/mediaqueries-5/#any-pointer) media feature. "any-pointer": AnyPointer = Ident, /// The [any-hover](https://w3c.github.io/csswg-drafts/mediaqueries-5/#any-hover) media feature. "any-hover": AnyHover = Ident, /// The [nav-controls](https://w3c.github.io/csswg-drafts/mediaqueries-5/#nav-controls) media feature. "nav-controls": NavControls = Ident, /// The [video-color-gamut](https://w3c.github.io/csswg-drafts/mediaqueries-5/#video-color-gamut) media feature. "video-color-gamut": VideoColorGamut = Ident, /// The [video-dynamic-range](https://w3c.github.io/csswg-drafts/mediaqueries-5/#video-dynamic-range) media feature. "video-dynamic-range": VideoDynamicRange = Ident, /// The [scripting](https://w3c.github.io/csswg-drafts/mediaqueries-5/#scripting) media feature. "scripting": Scripting = Ident, /// The [prefers-reduced-motion](https://w3c.github.io/csswg-drafts/mediaqueries-5/#prefers-reduced-motion) media feature. "prefers-reduced-motion": PrefersReducedMotion = Ident, /// The [prefers-reduced-transparency](https://w3c.github.io/csswg-drafts/mediaqueries-5/#prefers-reduced-transparency) media feature. "prefers-reduced-transparency": PrefersReducedTransparency = Ident, /// The [prefers-contrast](https://w3c.github.io/csswg-drafts/mediaqueries-5/#prefers-contrast) media feature. "prefers-contrast": PrefersContrast = Ident, /// The [forced-colors](https://w3c.github.io/csswg-drafts/mediaqueries-5/#forced-colors) media feature. "forced-colors": ForcedColors = Ident, /// The [prefers-color-scheme](https://w3c.github.io/csswg-drafts/mediaqueries-5/#prefers-color-scheme) media feature. "prefers-color-scheme": PrefersColorScheme = Ident, /// The [prefers-reduced-data](https://w3c.github.io/csswg-drafts/mediaqueries-5/#prefers-reduced-data) media feature. "prefers-reduced-data": PrefersReducedData = Ident, /// The [device-width](https://w3c.github.io/csswg-drafts/mediaqueries-5/#device-width) media feature. "device-width": DeviceWidth = Length, /// The [device-height](https://w3c.github.io/csswg-drafts/mediaqueries-5/#device-height) media feature. "device-height": DeviceHeight = Length, /// The [device-aspect-ratio](https://w3c.github.io/csswg-drafts/mediaqueries-5/#device-aspect-ratio) media feature. "device-aspect-ratio": DeviceAspectRatio = Ratio, /// The non-standard -webkit-device-pixel-ratio media feature. "-webkit-device-pixel-ratio": WebKitDevicePixelRatio = Number, /// The non-standard -moz-device-pixel-ratio media feature. "-moz-device-pixel-ratio": MozDevicePixelRatio = Number, // TODO: parse non-standard media queries? // -moz-device-orientation // -webkit-transform-3d } } pub(crate) trait FeatureToCss: ToCss { fn to_css_with_prefix(&self, prefix: &str, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write; } impl FeatureToCss for MediaFeatureId { fn to_css_with_prefix(&self, prefix: &str, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { match self { MediaFeatureId::WebKitDevicePixelRatio => { dest.write_str("-webkit-")?; dest.write_str(prefix)?; dest.write_str("device-pixel-ratio") } _ => { dest.write_str(prefix)?; self.to_css(dest) } } } } #[inline] fn write_min_max( operator: &MediaFeatureComparison, name: &MediaFeatureName, 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 { name.to_css_with_prefix(prefix, dest)?; } else { 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), /// An integer value. Integer(CSSInteger), /// A boolean value. Boolean(bool), /// A resolution. Resolution(Resolution), /// A ratio. Ratio(Ratio), /// An identifier. #[cfg_attr(feature = "serde", serde(borrow))] Ident(Ident<'i>), /// An environment variable reference. Env(EnvironmentVariable<'i>), } impl<'i> MediaFeatureValue<'i> { fn value_type(&self) -> MediaFeatureType { use MediaFeatureValue::*; match self { Length(..) => MediaFeatureType::Length, Number(..) => MediaFeatureType::Number, Integer(..) => MediaFeatureType::Integer, Boolean(..) => MediaFeatureType::Boolean, Resolution(..) => MediaFeatureType::Resolution, Ratio(..) => MediaFeatureType::Ratio, Ident(..) => MediaFeatureType::Ident, Env(..) => MediaFeatureType::Unknown, } } fn check_type(&self, expected_type: MediaFeatureType) -> bool { match (expected_type, self.value_type()) { (_, MediaFeatureType::Unknown) | (MediaFeatureType::Unknown, _) => true, (a, b) => a == b, } } } impl<'i> MediaFeatureValue<'i> { /// Parses a single media query feature value, with an expected type. /// If the type is unknown, pass MediaFeatureType::Unknown instead. pub fn parse<'t>( input: &mut Parser<'i, 't>, expected_type: MediaFeatureType, ) -> Result>> { if let Ok(value) = input.try_parse(|input| Self::parse_known(input, expected_type)) { return Ok(value); } Self::parse_unknown(input) } fn parse_known<'t>( input: &mut Parser<'i, 't>, expected_type: MediaFeatureType, ) -> Result>> { match expected_type { MediaFeatureType::Boolean => { let value = CSSInteger::parse(input)?; if value != 0 && value != 1 { return Err(input.new_custom_error(ParserError::InvalidValue)); } Ok(MediaFeatureValue::Boolean(value == 1)) } MediaFeatureType::Number => Ok(MediaFeatureValue::Number(CSSNumber::parse(input)?)), MediaFeatureType::Integer => Ok(MediaFeatureValue::Integer(CSSInteger::parse(input)?)), MediaFeatureType::Length => Ok(MediaFeatureValue::Length(Length::parse(input)?)), MediaFeatureType::Resolution => Ok(MediaFeatureValue::Resolution(Resolution::parse(input)?)), MediaFeatureType::Ratio => Ok(MediaFeatureValue::Ratio(Ratio::parse(input)?)), MediaFeatureType::Ident => Ok(MediaFeatureValue::Ident(Ident::parse(input)?)), MediaFeatureType::Unknown => Err(input.new_custom_error(ParserError::InvalidValue)), } } fn parse_unknown<'t>(input: &mut Parser<'i, 't>) -> Result>> { // Ratios are ambiguous with numbers because the second param is optional (e.g. 2/1 == 2). // We require the / delimiter 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::Integer(num) => num.to_css(dest), MediaFeatureValue::Boolean(b) => { if *b { dest.write_char('1') } else { dest.write_char('0') } } 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::Integer(num) => { MediaFeatureValue::Integer(num + if other.is_sign_positive() { 1 } else { -1 }) } MediaFeatureValue::Boolean(v) => MediaFeatureValue::Boolean(v), 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), // TODO: calc support } } } /// 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(QueryFeature::Boolean { name }) => { let name = match name { MediaFeatureName::Custom(name) => name, _ => 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, Targets}, }; 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)"), "(width >= 250px) and (color)"); assert_eq!( and("(min-width: 250px) or (color)", "(orientation: landscape)"), "((width >= 250px) or (color)) and (orientation: landscape)" ); assert_eq!( and("(min-width: 250px) and (color)", "(orientation: landscape)"), "(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 (width >= 250px)"); assert_eq!(and("(min-width: 250px)", "print"), "print and (width >= 250px)"); assert_eq!( and("print and (min-width: 250px)", "(color)"), "print and (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: Targets { browsers: Some(Browsers { chrome: Some(95 << 16), ..Default::default() }), ..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))" ); } }