lightningcss/
media_query.rs

1//! Media queries.
2use crate::error::{ErrorWithLocation, MinifyError, MinifyErrorKind, ParserError, PrinterError};
3use crate::macros::enum_property;
4use crate::parser::starts_with_ignore_ascii_case;
5use crate::printer::Printer;
6use crate::properties::custom::EnvironmentVariable;
7#[cfg(feature = "visitor")]
8use crate::rules::container::ContainerSizeFeatureId;
9use crate::rules::custom_media::CustomMediaRule;
10use crate::rules::Location;
11use crate::stylesheet::ParserOptions;
12use crate::targets::{should_compile, Targets};
13use crate::traits::{Parse, ToCss};
14use crate::values::ident::{DashedIdent, Ident};
15use crate::values::number::{CSSInteger, CSSNumber};
16use crate::values::string::CowArcStr;
17use crate::values::{length::Length, ratio::Ratio, resolution::Resolution};
18use crate::vendor_prefix::VendorPrefix;
19#[cfg(feature = "visitor")]
20use crate::visitor::Visit;
21use bitflags::bitflags;
22use cssparser::*;
23#[cfg(feature = "into_owned")]
24use static_self::IntoOwned;
25use std::borrow::Cow;
26use std::collections::{HashMap, HashSet};
27
28#[cfg(feature = "serde")]
29use crate::serialization::ValueWrapper;
30
31/// A [media query list](https://drafts.csswg.org/mediaqueries/#mq-list).
32#[derive(Clone, Debug, PartialEq, Default)]
33#[cfg_attr(feature = "visitor", derive(Visit), visit(visit_media_list, MEDIA_QUERIES))]
34#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
35#[cfg_attr(
36  feature = "serde",
37  derive(serde::Serialize, serde::Deserialize),
38  serde(rename_all = "camelCase")
39)]
40#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
41pub struct MediaList<'i> {
42  /// The list of media queries.
43  #[cfg_attr(feature = "serde", serde(borrow))]
44  pub media_queries: Vec<MediaQuery<'i>>,
45}
46
47impl<'i> MediaList<'i> {
48  /// Creates an empty media query list.
49  pub fn new() -> Self {
50    MediaList { media_queries: vec![] }
51  }
52
53  /// Parse a media query list from CSS.
54  pub fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
55    let mut media_queries = vec![];
56    loop {
57      match input.parse_until_before(Delimiter::Comma, |i| MediaQuery::parse(i)) {
58        Ok(mq) => {
59          media_queries.push(mq);
60        }
61        Err(err) => match err.kind {
62          ParseErrorKind::Basic(BasicParseErrorKind::EndOfInput) => break,
63          _ => return Err(err),
64        },
65      }
66
67      match input.next() {
68        Ok(&Token::Comma) => {}
69        Ok(_) => unreachable!(),
70        Err(_) => break,
71      }
72    }
73
74    Ok(MediaList { media_queries })
75  }
76
77  pub(crate) fn transform_custom_media(
78    &mut self,
79    loc: Location,
80    custom_media: &HashMap<CowArcStr<'i>, CustomMediaRule<'i>>,
81  ) -> Result<(), MinifyError> {
82    for query in self.media_queries.iter_mut() {
83      query.transform_custom_media(loc, custom_media)?;
84    }
85    Ok(())
86  }
87
88  pub(crate) fn transform_resolution(&mut self, targets: Targets) {
89    let mut i = 0;
90    while i < self.media_queries.len() {
91      let query = &self.media_queries[i];
92      let mut prefixes = query.get_necessary_prefixes(targets);
93      prefixes.remove(VendorPrefix::None);
94      if !prefixes.is_empty() {
95        let query = query.clone();
96        for prefix in prefixes {
97          let mut transformed = query.clone();
98          transformed.transform_resolution(prefix);
99          if !self.media_queries.contains(&transformed) {
100            self.media_queries.insert(i, transformed);
101          }
102          i += 1;
103        }
104      }
105
106      i += 1;
107    }
108  }
109
110  /// Returns whether the media query list always matches.
111  pub fn always_matches(&self) -> bool {
112    // If the media list is empty, it always matches.
113    self.media_queries.is_empty() || self.media_queries.iter().all(|mq| mq.always_matches())
114  }
115
116  /// Returns whether the media query list never matches.
117  pub fn never_matches(&self) -> bool {
118    !self.media_queries.is_empty() && self.media_queries.iter().all(|mq| mq.never_matches())
119  }
120
121  /// Attempts to combine the given media query list into this one. The resulting media query
122  /// list matches if both the original media query lists would have matched.
123  ///
124  /// Returns an error if the boolean logic is not possible.
125  pub fn and(&mut self, b: &MediaList<'i>) -> Result<(), ()> {
126    if self.media_queries.is_empty() {
127      self.media_queries.extend(b.media_queries.iter().cloned());
128      return Ok(());
129    }
130
131    for b in &b.media_queries {
132      if self.media_queries.contains(&b) {
133        continue;
134      }
135
136      for a in &mut self.media_queries {
137        a.and(&b)?;
138      }
139    }
140
141    Ok(())
142  }
143
144  /// Combines the given media query list into this one. The resulting media query list
145  /// matches if either of the original media query lists would have matched.
146  pub fn or(&mut self, b: &MediaList<'i>) {
147    for mq in &b.media_queries {
148      if !self.media_queries.contains(&mq) {
149        self.media_queries.push(mq.clone())
150      }
151    }
152  }
153}
154
155impl<'i> ToCss for MediaList<'i> {
156  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
157  where
158    W: std::fmt::Write,
159  {
160    if self.media_queries.is_empty() {
161      dest.write_str("not all")?;
162      return Ok(());
163    }
164
165    let mut first = true;
166    for query in &self.media_queries {
167      if !first {
168        dest.delim(',', false)?;
169      }
170      first = false;
171      query.to_css(dest)?;
172    }
173    Ok(())
174  }
175}
176
177enum_property! {
178  /// A [media query qualifier](https://drafts.csswg.org/mediaqueries/#mq-prefix).
179  pub enum Qualifier {
180    /// Prevents older browsers from matching the media query.
181    Only,
182    /// Negates a media query.
183    Not,
184  }
185}
186
187/// A [media type](https://drafts.csswg.org/mediaqueries/#media-types) within a media query.
188#[derive(Clone, Debug, PartialEq)]
189#[cfg_attr(feature = "visitor", derive(Visit))]
190#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
191#[cfg_attr(
192  feature = "serde",
193  derive(serde::Serialize, serde::Deserialize),
194  serde(rename_all = "kebab-case", into = "CowArcStr", from = "CowArcStr")
195)]
196pub enum MediaType<'i> {
197  /// Matches all devices.
198  All,
199  /// Matches printers, and devices intended to reproduce a printed
200  /// display, such as a web browser showing a document in “Print Preview”.
201  Print,
202  /// Matches all devices that aren’t matched by print.
203  Screen,
204  /// An unknown media type.
205  #[cfg_attr(feature = "serde", serde(borrow))]
206  Custom(CowArcStr<'i>),
207}
208
209impl<'i> From<CowArcStr<'i>> for MediaType<'i> {
210  fn from(name: CowArcStr<'i>) -> Self {
211    match_ignore_ascii_case! { &*name,
212      "all" => MediaType::All,
213      "print" => MediaType::Print,
214      "screen" => MediaType::Screen,
215      _ => MediaType::Custom(name)
216    }
217  }
218}
219
220impl<'i> Into<CowArcStr<'i>> for MediaType<'i> {
221  fn into(self) -> CowArcStr<'i> {
222    match self {
223      MediaType::All => "all".into(),
224      MediaType::Print => "print".into(),
225      MediaType::Screen => "screen".into(),
226      MediaType::Custom(desc) => desc,
227    }
228  }
229}
230
231impl<'i> Parse<'i> for MediaType<'i> {
232  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
233    let name: CowArcStr = input.expect_ident()?.into();
234    Ok(Self::from(name))
235  }
236}
237
238#[cfg(feature = "jsonschema")]
239#[cfg_attr(docsrs, doc(cfg(feature = "jsonschema")))]
240impl<'a> schemars::JsonSchema for MediaType<'a> {
241  fn is_referenceable() -> bool {
242    true
243  }
244
245  fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
246    str::json_schema(gen)
247  }
248
249  fn schema_name() -> String {
250    "MediaType".into()
251  }
252}
253
254/// A [media query](https://drafts.csswg.org/mediaqueries/#media).
255#[derive(Clone, Debug, PartialEq)]
256#[cfg_attr(feature = "visitor", derive(Visit))]
257#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
258#[cfg_attr(feature = "visitor", visit(visit_media_query, MEDIA_QUERIES))]
259#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(rename_all = "camelCase"))]
260#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
261pub struct MediaQuery<'i> {
262  /// The qualifier for this query.
263  pub qualifier: Option<Qualifier>,
264  /// The media type for this query, that can be known, unknown, or "all".
265  #[cfg_attr(feature = "serde", serde(borrow))]
266  pub media_type: MediaType<'i>,
267  /// The condition that this media query contains. This cannot have `or`
268  /// in the first level.
269  pub condition: Option<MediaCondition<'i>>,
270}
271
272impl<'i> Parse<'i> for MediaQuery<'i> {
273  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
274    let (qualifier, explicit_media_type) = input
275      .try_parse(|input| -> Result<_, ParseError<'i, ParserError<'i>>> {
276        let qualifier = input.try_parse(Qualifier::parse).ok();
277        let media_type = MediaType::parse(input)?;
278        Ok((qualifier, Some(media_type)))
279      })
280      .unwrap_or_default();
281
282    let condition = if explicit_media_type.is_none() {
283      Some(MediaCondition::parse_with_flags(input, QueryConditionFlags::ALLOW_OR)?)
284    } else if input.try_parse(|i| i.expect_ident_matching("and")).is_ok() {
285      Some(MediaCondition::parse_with_flags(input, QueryConditionFlags::empty())?)
286    } else {
287      None
288    };
289
290    let media_type = explicit_media_type.unwrap_or(MediaType::All);
291    Ok(Self {
292      qualifier,
293      media_type,
294      condition,
295    })
296  }
297}
298
299impl<'i> MediaQuery<'i> {
300  fn transform_custom_media(
301    &mut self,
302    loc: Location,
303    custom_media: &HashMap<CowArcStr<'i>, CustomMediaRule<'i>>,
304  ) -> Result<(), MinifyError> {
305    if let Some(condition) = &mut self.condition {
306      let used = process_condition(
307        loc,
308        custom_media,
309        &mut self.media_type,
310        &mut self.qualifier,
311        condition,
312        &mut HashSet::new(),
313      )?;
314      if !used {
315        self.condition = None;
316      }
317    }
318    Ok(())
319  }
320
321  fn get_necessary_prefixes(&self, targets: Targets) -> VendorPrefix {
322    if let Some(condition) = &self.condition {
323      condition.get_necessary_prefixes(targets)
324    } else {
325      VendorPrefix::empty()
326    }
327  }
328
329  fn transform_resolution(&mut self, prefix: VendorPrefix) {
330    if let Some(condition) = &mut self.condition {
331      condition.transform_resolution(prefix)
332    }
333  }
334
335  /// Returns whether the media query is guaranteed to always match.
336  pub fn always_matches(&self) -> bool {
337    self.qualifier == None && self.media_type == MediaType::All && self.condition == None
338  }
339
340  /// Returns whether the media query is guaranteed to never match.
341  pub fn never_matches(&self) -> bool {
342    self.qualifier == Some(Qualifier::Not) && self.media_type == MediaType::All && self.condition == None
343  }
344
345  /// Attempts to combine the given media query into this one. The resulting media query
346  /// matches if both of the original media queries would have matched.
347  ///
348  /// Returns an error if the boolean logic is not possible.
349  pub fn and<'a>(&mut self, b: &MediaQuery<'i>) -> Result<(), ()> {
350    let at = (&self.qualifier, &self.media_type);
351    let bt = (&b.qualifier, &b.media_type);
352    let (qualifier, media_type) = match (at, bt) {
353      // `not all and screen` => not all
354      // `screen and not all` => not all
355      ((&Some(Qualifier::Not), &MediaType::All), _) |
356      (_, (&Some(Qualifier::Not), &MediaType::All)) => (Some(Qualifier::Not), MediaType::All),
357      // `not screen and not print` => ERROR
358      // `not screen and not screen` => not screen
359      ((&Some(Qualifier::Not), a), (&Some(Qualifier::Not), b)) => {
360        if a == b {
361          (Some(Qualifier::Not), a.clone())
362        } else {
363          return Err(())
364        }
365      },
366      // `all and print` => print
367      // `print and all` => print
368      // `all and not print` => not print
369      ((_, MediaType::All), (q, t)) |
370      ((q, t), (_, MediaType::All)) |
371      // `not screen and print` => print
372      // `print and not screen` => print
373      ((&Some(Qualifier::Not), _), (q, t)) |
374      ((q, t), (&Some(Qualifier::Not), _)) => (q.clone(), t.clone()),
375      // `print and screen` => not all
376      ((_, a), (_, b)) if a != b => (Some(Qualifier::Not), MediaType::All),
377      ((_, a), _) => (None, a.clone())
378    };
379
380    self.qualifier = qualifier;
381    self.media_type = media_type;
382
383    if let Some(cond) = &b.condition {
384      self.condition = if let Some(condition) = &self.condition {
385        if condition != cond {
386          Some(MediaCondition::Operation {
387            conditions: vec![condition.clone(), cond.clone()],
388            operator: Operator::And,
389          })
390        } else {
391          Some(condition.clone())
392        }
393      } else {
394        Some(cond.clone())
395      }
396    }
397
398    Ok(())
399  }
400}
401
402impl<'i> ToCss for MediaQuery<'i> {
403  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
404  where
405    W: std::fmt::Write,
406  {
407    if let Some(qual) = self.qualifier {
408      qual.to_css(dest)?;
409      dest.write_char(' ')?;
410    }
411
412    match self.media_type {
413      MediaType::All => {
414        // We need to print "all" if there's a qualifier, or there's
415        // just an empty list of expressions.
416        //
417        // Otherwise, we'd serialize media queries like "(min-width:
418        // 40px)" in "all (min-width: 40px)", which is unexpected.
419        if self.qualifier.is_some() || self.condition.is_none() {
420          dest.write_str("all")?;
421        }
422      }
423      MediaType::Print => dest.write_str("print")?,
424      MediaType::Screen => dest.write_str("screen")?,
425      MediaType::Custom(ref desc) => dest.write_str(desc)?,
426    }
427
428    let condition = match self.condition {
429      Some(ref c) => c,
430      None => return Ok(()),
431    };
432
433    let needs_parens = if self.media_type != MediaType::All || self.qualifier.is_some() {
434      dest.write_str(" and ")?;
435      matches!(condition, MediaCondition::Operation { operator, .. } if *operator != Operator::And)
436    } else {
437      false
438    };
439
440    to_css_with_parens_if_needed(condition, dest, needs_parens)
441  }
442}
443
444#[cfg(feature = "serde")]
445#[derive(serde::Deserialize)]
446#[serde(untagged)]
447#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
448enum MediaQueryOrRaw<'i> {
449  #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
450  MediaQuery {
451    qualifier: Option<Qualifier>,
452    #[cfg_attr(feature = "serde", serde(borrow))]
453    media_type: MediaType<'i>,
454    condition: Option<MediaCondition<'i>>,
455  },
456  Raw {
457    raw: CowArcStr<'i>,
458  },
459}
460
461#[cfg(feature = "serde")]
462impl<'i, 'de: 'i> serde::Deserialize<'de> for MediaQuery<'i> {
463  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
464  where
465    D: serde::Deserializer<'de>,
466  {
467    let mq = MediaQueryOrRaw::deserialize(deserializer)?;
468    match mq {
469      MediaQueryOrRaw::MediaQuery {
470        qualifier,
471        media_type,
472        condition,
473      } => Ok(MediaQuery {
474        qualifier,
475        media_type,
476        condition,
477      }),
478      MediaQueryOrRaw::Raw { raw } => {
479        let res =
480          MediaQuery::parse_string(raw.as_ref()).map_err(|_| serde::de::Error::custom("Could not parse value"))?;
481        Ok(res.into_owned())
482      }
483    }
484  }
485}
486
487enum_property! {
488  /// A binary `and` or `or` operator.
489  pub enum Operator {
490    /// The `and` operator.
491    And,
492    /// The `or` operator.
493    Or,
494  }
495}
496
497/// Represents a media condition.
498#[derive(Clone, Debug, PartialEq)]
499#[cfg_attr(feature = "visitor", derive(Visit))]
500#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
501#[cfg_attr(
502  feature = "serde",
503  derive(serde::Serialize, serde::Deserialize),
504  serde(tag = "type", rename_all = "kebab-case")
505)]
506#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
507pub enum MediaCondition<'i> {
508  /// A media feature, implicitly parenthesized.
509  #[cfg_attr(feature = "serde", serde(borrow, with = "ValueWrapper::<MediaFeature>"))]
510  Feature(MediaFeature<'i>),
511  /// A negation of a condition.
512  #[cfg_attr(feature = "visitor", skip_type)]
513  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<Box<MediaCondition>>"))]
514  Not(Box<MediaCondition<'i>>),
515  /// A set of joint operations.
516  #[cfg_attr(feature = "visitor", skip_type)]
517  Operation {
518    /// The operator for the conditions.
519    operator: Operator,
520    /// The conditions for the operator.
521    conditions: Vec<MediaCondition<'i>>,
522  },
523}
524
525/// A trait for conditions such as media queries and container queries.
526pub(crate) trait QueryCondition<'i>: Sized {
527  fn parse_feature<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>>;
528  fn create_negation(condition: Box<Self>) -> Self;
529  fn create_operation(operator: Operator, conditions: Vec<Self>) -> Self;
530  fn parse_style_query<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
531    Err(input.new_error_for_next_token())
532  }
533
534  fn needs_parens(&self, parent_operator: Option<Operator>, targets: &Targets) -> bool;
535}
536
537impl<'i> QueryCondition<'i> for MediaCondition<'i> {
538  #[inline]
539  fn parse_feature<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
540    let feature = MediaFeature::parse(input)?;
541    Ok(Self::Feature(feature))
542  }
543
544  #[inline]
545  fn create_negation(condition: Box<MediaCondition<'i>>) -> Self {
546    Self::Not(condition)
547  }
548
549  #[inline]
550  fn create_operation(operator: Operator, conditions: Vec<MediaCondition<'i>>) -> Self {
551    Self::Operation { operator, conditions }
552  }
553
554  fn needs_parens(&self, parent_operator: Option<Operator>, targets: &Targets) -> bool {
555    match self {
556      MediaCondition::Not(_) => true,
557      MediaCondition::Operation { operator, .. } => Some(*operator) != parent_operator,
558      MediaCondition::Feature(f) => f.needs_parens(parent_operator, targets),
559    }
560  }
561}
562
563bitflags! {
564  /// Flags for `parse_query_condition`.
565  #[derive(PartialEq, Eq, Clone, Copy)]
566  pub(crate) struct QueryConditionFlags: u8 {
567    /// Whether to allow top-level "or" boolean logic.
568    const ALLOW_OR = 1 << 0;
569    /// Whether to allow style container queries.
570    const ALLOW_STYLE = 1 << 1;
571  }
572}
573
574impl<'i> MediaCondition<'i> {
575  /// Parse a single media condition.
576  fn parse_with_flags<'t>(
577    input: &mut Parser<'i, 't>,
578    flags: QueryConditionFlags,
579  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
580    parse_query_condition(input, flags)
581  }
582
583  fn get_necessary_prefixes(&self, targets: Targets) -> VendorPrefix {
584    match self {
585      MediaCondition::Feature(MediaFeature::Range {
586        name: MediaFeatureName::Standard(MediaFeatureId::Resolution),
587        ..
588      }) => targets.prefixes(VendorPrefix::None, crate::prefixes::Feature::AtResolution),
589      MediaCondition::Not(not) => not.get_necessary_prefixes(targets),
590      MediaCondition::Operation { conditions, .. } => {
591        let mut prefixes = VendorPrefix::empty();
592        for condition in conditions {
593          prefixes |= condition.get_necessary_prefixes(targets);
594        }
595        prefixes
596      }
597      _ => VendorPrefix::empty(),
598    }
599  }
600
601  fn transform_resolution(&mut self, prefix: VendorPrefix) {
602    match self {
603      MediaCondition::Feature(MediaFeature::Range {
604        name: MediaFeatureName::Standard(MediaFeatureId::Resolution),
605        operator,
606        value: MediaFeatureValue::Resolution(value),
607      }) => match prefix {
608        VendorPrefix::WebKit | VendorPrefix::Moz => {
609          *self = MediaCondition::Feature(MediaFeature::Range {
610            name: MediaFeatureName::Standard(match prefix {
611              VendorPrefix::WebKit => MediaFeatureId::WebKitDevicePixelRatio,
612              VendorPrefix::Moz => MediaFeatureId::MozDevicePixelRatio,
613              _ => unreachable!(),
614            }),
615            operator: *operator,
616            value: MediaFeatureValue::Number(match value {
617              Resolution::Dpi(dpi) => *dpi / 96.0,
618              Resolution::Dpcm(dpcm) => *dpcm * 2.54 / 96.0,
619              Resolution::Dppx(dppx) => *dppx,
620            }),
621          });
622        }
623        _ => {}
624      },
625      MediaCondition::Not(not) => not.transform_resolution(prefix),
626      MediaCondition::Operation { conditions, .. } => {
627        for condition in conditions {
628          condition.transform_resolution(prefix);
629        }
630      }
631      _ => {}
632    }
633  }
634}
635
636impl<'i> Parse<'i> for MediaCondition<'i> {
637  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
638    Self::parse_with_flags(input, QueryConditionFlags::ALLOW_OR)
639  }
640}
641
642/// Parse a single query condition.
643pub(crate) fn parse_query_condition<'t, 'i, P: QueryCondition<'i>>(
644  input: &mut Parser<'i, 't>,
645  flags: QueryConditionFlags,
646) -> Result<P, ParseError<'i, ParserError<'i>>> {
647  let location = input.current_source_location();
648  let (is_negation, is_style) = match *input.next()? {
649    Token::ParenthesisBlock => (false, false),
650    Token::Ident(ref ident) if ident.eq_ignore_ascii_case("not") => (true, false),
651    Token::Function(ref f)
652      if flags.contains(QueryConditionFlags::ALLOW_STYLE) && f.eq_ignore_ascii_case("style") =>
653    {
654      (false, true)
655    }
656    ref t => return Err(location.new_unexpected_token_error(t.clone())),
657  };
658
659  let first_condition = match (is_negation, is_style) {
660    (true, false) => {
661      let inner_condition = parse_parens_or_function(input, flags)?;
662      return Ok(P::create_negation(Box::new(inner_condition)));
663    }
664    (true, true) => {
665      let inner_condition = P::parse_style_query(input)?;
666      return Ok(P::create_negation(Box::new(inner_condition)));
667    }
668    (false, false) => parse_paren_block(input, flags)?,
669    (false, true) => P::parse_style_query(input)?,
670  };
671
672  let operator = match input.try_parse(Operator::parse) {
673    Ok(op) => op,
674    Err(..) => return Ok(first_condition),
675  };
676
677  if !flags.contains(QueryConditionFlags::ALLOW_OR) && operator == Operator::Or {
678    return Err(location.new_unexpected_token_error(Token::Ident("or".into())));
679  }
680
681  let mut conditions = vec![];
682  conditions.push(first_condition);
683  conditions.push(parse_parens_or_function(input, flags)?);
684
685  let delim = match operator {
686    Operator::And => "and",
687    Operator::Or => "or",
688  };
689
690  loop {
691    if input.try_parse(|i| i.expect_ident_matching(delim)).is_err() {
692      return Ok(P::create_operation(operator, conditions));
693    }
694
695    conditions.push(parse_parens_or_function(input, flags)?);
696  }
697}
698
699/// Parse a media condition in parentheses, or a style() function.
700fn parse_parens_or_function<'t, 'i, P: QueryCondition<'i>>(
701  input: &mut Parser<'i, 't>,
702  flags: QueryConditionFlags,
703) -> Result<P, ParseError<'i, ParserError<'i>>> {
704  let location = input.current_source_location();
705  match *input.next()? {
706    Token::ParenthesisBlock => parse_paren_block(input, flags),
707    Token::Function(ref f)
708      if flags.contains(QueryConditionFlags::ALLOW_STYLE) && f.eq_ignore_ascii_case("style") =>
709    {
710      P::parse_style_query(input)
711    }
712    ref t => return Err(location.new_unexpected_token_error(t.clone())),
713  }
714}
715
716fn parse_paren_block<'t, 'i, P: QueryCondition<'i>>(
717  input: &mut Parser<'i, 't>,
718  flags: QueryConditionFlags,
719) -> Result<P, ParseError<'i, ParserError<'i>>> {
720  input.parse_nested_block(|input| {
721    if let Ok(inner) = input.try_parse(|i| parse_query_condition(i, flags | QueryConditionFlags::ALLOW_OR)) {
722      return Ok(inner);
723    }
724
725    P::parse_feature(input)
726  })
727}
728
729pub(crate) fn to_css_with_parens_if_needed<V: ToCss, W>(
730  value: V,
731  dest: &mut Printer<W>,
732  needs_parens: bool,
733) -> Result<(), PrinterError>
734where
735  W: std::fmt::Write,
736{
737  if needs_parens {
738    dest.write_char('(')?;
739  }
740  value.to_css(dest)?;
741  if needs_parens {
742    dest.write_char(')')?;
743  }
744  Ok(())
745}
746
747pub(crate) fn operation_to_css<'i, V: ToCss + QueryCondition<'i>, W>(
748  operator: Operator,
749  conditions: &Vec<V>,
750  dest: &mut Printer<W>,
751) -> Result<(), PrinterError>
752where
753  W: std::fmt::Write,
754{
755  let mut iter = conditions.iter();
756  let first = iter.next().unwrap();
757  to_css_with_parens_if_needed(first, dest, first.needs_parens(Some(operator), &dest.targets))?;
758  for item in iter {
759    dest.write_char(' ')?;
760    operator.to_css(dest)?;
761    dest.write_char(' ')?;
762    to_css_with_parens_if_needed(item, dest, item.needs_parens(Some(operator), &dest.targets))?;
763  }
764
765  Ok(())
766}
767
768impl<'i> ToCss for MediaCondition<'i> {
769  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
770  where
771    W: std::fmt::Write,
772  {
773    match *self {
774      MediaCondition::Feature(ref f) => f.to_css(dest),
775      MediaCondition::Not(ref c) => {
776        dest.write_str("not ")?;
777        to_css_with_parens_if_needed(&**c, dest, c.needs_parens(None, &dest.targets))
778      }
779      MediaCondition::Operation {
780        ref conditions,
781        operator,
782      } => operation_to_css(operator, conditions, dest),
783    }
784  }
785}
786
787/// A [comparator](https://drafts.csswg.org/mediaqueries/#typedef-mf-comparison) within a media query.
788#[derive(Clone, Copy, Debug, Eq, PartialEq)]
789#[cfg_attr(feature = "visitor", derive(Visit))]
790#[cfg_attr(
791  feature = "serde",
792  derive(serde::Serialize, serde::Deserialize),
793  serde(rename_all = "kebab-case")
794)]
795#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
796#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
797pub enum MediaFeatureComparison {
798  /// `=`
799  Equal,
800  /// `>`
801  GreaterThan,
802  /// `>=`
803  GreaterThanEqual,
804  /// `<`
805  LessThan,
806  /// `<=`
807  LessThanEqual,
808}
809
810impl ToCss for MediaFeatureComparison {
811  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
812  where
813    W: std::fmt::Write,
814  {
815    use MediaFeatureComparison::*;
816    match self {
817      Equal => dest.delim('=', true),
818      GreaterThan => dest.delim('>', true),
819      GreaterThanEqual => {
820        dest.whitespace()?;
821        dest.write_str(">=")?;
822        dest.whitespace()
823      }
824      LessThan => dest.delim('<', true),
825      LessThanEqual => {
826        dest.whitespace()?;
827        dest.write_str("<=")?;
828        dest.whitespace()
829      }
830    }
831  }
832}
833
834impl MediaFeatureComparison {
835  fn opposite(&self) -> MediaFeatureComparison {
836    match self {
837      MediaFeatureComparison::GreaterThan => MediaFeatureComparison::LessThan,
838      MediaFeatureComparison::GreaterThanEqual => MediaFeatureComparison::LessThanEqual,
839      MediaFeatureComparison::LessThan => MediaFeatureComparison::GreaterThan,
840      MediaFeatureComparison::LessThanEqual => MediaFeatureComparison::GreaterThanEqual,
841      MediaFeatureComparison::Equal => MediaFeatureComparison::Equal,
842    }
843  }
844}
845
846/// A generic media feature or container feature.
847#[derive(Clone, Debug, PartialEq)]
848#[cfg_attr(
849  feature = "visitor",
850  derive(Visit),
851  visit(visit_media_feature, MEDIA_QUERIES, <'i, MediaFeatureId>),
852  visit(<'i, ContainerSizeFeatureId>)
853)]
854#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
855#[cfg_attr(
856  feature = "serde",
857  derive(serde::Serialize, serde::Deserialize),
858  serde(tag = "type", rename_all = "kebab-case")
859)]
860#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
861pub enum QueryFeature<'i, FeatureId> {
862  /// A plain media feature, e.g. `(min-width: 240px)`.
863  Plain {
864    /// The name of the feature.
865    #[cfg_attr(feature = "serde", serde(borrow))]
866    name: MediaFeatureName<'i, FeatureId>,
867    /// The feature value.
868    value: MediaFeatureValue<'i>,
869  },
870  /// A boolean feature, e.g. `(hover)`.
871  Boolean {
872    /// The name of the feature.
873    name: MediaFeatureName<'i, FeatureId>,
874  },
875  /// A range, e.g. `(width > 240px)`.
876  Range {
877    /// The name of the feature.
878    name: MediaFeatureName<'i, FeatureId>,
879    /// A comparator.
880    operator: MediaFeatureComparison,
881    /// The feature value.
882    value: MediaFeatureValue<'i>,
883  },
884  /// An interval, e.g. `(120px < width < 240px)`.
885  #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
886  Interval {
887    /// The name of the feature.
888    name: MediaFeatureName<'i, FeatureId>,
889    /// A start value.
890    start: MediaFeatureValue<'i>,
891    /// A comparator for the start value.
892    start_operator: MediaFeatureComparison,
893    /// The end value.
894    end: MediaFeatureValue<'i>,
895    /// A comparator for the end value.
896    end_operator: MediaFeatureComparison,
897  },
898}
899
900/// A [media feature](https://drafts.csswg.org/mediaqueries/#typedef-media-feature)
901pub type MediaFeature<'i> = QueryFeature<'i, MediaFeatureId>;
902
903impl<'i, FeatureId> Parse<'i> for QueryFeature<'i, FeatureId>
904where
905  FeatureId: for<'x> Parse<'x> + std::fmt::Debug + PartialEq + ValueType,
906{
907  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
908    match input.try_parse(Self::parse_name_first) {
909      Ok(res) => Ok(res),
910      Err(
911        err @ ParseError {
912          kind: ParseErrorKind::Custom(ParserError::InvalidMediaQuery),
913          ..
914        },
915      ) => Err(err),
916      _ => Self::parse_value_first(input),
917    }
918  }
919}
920
921impl<'i, FeatureId> QueryFeature<'i, FeatureId>
922where
923  FeatureId: for<'x> Parse<'x> + std::fmt::Debug + PartialEq + ValueType,
924{
925  fn parse_name_first<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
926    let (name, legacy_op) = MediaFeatureName::parse(input)?;
927
928    let operator = input.try_parse(|input| consume_operation_or_colon(input, true));
929    let operator = match operator {
930      Err(..) => return Ok(QueryFeature::Boolean { name }),
931      Ok(operator) => operator,
932    };
933
934    if operator.is_some() && legacy_op.is_some() {
935      return Err(input.new_custom_error(ParserError::InvalidMediaQuery));
936    }
937
938    let value = MediaFeatureValue::parse(input, name.value_type())?;
939    if !value.check_type(name.value_type()) {
940      return Err(input.new_custom_error(ParserError::InvalidMediaQuery));
941    }
942
943    if let Some(operator) = operator.or(legacy_op) {
944      if !name.value_type().allows_ranges() {
945        return Err(input.new_custom_error(ParserError::InvalidMediaQuery));
946      }
947
948      Ok(QueryFeature::Range { name, operator, value })
949    } else {
950      Ok(QueryFeature::Plain { name, value })
951    }
952  }
953
954  fn parse_value_first<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
955    // We need to find the feature name first so we know the type.
956    let start = input.state();
957    let name = loop {
958      if let Ok((name, legacy_op)) = MediaFeatureName::parse(input) {
959        if legacy_op.is_some() {
960          return Err(input.new_custom_error(ParserError::InvalidMediaQuery));
961        }
962        break name;
963      }
964      if input.is_exhausted() {
965        return Err(input.new_custom_error(ParserError::InvalidMediaQuery));
966      }
967    };
968
969    input.reset(&start);
970
971    // Now we can parse the first value.
972    let value = MediaFeatureValue::parse(input, name.value_type())?;
973    let operator = consume_operation_or_colon(input, false)?;
974
975    // Skip over the feature name again.
976    {
977      let (feature_name, _) = MediaFeatureName::parse(input)?;
978      debug_assert_eq!(name, feature_name);
979    }
980
981    if !name.value_type().allows_ranges() || !value.check_type(name.value_type()) {
982      return Err(input.new_custom_error(ParserError::InvalidMediaQuery));
983    }
984
985    if let Ok(end_operator) = input.try_parse(|input| consume_operation_or_colon(input, false)) {
986      let start_operator = operator.unwrap();
987      let end_operator = end_operator.unwrap();
988      // Start and end operators must be matching.
989      match (start_operator, end_operator) {
990        (MediaFeatureComparison::GreaterThan, MediaFeatureComparison::GreaterThan)
991        | (MediaFeatureComparison::GreaterThan, MediaFeatureComparison::GreaterThanEqual)
992        | (MediaFeatureComparison::GreaterThanEqual, MediaFeatureComparison::GreaterThanEqual)
993        | (MediaFeatureComparison::GreaterThanEqual, MediaFeatureComparison::GreaterThan)
994        | (MediaFeatureComparison::LessThan, MediaFeatureComparison::LessThan)
995        | (MediaFeatureComparison::LessThan, MediaFeatureComparison::LessThanEqual)
996        | (MediaFeatureComparison::LessThanEqual, MediaFeatureComparison::LessThanEqual)
997        | (MediaFeatureComparison::LessThanEqual, MediaFeatureComparison::LessThan) => {}
998        _ => return Err(input.new_custom_error(ParserError::InvalidMediaQuery)),
999      };
1000
1001      let end_value = MediaFeatureValue::parse(input, name.value_type())?;
1002      if !end_value.check_type(name.value_type()) {
1003        return Err(input.new_custom_error(ParserError::InvalidMediaQuery));
1004      }
1005
1006      Ok(QueryFeature::Interval {
1007        name,
1008        start: value,
1009        start_operator,
1010        end: end_value,
1011        end_operator,
1012      })
1013    } else {
1014      let operator = operator.unwrap().opposite();
1015      Ok(QueryFeature::Range { name, operator, value })
1016    }
1017  }
1018
1019  pub(crate) fn needs_parens(&self, parent_operator: Option<Operator>, targets: &Targets) -> bool {
1020    parent_operator != Some(Operator::And)
1021      && matches!(self, QueryFeature::Interval { .. })
1022      && should_compile!(targets, MediaIntervalSyntax)
1023  }
1024}
1025
1026impl<'i, FeatureId: FeatureToCss> ToCss for QueryFeature<'i, FeatureId> {
1027  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
1028  where
1029    W: std::fmt::Write,
1030  {
1031    dest.write_char('(')?;
1032
1033    match self {
1034      QueryFeature::Boolean { name } => {
1035        name.to_css(dest)?;
1036      }
1037      QueryFeature::Plain { name, value } => {
1038        name.to_css(dest)?;
1039        dest.delim(':', false)?;
1040        value.to_css(dest)?;
1041      }
1042      QueryFeature::Range { name, operator, value } => {
1043        // If range syntax is unsupported, use min/max prefix if possible.
1044        if should_compile!(dest.targets, MediaRangeSyntax) {
1045          return write_min_max(operator, name, value, dest);
1046        }
1047
1048        name.to_css(dest)?;
1049        operator.to_css(dest)?;
1050        value.to_css(dest)?;
1051      }
1052      QueryFeature::Interval {
1053        name,
1054        start,
1055        start_operator,
1056        end,
1057        end_operator,
1058      } => {
1059        if should_compile!(dest.targets, MediaIntervalSyntax) {
1060          write_min_max(&start_operator.opposite(), name, start, dest)?;
1061          dest.write_str(" and (")?;
1062          return write_min_max(end_operator, name, end, dest);
1063        }
1064
1065        start.to_css(dest)?;
1066        start_operator.to_css(dest)?;
1067        name.to_css(dest)?;
1068        end_operator.to_css(dest)?;
1069        end.to_css(dest)?;
1070      }
1071    }
1072
1073    dest.write_char(')')
1074  }
1075}
1076
1077/// A media feature name.
1078#[derive(Debug, Clone, PartialEq)]
1079#[cfg_attr(feature = "visitor", derive(Visit))]
1080#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
1081#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(untagged))]
1082#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
1083pub enum MediaFeatureName<'i, FeatureId> {
1084  /// A standard media query feature identifier.
1085  Standard(FeatureId),
1086  /// A custom author-defined environment variable.
1087  #[cfg_attr(feature = "serde", serde(borrow))]
1088  Custom(DashedIdent<'i>),
1089  /// An unknown environment variable.
1090  Unknown(Ident<'i>),
1091}
1092
1093impl<'i, FeatureId: for<'x> Parse<'x>> MediaFeatureName<'i, FeatureId> {
1094  /// Parses a media feature name.
1095  pub fn parse<'t>(
1096    input: &mut Parser<'i, 't>,
1097  ) -> Result<(Self, Option<MediaFeatureComparison>), ParseError<'i, ParserError<'i>>> {
1098    let ident = input.expect_ident()?;
1099
1100    if ident.starts_with("--") {
1101      return Ok((MediaFeatureName::Custom(DashedIdent(ident.into())), None));
1102    }
1103
1104    let mut name = ident.as_ref();
1105
1106    // Webkit places its prefixes before "min" and "max". Remove it first, and
1107    // re-add after removing min/max.
1108    let is_webkit = starts_with_ignore_ascii_case(&name, "-webkit-");
1109    if is_webkit {
1110      name = &name[8..];
1111    }
1112
1113    let comparator = if starts_with_ignore_ascii_case(&name, "min-") {
1114      name = &name[4..];
1115      Some(MediaFeatureComparison::GreaterThanEqual)
1116    } else if starts_with_ignore_ascii_case(&name, "max-") {
1117      name = &name[4..];
1118      Some(MediaFeatureComparison::LessThanEqual)
1119    } else {
1120      None
1121    };
1122
1123    let name = if is_webkit {
1124      Cow::Owned(format!("-webkit-{}", name))
1125    } else {
1126      Cow::Borrowed(name)
1127    };
1128
1129    if let Ok(standard) = FeatureId::parse_string(&name) {
1130      return Ok((MediaFeatureName::Standard(standard), comparator));
1131    }
1132
1133    Ok((MediaFeatureName::Unknown(Ident(ident.into())), None))
1134  }
1135}
1136
1137mod private {
1138  use super::*;
1139
1140  /// A trait for feature ids which can get a value type.
1141  pub trait ValueType {
1142    /// Returns the value type for this feature id.
1143    fn value_type(&self) -> MediaFeatureType;
1144  }
1145}
1146
1147pub(crate) use private::ValueType;
1148
1149impl<'i, FeatureId: ValueType> ValueType for MediaFeatureName<'i, FeatureId> {
1150  fn value_type(&self) -> MediaFeatureType {
1151    match self {
1152      Self::Standard(standard) => standard.value_type(),
1153      _ => MediaFeatureType::Unknown,
1154    }
1155  }
1156}
1157
1158impl<'i, FeatureId: FeatureToCss> ToCss for MediaFeatureName<'i, FeatureId> {
1159  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
1160  where
1161    W: std::fmt::Write,
1162  {
1163    match self {
1164      Self::Standard(v) => v.to_css(dest),
1165      Self::Custom(v) => v.to_css(dest),
1166      Self::Unknown(v) => v.to_css(dest),
1167    }
1168  }
1169}
1170
1171impl<'i, FeatureId: FeatureToCss> FeatureToCss for MediaFeatureName<'i, FeatureId> {
1172  fn to_css_with_prefix<W>(&self, prefix: &str, dest: &mut Printer<W>) -> Result<(), PrinterError>
1173  where
1174    W: std::fmt::Write,
1175  {
1176    match self {
1177      Self::Standard(v) => v.to_css_with_prefix(prefix, dest),
1178      Self::Custom(v) => {
1179        dest.write_str(prefix)?;
1180        v.to_css(dest)
1181      }
1182      Self::Unknown(v) => {
1183        dest.write_str(prefix)?;
1184        v.to_css(dest)
1185      }
1186    }
1187  }
1188}
1189
1190/// The type of a media feature.
1191#[derive(PartialEq)]
1192pub enum MediaFeatureType {
1193  /// A length value.
1194  Length,
1195  /// A number value.
1196  Number,
1197  /// An integer value.
1198  Integer,
1199  /// A boolean value, either 0 or 1.
1200  Boolean,
1201  /// A resolution.
1202  Resolution,
1203  /// A ratio.
1204  Ratio,
1205  /// An identifier.
1206  Ident,
1207  /// An unknown type.
1208  Unknown,
1209}
1210
1211impl MediaFeatureType {
1212  fn allows_ranges(&self) -> bool {
1213    use MediaFeatureType::*;
1214    match self {
1215      Length => true,
1216      Number => true,
1217      Integer => true,
1218      Boolean => false,
1219      Resolution => true,
1220      Ratio => true,
1221      Ident => false,
1222      Unknown => true,
1223    }
1224  }
1225}
1226
1227macro_rules! define_query_features {
1228  (
1229    $(#[$outer:meta])*
1230    $vis:vis enum $name:ident {
1231      $(
1232        $(#[$meta: meta])*
1233        $str: literal: $id: ident = $ty: ident,
1234      )+
1235    }
1236  ) => {
1237    crate::macros::enum_property! {
1238      $(#[$outer])*
1239      $vis enum $name {
1240        $(
1241          $(#[$meta])*
1242          $str: $id,
1243        )+
1244      }
1245    }
1246
1247    impl ValueType for $name {
1248      fn value_type(&self) -> MediaFeatureType {
1249        match self {
1250          $(
1251            Self::$id => MediaFeatureType::$ty,
1252          )+
1253        }
1254      }
1255    }
1256  }
1257}
1258
1259pub(crate) use define_query_features;
1260
1261define_query_features! {
1262  /// A media query feature identifier.
1263  pub enum MediaFeatureId {
1264    /// The [width](https://w3c.github.io/csswg-drafts/mediaqueries-5/#width) media feature.
1265    "width": Width = Length,
1266    /// The [height](https://w3c.github.io/csswg-drafts/mediaqueries-5/#height) media feature.
1267    "height": Height = Length,
1268    /// The [aspect-ratio](https://w3c.github.io/csswg-drafts/mediaqueries-5/#aspect-ratio) media feature.
1269    "aspect-ratio": AspectRatio = Ratio,
1270    /// The [orientation](https://w3c.github.io/csswg-drafts/mediaqueries-5/#orientation) media feature.
1271    "orientation": Orientation = Ident,
1272    /// The [overflow-block](https://w3c.github.io/csswg-drafts/mediaqueries-5/#overflow-block) media feature.
1273    "overflow-block": OverflowBlock = Ident,
1274    /// The [overflow-inline](https://w3c.github.io/csswg-drafts/mediaqueries-5/#overflow-inline) media feature.
1275    "overflow-inline": OverflowInline = Ident,
1276    /// The [horizontal-viewport-segments](https://w3c.github.io/csswg-drafts/mediaqueries-5/#horizontal-viewport-segments) media feature.
1277    "horizontal-viewport-segments": HorizontalViewportSegments = Integer,
1278    /// The [vertical-viewport-segments](https://w3c.github.io/csswg-drafts/mediaqueries-5/#vertical-viewport-segments) media feature.
1279    "vertical-viewport-segments": VerticalViewportSegments = Integer,
1280    /// The [display-mode](https://w3c.github.io/csswg-drafts/mediaqueries-5/#display-mode) media feature.
1281    "display-mode": DisplayMode = Ident,
1282    /// The [resolution](https://w3c.github.io/csswg-drafts/mediaqueries-5/#resolution) media feature.
1283    "resolution": Resolution = Resolution, // | infinite??
1284    /// The [scan](https://w3c.github.io/csswg-drafts/mediaqueries-5/#scan) media feature.
1285    "scan": Scan = Ident,
1286    /// The [grid](https://w3c.github.io/csswg-drafts/mediaqueries-5/#grid) media feature.
1287    "grid": Grid = Boolean,
1288    /// The [update](https://w3c.github.io/csswg-drafts/mediaqueries-5/#update) media feature.
1289    "update": Update = Ident,
1290    /// The [environment-blending](https://w3c.github.io/csswg-drafts/mediaqueries-5/#environment-blending) media feature.
1291    "environment-blending": EnvironmentBlending = Ident,
1292    /// The [color](https://w3c.github.io/csswg-drafts/mediaqueries-5/#color) media feature.
1293    "color": Color = Integer,
1294    /// The [color-index](https://w3c.github.io/csswg-drafts/mediaqueries-5/#color-index) media feature.
1295    "color-index": ColorIndex = Integer,
1296    /// The [monochrome](https://w3c.github.io/csswg-drafts/mediaqueries-5/#monochrome) media feature.
1297    "monochrome": Monochrome = Integer,
1298    /// The [color-gamut](https://w3c.github.io/csswg-drafts/mediaqueries-5/#color-gamut) media feature.
1299    "color-gamut": ColorGamut = Ident,
1300    /// The [dynamic-range](https://w3c.github.io/csswg-drafts/mediaqueries-5/#dynamic-range) media feature.
1301    "dynamic-range": DynamicRange = Ident,
1302    /// The [inverted-colors](https://w3c.github.io/csswg-drafts/mediaqueries-5/#inverted-colors) media feature.
1303    "inverted-colors": InvertedColors = Ident,
1304    /// The [pointer](https://w3c.github.io/csswg-drafts/mediaqueries-5/#pointer) media feature.
1305    "pointer": Pointer = Ident,
1306    /// The [hover](https://w3c.github.io/csswg-drafts/mediaqueries-5/#hover) media feature.
1307    "hover": Hover = Ident,
1308    /// The [any-pointer](https://w3c.github.io/csswg-drafts/mediaqueries-5/#any-pointer) media feature.
1309    "any-pointer": AnyPointer = Ident,
1310    /// The [any-hover](https://w3c.github.io/csswg-drafts/mediaqueries-5/#any-hover) media feature.
1311    "any-hover": AnyHover = Ident,
1312    /// The [nav-controls](https://w3c.github.io/csswg-drafts/mediaqueries-5/#nav-controls) media feature.
1313    "nav-controls": NavControls = Ident,
1314    /// The [video-color-gamut](https://w3c.github.io/csswg-drafts/mediaqueries-5/#video-color-gamut) media feature.
1315    "video-color-gamut": VideoColorGamut = Ident,
1316    /// The [video-dynamic-range](https://w3c.github.io/csswg-drafts/mediaqueries-5/#video-dynamic-range) media feature.
1317    "video-dynamic-range": VideoDynamicRange = Ident,
1318    /// The [scripting](https://w3c.github.io/csswg-drafts/mediaqueries-5/#scripting) media feature.
1319    "scripting": Scripting = Ident,
1320    /// The [prefers-reduced-motion](https://w3c.github.io/csswg-drafts/mediaqueries-5/#prefers-reduced-motion) media feature.
1321    "prefers-reduced-motion": PrefersReducedMotion = Ident,
1322    /// The [prefers-reduced-transparency](https://w3c.github.io/csswg-drafts/mediaqueries-5/#prefers-reduced-transparency) media feature.
1323    "prefers-reduced-transparency": PrefersReducedTransparency = Ident,
1324    /// The [prefers-contrast](https://w3c.github.io/csswg-drafts/mediaqueries-5/#prefers-contrast) media feature.
1325    "prefers-contrast": PrefersContrast = Ident,
1326    /// The [forced-colors](https://w3c.github.io/csswg-drafts/mediaqueries-5/#forced-colors) media feature.
1327    "forced-colors": ForcedColors = Ident,
1328    /// The [prefers-color-scheme](https://w3c.github.io/csswg-drafts/mediaqueries-5/#prefers-color-scheme) media feature.
1329    "prefers-color-scheme": PrefersColorScheme = Ident,
1330    /// The [prefers-reduced-data](https://w3c.github.io/csswg-drafts/mediaqueries-5/#prefers-reduced-data) media feature.
1331    "prefers-reduced-data": PrefersReducedData = Ident,
1332    /// The [device-width](https://w3c.github.io/csswg-drafts/mediaqueries-5/#device-width) media feature.
1333    "device-width": DeviceWidth = Length,
1334    /// The [device-height](https://w3c.github.io/csswg-drafts/mediaqueries-5/#device-height) media feature.
1335    "device-height": DeviceHeight = Length,
1336    /// The [device-aspect-ratio](https://w3c.github.io/csswg-drafts/mediaqueries-5/#device-aspect-ratio) media feature.
1337    "device-aspect-ratio": DeviceAspectRatio = Ratio,
1338
1339    /// The non-standard -webkit-device-pixel-ratio media feature.
1340    "-webkit-device-pixel-ratio": WebKitDevicePixelRatio = Number,
1341    /// The non-standard -moz-device-pixel-ratio media feature.
1342    "-moz-device-pixel-ratio": MozDevicePixelRatio = Number,
1343
1344    // TODO: parse non-standard media queries?
1345    // -moz-device-orientation
1346    // -webkit-transform-3d
1347  }
1348}
1349
1350pub(crate) trait FeatureToCss: ToCss {
1351  fn to_css_with_prefix<W>(&self, prefix: &str, dest: &mut Printer<W>) -> Result<(), PrinterError>
1352  where
1353    W: std::fmt::Write;
1354}
1355
1356impl FeatureToCss for MediaFeatureId {
1357  fn to_css_with_prefix<W>(&self, prefix: &str, dest: &mut Printer<W>) -> Result<(), PrinterError>
1358  where
1359    W: std::fmt::Write,
1360  {
1361    match self {
1362      MediaFeatureId::WebKitDevicePixelRatio => {
1363        dest.write_str("-webkit-")?;
1364        dest.write_str(prefix)?;
1365        dest.write_str("device-pixel-ratio")
1366      }
1367      _ => {
1368        dest.write_str(prefix)?;
1369        self.to_css(dest)
1370      }
1371    }
1372  }
1373}
1374
1375#[inline]
1376fn write_min_max<W, FeatureId: FeatureToCss>(
1377  operator: &MediaFeatureComparison,
1378  name: &MediaFeatureName<FeatureId>,
1379  value: &MediaFeatureValue,
1380  dest: &mut Printer<W>,
1381) -> Result<(), PrinterError>
1382where
1383  W: std::fmt::Write,
1384{
1385  let prefix = match operator {
1386    MediaFeatureComparison::GreaterThan | MediaFeatureComparison::GreaterThanEqual => Some("min-"),
1387    MediaFeatureComparison::LessThan | MediaFeatureComparison::LessThanEqual => Some("max-"),
1388    MediaFeatureComparison::Equal => None,
1389  };
1390
1391  if let Some(prefix) = prefix {
1392    name.to_css_with_prefix(prefix, dest)?;
1393  } else {
1394    name.to_css(dest)?;
1395  }
1396
1397  dest.delim(':', false)?;
1398
1399  let adjusted = match operator {
1400    MediaFeatureComparison::GreaterThan => Some(value.clone() + 0.001),
1401    MediaFeatureComparison::LessThan => Some(value.clone() + -0.001),
1402    _ => None,
1403  };
1404
1405  if let Some(value) = adjusted {
1406    value.to_css(dest)?;
1407  } else {
1408    value.to_css(dest)?;
1409  }
1410
1411  dest.write_char(')')?;
1412  Ok(())
1413}
1414
1415/// [media feature value](https://drafts.csswg.org/mediaqueries/#typedef-mf-value) within a media query.
1416///
1417/// See [MediaFeature](MediaFeature).
1418#[derive(Clone, Debug, PartialEq)]
1419#[cfg_attr(feature = "visitor", derive(Visit), visit(visit_media_feature_value, MEDIA_QUERIES))]
1420#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
1421#[cfg_attr(
1422  feature = "serde",
1423  derive(serde::Serialize, serde::Deserialize),
1424  serde(tag = "type", content = "value", rename_all = "kebab-case")
1425)]
1426#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
1427pub enum MediaFeatureValue<'i> {
1428  /// A length value.
1429  Length(Length),
1430  /// A number value.
1431  Number(CSSNumber),
1432  /// An integer value.
1433  Integer(CSSInteger),
1434  /// A boolean value.
1435  Boolean(bool),
1436  /// A resolution.
1437  Resolution(Resolution),
1438  /// A ratio.
1439  Ratio(Ratio),
1440  /// An identifier.
1441  #[cfg_attr(feature = "serde", serde(borrow))]
1442  Ident(Ident<'i>),
1443  /// An environment variable reference.
1444  Env(EnvironmentVariable<'i>),
1445}
1446
1447impl<'i> MediaFeatureValue<'i> {
1448  fn value_type(&self) -> MediaFeatureType {
1449    use MediaFeatureValue::*;
1450    match self {
1451      Length(..) => MediaFeatureType::Length,
1452      Number(..) => MediaFeatureType::Number,
1453      Integer(..) => MediaFeatureType::Integer,
1454      Boolean(..) => MediaFeatureType::Boolean,
1455      Resolution(..) => MediaFeatureType::Resolution,
1456      Ratio(..) => MediaFeatureType::Ratio,
1457      Ident(..) => MediaFeatureType::Ident,
1458      Env(..) => MediaFeatureType::Unknown,
1459    }
1460  }
1461
1462  fn check_type(&self, expected_type: MediaFeatureType) -> bool {
1463    match (expected_type, self.value_type()) {
1464      (_, MediaFeatureType::Unknown) | (MediaFeatureType::Unknown, _) => true,
1465      (a, b) => a == b,
1466    }
1467  }
1468}
1469
1470impl<'i> MediaFeatureValue<'i> {
1471  /// Parses a single media query feature value, with an expected type.
1472  /// If the type is unknown, pass MediaFeatureType::Unknown instead.
1473  pub fn parse<'t>(
1474    input: &mut Parser<'i, 't>,
1475    expected_type: MediaFeatureType,
1476  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
1477    if let Ok(value) = input.try_parse(|input| Self::parse_known(input, expected_type)) {
1478      return Ok(value);
1479    }
1480
1481    Self::parse_unknown(input)
1482  }
1483
1484  fn parse_known<'t>(
1485    input: &mut Parser<'i, 't>,
1486    expected_type: MediaFeatureType,
1487  ) -> Result<Self, ParseError<'i, ParserError<'i>>> {
1488    match expected_type {
1489      MediaFeatureType::Boolean => {
1490        let value = CSSInteger::parse(input)?;
1491        if value != 0 && value != 1 {
1492          return Err(input.new_custom_error(ParserError::InvalidValue));
1493        }
1494        Ok(MediaFeatureValue::Boolean(value == 1))
1495      }
1496      MediaFeatureType::Number => Ok(MediaFeatureValue::Number(CSSNumber::parse(input)?)),
1497      MediaFeatureType::Integer => Ok(MediaFeatureValue::Integer(CSSInteger::parse(input)?)),
1498      MediaFeatureType::Length => Ok(MediaFeatureValue::Length(Length::parse(input)?)),
1499      MediaFeatureType::Resolution => Ok(MediaFeatureValue::Resolution(Resolution::parse(input)?)),
1500      MediaFeatureType::Ratio => Ok(MediaFeatureValue::Ratio(Ratio::parse(input)?)),
1501      MediaFeatureType::Ident => Ok(MediaFeatureValue::Ident(Ident::parse(input)?)),
1502      MediaFeatureType::Unknown => Err(input.new_custom_error(ParserError::InvalidValue)),
1503    }
1504  }
1505
1506  fn parse_unknown<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
1507    // Ratios are ambiguous with numbers because the second param is optional (e.g. 2/1 == 2).
1508    // We require the / delimiter when parsing ratios so that 2/1 ends up as a ratio and 2 is
1509    // parsed as a number.
1510    if let Ok(ratio) = input.try_parse(Ratio::parse_required) {
1511      return Ok(MediaFeatureValue::Ratio(ratio));
1512    }
1513
1514    // Parse number next so that unitless values are not parsed as lengths.
1515    if let Ok(num) = input.try_parse(CSSNumber::parse) {
1516      return Ok(MediaFeatureValue::Number(num));
1517    }
1518
1519    if let Ok(length) = input.try_parse(Length::parse) {
1520      return Ok(MediaFeatureValue::Length(length));
1521    }
1522
1523    if let Ok(res) = input.try_parse(Resolution::parse) {
1524      return Ok(MediaFeatureValue::Resolution(res));
1525    }
1526
1527    if let Ok(env) = input.try_parse(|input| EnvironmentVariable::parse(input, &ParserOptions::default(), 0)) {
1528      return Ok(MediaFeatureValue::Env(env));
1529    }
1530
1531    let ident = Ident::parse(input)?;
1532    Ok(MediaFeatureValue::Ident(ident))
1533  }
1534}
1535
1536impl<'i> ToCss for MediaFeatureValue<'i> {
1537  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
1538  where
1539    W: std::fmt::Write,
1540  {
1541    match self {
1542      MediaFeatureValue::Length(len) => len.to_css(dest),
1543      MediaFeatureValue::Number(num) => num.to_css(dest),
1544      MediaFeatureValue::Integer(num) => num.to_css(dest),
1545      MediaFeatureValue::Boolean(b) => {
1546        if *b {
1547          dest.write_char('1')
1548        } else {
1549          dest.write_char('0')
1550        }
1551      }
1552      MediaFeatureValue::Resolution(res) => res.to_css(dest),
1553      MediaFeatureValue::Ratio(ratio) => ratio.to_css(dest),
1554      MediaFeatureValue::Ident(id) => {
1555        id.to_css(dest)?;
1556        Ok(())
1557      }
1558      MediaFeatureValue::Env(env) => env.to_css(dest, false),
1559    }
1560  }
1561}
1562
1563impl<'i> std::ops::Add<f32> for MediaFeatureValue<'i> {
1564  type Output = Self;
1565
1566  fn add(self, other: f32) -> Self {
1567    match self {
1568      MediaFeatureValue::Length(len) => MediaFeatureValue::Length(len + Length::px(other)),
1569      MediaFeatureValue::Number(num) => MediaFeatureValue::Number(num + other),
1570      MediaFeatureValue::Integer(num) => {
1571        MediaFeatureValue::Integer(num + if other.is_sign_positive() { 1 } else { -1 })
1572      }
1573      MediaFeatureValue::Boolean(v) => MediaFeatureValue::Boolean(v),
1574      MediaFeatureValue::Resolution(res) => MediaFeatureValue::Resolution(res + other),
1575      MediaFeatureValue::Ratio(ratio) => MediaFeatureValue::Ratio(ratio + other),
1576      MediaFeatureValue::Ident(id) => MediaFeatureValue::Ident(id),
1577      MediaFeatureValue::Env(env) => MediaFeatureValue::Env(env), // TODO: calc support
1578    }
1579  }
1580}
1581
1582/// Consumes an operation or a colon, or returns an error.
1583fn consume_operation_or_colon<'i, 't>(
1584  input: &mut Parser<'i, 't>,
1585  allow_colon: bool,
1586) -> Result<Option<MediaFeatureComparison>, ParseError<'i, ParserError<'i>>> {
1587  let location = input.current_source_location();
1588  let first_delim = {
1589    let location = input.current_source_location();
1590    let next_token = input.next()?;
1591    match next_token {
1592      Token::Colon if allow_colon => return Ok(None),
1593      Token::Delim(oper) => oper,
1594      t => return Err(location.new_unexpected_token_error(t.clone())),
1595    }
1596  };
1597  Ok(Some(match first_delim {
1598    '=' => MediaFeatureComparison::Equal,
1599    '>' => {
1600      if input.try_parse(|i| i.expect_delim('=')).is_ok() {
1601        MediaFeatureComparison::GreaterThanEqual
1602      } else {
1603        MediaFeatureComparison::GreaterThan
1604      }
1605    }
1606    '<' => {
1607      if input.try_parse(|i| i.expect_delim('=')).is_ok() {
1608        MediaFeatureComparison::LessThanEqual
1609      } else {
1610        MediaFeatureComparison::LessThan
1611      }
1612    }
1613    d => return Err(location.new_unexpected_token_error(Token::Delim(*d))),
1614  }))
1615}
1616
1617fn process_condition<'i>(
1618  loc: Location,
1619  custom_media: &HashMap<CowArcStr<'i>, CustomMediaRule<'i>>,
1620  media_type: &mut MediaType<'i>,
1621  qualifier: &mut Option<Qualifier>,
1622  condition: &mut MediaCondition<'i>,
1623  seen: &mut HashSet<DashedIdent<'i>>,
1624) -> Result<bool, MinifyError> {
1625  match condition {
1626    MediaCondition::Not(cond) => {
1627      let used = process_condition(loc, custom_media, media_type, qualifier, &mut *cond, seen)?;
1628      if !used {
1629        // If unused, only a media type remains so apply a not qualifier.
1630        // If it is already not, then it cancels out.
1631        *qualifier = if *qualifier == Some(Qualifier::Not) {
1632          None
1633        } else {
1634          Some(Qualifier::Not)
1635        };
1636        return Ok(false);
1637      }
1638
1639      // Unwrap nested nots
1640      match &**cond {
1641        MediaCondition::Not(cond) => {
1642          *condition = (**cond).clone();
1643        }
1644        _ => {}
1645      }
1646    }
1647    MediaCondition::Operation { conditions, .. } => {
1648      let mut res = Ok(true);
1649      conditions.retain_mut(|condition| {
1650        let r = process_condition(loc, custom_media, media_type, qualifier, condition, seen);
1651        if let Ok(used) = r {
1652          used
1653        } else {
1654          res = r;
1655          false
1656        }
1657      });
1658      return res;
1659    }
1660    MediaCondition::Feature(QueryFeature::Boolean { name }) => {
1661      let name = match name {
1662        MediaFeatureName::Custom(name) => name,
1663        _ => return Ok(true),
1664      };
1665
1666      if seen.contains(name) {
1667        return Err(ErrorWithLocation {
1668          kind: MinifyErrorKind::CircularCustomMedia { name: name.to_string() },
1669          loc,
1670        });
1671      }
1672
1673      let rule = custom_media.get(&name.0).ok_or_else(|| ErrorWithLocation {
1674        kind: MinifyErrorKind::CustomMediaNotDefined { name: name.to_string() },
1675        loc,
1676      })?;
1677
1678      seen.insert(name.clone());
1679
1680      let mut res = Ok(true);
1681      let mut conditions: Vec<MediaCondition> = rule
1682        .query
1683        .media_queries
1684        .iter()
1685        .filter_map(|query| {
1686          if query.media_type != MediaType::All || query.qualifier != None {
1687            if *media_type == MediaType::All {
1688              // `not all` will never match.
1689              if *qualifier == Some(Qualifier::Not) {
1690                res = Ok(false);
1691                return None;
1692              }
1693
1694              // Propagate media type and qualifier to @media rule.
1695              *media_type = query.media_type.clone();
1696              *qualifier = query.qualifier.clone();
1697            } else if query.media_type != *media_type || query.qualifier != *qualifier {
1698              // Boolean logic with media types is hard to emulate, so we error for now.
1699              res = Err(ErrorWithLocation {
1700                kind: MinifyErrorKind::UnsupportedCustomMediaBooleanLogic {
1701                  custom_media_loc: rule.loc,
1702                },
1703                loc,
1704              });
1705              return None;
1706            }
1707          }
1708
1709          if let Some(condition) = &query.condition {
1710            let mut condition = condition.clone();
1711            let r = process_condition(loc, custom_media, media_type, qualifier, &mut condition, seen);
1712            if r.is_err() {
1713              res = r;
1714            }
1715            // Parentheses are required around the condition unless there is a single media feature.
1716            match condition {
1717              MediaCondition::Feature(..) => Some(condition),
1718              _ => Some(condition),
1719            }
1720          } else {
1721            None
1722          }
1723        })
1724        .collect();
1725
1726      seen.remove(name);
1727
1728      if res.is_err() {
1729        return res;
1730      }
1731
1732      if conditions.is_empty() {
1733        return Ok(false);
1734      }
1735
1736      if conditions.len() == 1 {
1737        *condition = conditions.pop().unwrap();
1738      } else {
1739        *condition = MediaCondition::Operation {
1740          conditions,
1741          operator: Operator::Or,
1742        };
1743      }
1744    }
1745    _ => {}
1746  }
1747
1748  Ok(true)
1749}
1750
1751#[cfg(test)]
1752mod tests {
1753  use super::*;
1754  use crate::{
1755    stylesheet::PrinterOptions,
1756    targets::{Browsers, Targets},
1757  };
1758
1759  fn parse(s: &str) -> MediaQuery {
1760    let mut input = ParserInput::new(&s);
1761    let mut parser = Parser::new(&mut input);
1762    MediaQuery::parse(&mut parser).unwrap()
1763  }
1764
1765  fn and(a: &str, b: &str) -> String {
1766    let mut a = parse(a);
1767    let b = parse(b);
1768    a.and(&b).unwrap();
1769    a.to_css_string(PrinterOptions::default()).unwrap()
1770  }
1771
1772  #[test]
1773  fn test_and() {
1774    assert_eq!(and("(min-width: 250px)", "(color)"), "(width >= 250px) and (color)");
1775    assert_eq!(
1776      and("(min-width: 250px) or (color)", "(orientation: landscape)"),
1777      "((width >= 250px) or (color)) and (orientation: landscape)"
1778    );
1779    assert_eq!(
1780      and("(min-width: 250px) and (color)", "(orientation: landscape)"),
1781      "(width >= 250px) and (color) and (orientation: landscape)"
1782    );
1783    assert_eq!(and("all", "print"), "print");
1784    assert_eq!(and("print", "all"), "print");
1785    assert_eq!(and("all", "not print"), "not print");
1786    assert_eq!(and("not print", "all"), "not print");
1787    assert_eq!(and("not all", "print"), "not all");
1788    assert_eq!(and("print", "not all"), "not all");
1789    assert_eq!(and("print", "screen"), "not all");
1790    assert_eq!(and("not print", "screen"), "screen");
1791    assert_eq!(and("print", "not screen"), "print");
1792    assert_eq!(and("not screen", "print"), "print");
1793    assert_eq!(and("not screen", "not all"), "not all");
1794    assert_eq!(and("print", "(min-width: 250px)"), "print and (width >= 250px)");
1795    assert_eq!(and("(min-width: 250px)", "print"), "print and (width >= 250px)");
1796    assert_eq!(
1797      and("print and (min-width: 250px)", "(color)"),
1798      "print and (width >= 250px) and (color)"
1799    );
1800    assert_eq!(and("all", "only screen"), "only screen");
1801    assert_eq!(and("only screen", "all"), "only screen");
1802    assert_eq!(and("print", "print"), "print");
1803  }
1804
1805  #[test]
1806  fn test_negated_interval_parens() {
1807    let media_query = parse("screen and not (200px <= width < 500px)");
1808    let printer_options = PrinterOptions {
1809      targets: Targets {
1810        browsers: Some(Browsers {
1811          chrome: Some(95 << 16),
1812          ..Default::default()
1813        }),
1814        ..Default::default()
1815      },
1816      ..Default::default()
1817    };
1818    assert_eq!(
1819      media_query.to_css_string(printer_options).unwrap(),
1820      "screen and not ((min-width: 200px) and (max-width: 499.999px))"
1821    );
1822  }
1823}