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, 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#[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>(
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 pub fn always_matches(&self) -> bool {
119 self.media_queries.is_empty() || self.media_queries.iter().all(|mq| mq.always_matches())
121 }
122
123 pub fn never_matches(&self) -> bool {
125 !self.media_queries.is_empty() && self.media_queries.iter().all(|mq| mq.never_matches())
126 }
127
128 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 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 pub enum Qualifier {
187 Only,
189 Not,
191 }
192}
193
194#[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 All,
206 Print,
209 Screen,
211 #[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#[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 pub qualifier: Option<Qualifier>,
271 #[cfg_attr(feature = "serde", serde(borrow))]
273 pub media_type: MediaType<'i>,
274 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 pub fn always_matches(&self) -> bool {
355 self.qualifier == None && self.media_type == MediaType::All && self.condition == None
356 }
357
358 pub fn never_matches(&self) -> bool {
360 self.qualifier == Some(Qualifier::Not) && self.media_type == MediaType::All && self.condition == None
361 }
362
363 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 ((&Some(Qualifier::Not), &MediaType::All), _) |
374 (_, (&Some(Qualifier::Not), &MediaType::All)) => (Some(Qualifier::Not), MediaType::All),
375 ((&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 ((_, MediaType::All), (q, t)) |
388 ((q, t), (_, MediaType::All)) |
389 ((&Some(Qualifier::Not), _), (q, t)) |
392 ((q, t), (&Some(Qualifier::Not), _)) => (q.clone(), t.clone()),
393 ((_, 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 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 pub enum Operator {
508 And,
510 Or,
512 }
513}
514
515#[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 #[cfg_attr(feature = "serde", serde(borrow, with = "ValueWrapper::<MediaFeature>"))]
528 Feature(MediaFeature<'i>),
529 #[cfg_attr(feature = "visitor", skip_type)]
531 #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<Box<MediaCondition>>"))]
532 Not(Box<MediaCondition<'i>>),
533 #[cfg_attr(feature = "visitor", skip_type)]
535 Operation {
536 operator: Operator,
538 conditions: Vec<MediaCondition<'i>>,
540 },
541 #[cfg_attr(feature = "serde", serde(borrow, with = "ValueWrapper::<TokenList>"))]
543 Unknown(TokenList<'i>),
544}
545
546pub(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 #[derive(PartialEq, Eq, Clone, Copy)]
604 pub(crate) struct QueryConditionFlags: u8 {
605 const ALLOW_OR = 1 << 0;
607 const ALLOW_STYLE = 1 << 1;
609 const ALLOW_SCROLL_STATE = 1 << 2;
611 }
612}
613
614impl<'i> MediaCondition<'i> {
615 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
695pub(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
769fn 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 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#[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 Equal,
899 GreaterThan,
901 GreaterThanEqual,
903 LessThan,
905 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#[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 Plain {
974 #[cfg_attr(feature = "serde", serde(borrow))]
976 name: MediaFeatureName<'i, FeatureId>,
977 value: MediaFeatureValue<'i>,
979 },
980 Boolean {
982 name: MediaFeatureName<'i, FeatureId>,
984 },
985 Range {
987 name: MediaFeatureName<'i, FeatureId>,
989 operator: MediaFeatureComparison,
991 value: MediaFeatureValue<'i>,
993 },
994 #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
996 Interval {
997 name: MediaFeatureName<'i, FeatureId>,
999 start: MediaFeatureValue<'i>,
1001 start_operator: MediaFeatureComparison,
1003 end: MediaFeatureValue<'i>,
1005 end_operator: MediaFeatureComparison,
1007 },
1008}
1009
1010pub 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 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 let value = MediaFeatureValue::parse(input, name.value_type())?;
1096 let operator = consume_operation_or_colon(input, false)?;
1097
1098 {
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 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 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#[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 Standard(FeatureId),
1232 #[cfg_attr(feature = "serde", serde(borrow))]
1234 Custom(DashedIdent<'i>),
1235 Unknown(Ident<'i>),
1237}
1238
1239impl<'i, FeatureId: for<'x> Parse<'x>> MediaFeatureName<'i, FeatureId> {
1240 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 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 pub trait ValueType {
1288 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#[derive(PartialEq)]
1338pub enum MediaFeatureType {
1339 Length,
1341 Number,
1343 Integer,
1345 Boolean,
1347 Resolution,
1349 Ratio,
1351 Ident,
1353 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 pub enum MediaFeatureId {
1410 "width": Width = Length,
1412 "height": Height = Length,
1414 "aspect-ratio": AspectRatio = Ratio,
1416 "orientation": Orientation = Ident,
1418 "overflow-block": OverflowBlock = Ident,
1420 "overflow-inline": OverflowInline = Ident,
1422 "horizontal-viewport-segments": HorizontalViewportSegments = Integer,
1424 "vertical-viewport-segments": VerticalViewportSegments = Integer,
1426 "display-mode": DisplayMode = Ident,
1428 "resolution": Resolution = Resolution, "scan": Scan = Ident,
1432 "grid": Grid = Boolean,
1434 "update": Update = Ident,
1436 "environment-blending": EnvironmentBlending = Ident,
1438 "color": Color = Integer,
1440 "color-index": ColorIndex = Integer,
1442 "monochrome": Monochrome = Integer,
1444 "color-gamut": ColorGamut = Ident,
1446 "dynamic-range": DynamicRange = Ident,
1448 "inverted-colors": InvertedColors = Ident,
1450 "pointer": Pointer = Ident,
1452 "hover": Hover = Ident,
1454 "any-pointer": AnyPointer = Ident,
1456 "any-hover": AnyHover = Ident,
1458 "nav-controls": NavControls = Ident,
1460 "video-color-gamut": VideoColorGamut = Ident,
1462 "video-dynamic-range": VideoDynamicRange = Ident,
1464 "scripting": Scripting = Ident,
1466 "prefers-reduced-motion": PrefersReducedMotion = Ident,
1468 "prefers-reduced-transparency": PrefersReducedTransparency = Ident,
1470 "prefers-contrast": PrefersContrast = Ident,
1472 "forced-colors": ForcedColors = Ident,
1474 "prefers-color-scheme": PrefersColorScheme = Ident,
1476 "prefers-reduced-data": PrefersReducedData = Ident,
1478 "device-width": DeviceWidth = Length,
1480 "device-height": DeviceHeight = Length,
1482 "device-aspect-ratio": DeviceAspectRatio = Ratio,
1484
1485 "-webkit-device-pixel-ratio": WebKitDevicePixelRatio = Number,
1487 "-moz-device-pixel-ratio": MozDevicePixelRatio = Number,
1489
1490 }
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#[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 Length(Length),
1590 Number(CSSNumber),
1592 Integer(CSSInteger),
1594 Boolean(bool),
1596 Resolution(Resolution),
1598 Ratio(Ratio),
1600 #[cfg_attr(feature = "serde", serde(borrow))]
1602 Ident(Ident<'i>),
1603 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 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 if let Ok(ratio) = input.try_parse(Ratio::parse_required) {
1671 return Ok(MediaFeatureValue::Ratio(ratio));
1672 }
1673
1674 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
1723fn 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 *qualifier = if *qualifier == Some(Qualifier::Not) {
1773 None
1774 } else {
1775 Some(Qualifier::Not)
1776 };
1777 return Ok(false);
1778 }
1779
1780 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 if *qualifier == Some(Qualifier::Not) {
1831 res = Ok(false);
1832 return None;
1833 }
1834
1835 *media_type = query.media_type.clone();
1837 *qualifier = query.qualifier.clone();
1838 } else if query.media_type != *media_type || query.qualifier != *qualifier {
1839 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 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}