1use 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#[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 #[cfg_attr(feature = "serde", serde(borrow))]
44 pub media_queries: Vec<MediaQuery<'i>>,
45}
46
47impl<'i> MediaList<'i> {
48 pub fn new() -> Self {
50 MediaList { media_queries: vec![] }
51 }
52
53 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 pub fn always_matches(&self) -> bool {
112 self.media_queries.is_empty() || self.media_queries.iter().all(|mq| mq.always_matches())
114 }
115
116 pub fn never_matches(&self) -> bool {
118 !self.media_queries.is_empty() && self.media_queries.iter().all(|mq| mq.never_matches())
119 }
120
121 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 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 pub enum Qualifier {
180 Only,
182 Not,
184 }
185}
186
187#[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 All,
199 Print,
202 Screen,
204 #[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#[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 pub qualifier: Option<Qualifier>,
264 #[cfg_attr(feature = "serde", serde(borrow))]
266 pub media_type: MediaType<'i>,
267 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 pub fn always_matches(&self) -> bool {
337 self.qualifier == None && self.media_type == MediaType::All && self.condition == None
338 }
339
340 pub fn never_matches(&self) -> bool {
342 self.qualifier == Some(Qualifier::Not) && self.media_type == MediaType::All && self.condition == None
343 }
344
345 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 ((&Some(Qualifier::Not), &MediaType::All), _) |
356 (_, (&Some(Qualifier::Not), &MediaType::All)) => (Some(Qualifier::Not), MediaType::All),
357 ((&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 ((_, MediaType::All), (q, t)) |
370 ((q, t), (_, MediaType::All)) |
371 ((&Some(Qualifier::Not), _), (q, t)) |
374 ((q, t), (&Some(Qualifier::Not), _)) => (q.clone(), t.clone()),
375 ((_, 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 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 pub enum Operator {
490 And,
492 Or,
494 }
495}
496
497#[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 #[cfg_attr(feature = "serde", serde(borrow, with = "ValueWrapper::<MediaFeature>"))]
510 Feature(MediaFeature<'i>),
511 #[cfg_attr(feature = "visitor", skip_type)]
513 #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<Box<MediaCondition>>"))]
514 Not(Box<MediaCondition<'i>>),
515 #[cfg_attr(feature = "visitor", skip_type)]
517 Operation {
518 operator: Operator,
520 conditions: Vec<MediaCondition<'i>>,
522 },
523}
524
525pub(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 #[derive(PartialEq, Eq, Clone, Copy)]
566 pub(crate) struct QueryConditionFlags: u8 {
567 const ALLOW_OR = 1 << 0;
569 const ALLOW_STYLE = 1 << 1;
571 }
572}
573
574impl<'i> MediaCondition<'i> {
575 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
642pub(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
699fn 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#[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 Equal,
800 GreaterThan,
802 GreaterThanEqual,
804 LessThan,
806 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#[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 Plain {
864 #[cfg_attr(feature = "serde", serde(borrow))]
866 name: MediaFeatureName<'i, FeatureId>,
867 value: MediaFeatureValue<'i>,
869 },
870 Boolean {
872 name: MediaFeatureName<'i, FeatureId>,
874 },
875 Range {
877 name: MediaFeatureName<'i, FeatureId>,
879 operator: MediaFeatureComparison,
881 value: MediaFeatureValue<'i>,
883 },
884 #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
886 Interval {
887 name: MediaFeatureName<'i, FeatureId>,
889 start: MediaFeatureValue<'i>,
891 start_operator: MediaFeatureComparison,
893 end: MediaFeatureValue<'i>,
895 end_operator: MediaFeatureComparison,
897 },
898}
899
900pub 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 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 let value = MediaFeatureValue::parse(input, name.value_type())?;
973 let operator = consume_operation_or_colon(input, false)?;
974
975 {
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 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 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#[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 Standard(FeatureId),
1086 #[cfg_attr(feature = "serde", serde(borrow))]
1088 Custom(DashedIdent<'i>),
1089 Unknown(Ident<'i>),
1091}
1092
1093impl<'i, FeatureId: for<'x> Parse<'x>> MediaFeatureName<'i, FeatureId> {
1094 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 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 pub trait ValueType {
1142 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#[derive(PartialEq)]
1192pub enum MediaFeatureType {
1193 Length,
1195 Number,
1197 Integer,
1199 Boolean,
1201 Resolution,
1203 Ratio,
1205 Ident,
1207 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 pub enum MediaFeatureId {
1264 "width": Width = Length,
1266 "height": Height = Length,
1268 "aspect-ratio": AspectRatio = Ratio,
1270 "orientation": Orientation = Ident,
1272 "overflow-block": OverflowBlock = Ident,
1274 "overflow-inline": OverflowInline = Ident,
1276 "horizontal-viewport-segments": HorizontalViewportSegments = Integer,
1278 "vertical-viewport-segments": VerticalViewportSegments = Integer,
1280 "display-mode": DisplayMode = Ident,
1282 "resolution": Resolution = Resolution, "scan": Scan = Ident,
1286 "grid": Grid = Boolean,
1288 "update": Update = Ident,
1290 "environment-blending": EnvironmentBlending = Ident,
1292 "color": Color = Integer,
1294 "color-index": ColorIndex = Integer,
1296 "monochrome": Monochrome = Integer,
1298 "color-gamut": ColorGamut = Ident,
1300 "dynamic-range": DynamicRange = Ident,
1302 "inverted-colors": InvertedColors = Ident,
1304 "pointer": Pointer = Ident,
1306 "hover": Hover = Ident,
1308 "any-pointer": AnyPointer = Ident,
1310 "any-hover": AnyHover = Ident,
1312 "nav-controls": NavControls = Ident,
1314 "video-color-gamut": VideoColorGamut = Ident,
1316 "video-dynamic-range": VideoDynamicRange = Ident,
1318 "scripting": Scripting = Ident,
1320 "prefers-reduced-motion": PrefersReducedMotion = Ident,
1322 "prefers-reduced-transparency": PrefersReducedTransparency = Ident,
1324 "prefers-contrast": PrefersContrast = Ident,
1326 "forced-colors": ForcedColors = Ident,
1328 "prefers-color-scheme": PrefersColorScheme = Ident,
1330 "prefers-reduced-data": PrefersReducedData = Ident,
1332 "device-width": DeviceWidth = Length,
1334 "device-height": DeviceHeight = Length,
1336 "device-aspect-ratio": DeviceAspectRatio = Ratio,
1338
1339 "-webkit-device-pixel-ratio": WebKitDevicePixelRatio = Number,
1341 "-moz-device-pixel-ratio": MozDevicePixelRatio = Number,
1343
1344 }
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#[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 Length(Length),
1430 Number(CSSNumber),
1432 Integer(CSSInteger),
1434 Boolean(bool),
1436 Resolution(Resolution),
1438 Ratio(Ratio),
1440 #[cfg_attr(feature = "serde", serde(borrow))]
1442 Ident(Ident<'i>),
1443 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 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 if let Ok(ratio) = input.try_parse(Ratio::parse_required) {
1511 return Ok(MediaFeatureValue::Ratio(ratio));
1512 }
1513
1514 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), }
1579 }
1580}
1581
1582fn 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 *qualifier = if *qualifier == Some(Qualifier::Not) {
1632 None
1633 } else {
1634 Some(Qualifier::Not)
1635 };
1636 return Ok(false);
1637 }
1638
1639 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 if *qualifier == Some(Qualifier::Not) {
1690 res = Ok(false);
1691 return None;
1692 }
1693
1694 *media_type = query.media_type.clone();
1696 *qualifier = query.qualifier.clone();
1697 } else if query.media_type != *media_type || query.qualifier != *qualifier {
1698 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 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}