Skip to main content

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