1use crate::ast::{
35 GeneralEnclosed, MediaCondition, MediaConditionWithoutOr, MediaFeature, MediaInParens,
36 MediaModifier, MediaQuery, MediaType, MfComparison, MfName, MfRange, MfRangeDirection, MfValue,
37};
38use crate::tokenizer::{NumericType, Token, tokenize};
39
40#[derive(Debug, Clone, PartialEq)]
44#[non_exhaustive]
45pub enum ParseError {
46 UnbalancedParens,
49 EmptyMediaQuery,
52 TrailingTokens(Token),
54 ExpectedMediaType(Option<Token>),
56 ReservedMediaType(String),
59 ExpectedMediaInParens(Option<Token>),
61 ExpectedMfName(Option<Token>),
63 ExpectedColon(Option<Token>),
65 ExpectedMfValue(Option<Token>),
67 InvalidRatio,
69 ExpectedMfComparison(Option<Token>),
71 InvalidMfRangeInterval,
76 ExpectedKeyword {
78 keyword: &'static str,
80 found: Option<Token>,
82 },
83}
84
85fn matching_close(token: &Token) -> Option<Token> {
91 match token {
92 Token::OpenParen | Token::Function(_) => Some(Token::CloseParen),
93 Token::OpenSquare => Some(Token::CloseSquare),
94 Token::OpenCurly => Some(Token::CloseCurly),
95 _ => None,
96 }
97}
98
99fn is_ident_ci(token: Option<&Token>, keyword: &str) -> bool {
100 matches!(token, Some(Token::Ident(s)) if s.eq_ignore_ascii_case(keyword))
101}
102
103type PositionedToken = (Token, bool);
112
113struct Parser<'a> {
119 tokens: &'a [PositionedToken],
120 pos: usize,
121}
122
123impl<'a> Parser<'a> {
124 fn new(tokens: &'a [PositionedToken]) -> Self {
125 Self { tokens, pos: 0 }
126 }
127
128 fn peek(&self) -> Option<&Token> {
129 self.tokens.get(self.pos).map(|(token, _)| token)
130 }
131
132 fn peek_at(&self, offset: usize) -> Option<&Token> {
133 self.tokens.get(self.pos + offset).map(|(token, _)| token)
134 }
135
136 fn advance(&mut self) -> Option<Token> {
137 let token = self.peek().cloned();
138 if token.is_some() {
139 self.pos += 1;
140 }
141 token
142 }
143
144 fn is_at_end(&self) -> bool {
145 self.pos == self.tokens.len()
146 }
147
148 fn current_preceded_by_whitespace(&self) -> bool {
155 self.tokens
156 .get(self.pos)
157 .map(|(_, preceded_by_ws)| *preceded_by_ws)
158 .unwrap_or(false)
159 }
160
161 fn consume_block(&mut self) -> Result<Vec<PositionedToken>, ParseError> {
169 let opener = self.tokens[self.pos].0.clone();
170 let mut closers = vec![matching_close(&opener).expect("caller checked an opener")];
171 self.pos += 1;
172 let mut inner = Vec::new();
173 loop {
174 match self.tokens.get(self.pos).cloned() {
175 None => return Err(ParseError::UnbalancedParens),
176 Some((token, preceded_by_ws)) => {
177 let closed_outermost = track_bracket_depth(&token, &mut closers);
178 self.pos += 1;
179 if closed_outermost {
180 return Ok(inner);
181 }
182 inner.push((token, preceded_by_ws));
183 }
184 }
185 }
186 }
187}
188
189fn track_bracket_depth(token: &Token, closers: &mut Vec<Token>) -> bool {
201 if Some(token) == closers.last() {
202 closers.pop();
203 closers.is_empty()
204 } else {
205 if let Some(closer) = matching_close(token) {
206 closers.push(closer);
207 }
208 false
209 }
210}
211
212fn parse_fully<T>(
216 tokens: &[PositionedToken],
217 production: impl Fn(&mut Parser) -> Result<T, ParseError>,
218) -> Result<T, ParseError> {
219 let mut parser = Parser::new(tokens);
220 let result = production(&mut parser)?;
221 if parser.is_at_end() {
222 Ok(result)
223 } else {
224 Err(ParseError::TrailingTokens(
225 parser.peek().expect("not at end").clone(),
226 ))
227 }
228}
229
230fn expect_end(parser: &Parser) -> Result<(), ParseError> {
231 match parser.peek() {
232 None => Ok(()),
233 Some(token) => Err(ParseError::TrailingTokens(token.clone())),
234 }
235}
236
237fn consume_keyword(parser: &mut Parser, keyword: &'static str) -> Result<(), ParseError> {
242 match parser.advance() {
243 Some(Token::Ident(s)) if s.eq_ignore_ascii_case(keyword) => Ok(()),
244 found => Err(ParseError::ExpectedKeyword { keyword, found }),
245 }
246}
247
248fn media_not(parser: &mut Parser) -> Result<MediaInParens, ParseError> {
251 consume_keyword(parser, "not")?;
252 media_in_parens(parser)
253}
254
255fn media_and(parser: &mut Parser) -> Result<MediaInParens, ParseError> {
257 consume_keyword(parser, "and")?;
258 media_in_parens(parser)
259}
260
261fn media_or(parser: &mut Parser) -> Result<MediaInParens, ParseError> {
263 consume_keyword(parser, "or")?;
264 media_in_parens(parser)
265}
266
267fn in_parens_chain(
275 parser: &mut Parser,
276 first: MediaInParens,
277 keyword: &'static str,
278 next: impl Fn(&mut Parser) -> Result<MediaInParens, ParseError>,
279) -> Result<Vec<MediaInParens>, ParseError> {
280 let mut items = vec![first];
281 while is_ident_ci(parser.peek(), keyword) {
282 items.push(next(parser)?);
283 }
284 Ok(items)
285}
286
287fn media_not_or_first_in_parens(parser: &mut Parser) -> Result<(bool, MediaInParens), ParseError> {
293 if is_ident_ci(parser.peek(), "not") {
294 return Ok((true, media_not(parser)?));
295 }
296 Ok((false, media_in_parens(parser)?))
297}
298
299fn media_condition(parser: &mut Parser) -> Result<MediaCondition, ParseError> {
301 let (is_not, first) = media_not_or_first_in_parens(parser)?;
302 if is_not {
303 return Ok(MediaCondition::Not(first));
304 }
305 if is_ident_ci(parser.peek(), "or") {
306 return Ok(MediaCondition::Or(in_parens_chain(
307 parser, first, "or", media_or,
308 )?));
309 }
310 Ok(MediaCondition::And(in_parens_chain(
311 parser, first, "and", media_and,
312 )?))
313}
314
315fn media_condition_without_or(parser: &mut Parser) -> Result<MediaConditionWithoutOr, ParseError> {
317 let (is_not, first) = media_not_or_first_in_parens(parser)?;
318 if is_not {
319 return Ok(MediaConditionWithoutOr::Not(first));
320 }
321 Ok(MediaConditionWithoutOr::And(in_parens_chain(
322 parser, first, "and", media_and,
323 )?))
324}
325
326fn media_in_parens(parser: &mut Parser) -> Result<MediaInParens, ParseError> {
328 match parser.peek() {
329 Some(Token::OpenParen) => {
330 let inner = parser.consume_block()?;
331 if let Ok(condition) = parse_fully(&inner, media_condition) {
332 return Ok(MediaInParens::Condition(Box::new(condition)));
333 }
334 if let Ok(feature) = parse_fully(&inner, media_feature) {
335 return Ok(MediaInParens::Feature(feature));
336 }
337 Ok(MediaInParens::GeneralEnclosed(GeneralEnclosed {
338 tokens: inner.into_iter().map(|(token, _)| token).collect(),
339 }))
340 }
341 Some(Token::Function(_)) => {
342 let function_token = parser.tokens[parser.pos].0.clone();
344 let inner = parser.consume_block()?;
345 let mut tokens = vec![function_token];
346 tokens.extend(inner.into_iter().map(|(token, _)| token));
347 Ok(MediaInParens::GeneralEnclosed(GeneralEnclosed { tokens }))
348 }
349 other => Err(ParseError::ExpectedMediaInParens(other.cloned())),
350 }
351}
352
353fn mf_name(parser: &mut Parser) -> Result<MfName, ParseError> {
355 match parser.advance() {
356 Some(Token::Ident(name)) => Ok(MfName(name)),
357 other => Err(ParseError::ExpectedMfName(other)),
358 }
359}
360
361fn mf_value(parser: &mut Parser) -> Result<MfValue, ParseError> {
363 match parser.advance() {
364 Some(Token::Number { value, type_flag }) => {
365 if matches!(parser.peek(), Some(Token::Delim('/'))) {
366 parser.advance();
367 match parser.advance() {
368 Some(Token::Number {
369 value: denominator,
370 type_flag: NumericType::Integer,
371 }) if type_flag == NumericType::Integer
372 && value >= 0.0
373 && denominator >= 0.0
374 && value <= u32::MAX as f64
375 && denominator <= u32::MAX as f64 =>
376 {
377 Ok(MfValue::Ratio {
378 numerator: value as u32,
379 denominator: denominator as u32,
380 })
381 }
382 _ => Err(ParseError::InvalidRatio),
383 }
384 } else {
385 Ok(MfValue::Number(value))
386 }
387 }
388 Some(Token::Dimension { value, unit, .. }) => Ok(MfValue::Dimension { value, unit }),
389 Some(Token::Ident(name)) => Ok(MfValue::Ident(name)),
390 other => Err(ParseError::ExpectedMfValue(other)),
391 }
392}
393
394fn starts_mf_comparison(token: Option<&Token>) -> bool {
397 matches!(token, Some(Token::Delim('<' | '>' | '=')))
398}
399
400fn mf_comparison(parser: &mut Parser) -> Result<MfComparison, ParseError> {
411 match parser.advance() {
412 Some(Token::Delim('<')) => {
413 if parser.peek() == Some(&Token::Delim('=')) && !parser.current_preceded_by_whitespace()
414 {
415 parser.advance();
416 Ok(MfComparison::Le)
417 } else {
418 Ok(MfComparison::Lt)
419 }
420 }
421 Some(Token::Delim('>')) => {
422 if parser.peek() == Some(&Token::Delim('=')) && !parser.current_preceded_by_whitespace()
423 {
424 parser.advance();
425 Ok(MfComparison::Ge)
426 } else {
427 Ok(MfComparison::Gt)
428 }
429 }
430 Some(Token::Delim('=')) => Ok(MfComparison::Eq),
431 other => Err(ParseError::ExpectedMfComparison(other)),
432 }
433}
434
435fn media_feature(parser: &mut Parser) -> Result<MediaFeature, ParseError> {
451 if matches!(parser.peek(), Some(Token::Ident(_))) {
452 if matches!(parser.peek_at(1), Some(Token::Colon)) {
453 let name = mf_name(parser)?;
454 parser.advance();
455 return Ok(MediaFeature::Plain {
456 name,
457 value: mf_value(parser)?,
458 });
459 }
460 if parser.peek_at(1).is_none() {
461 return Ok(MediaFeature::Boolean(mf_name(parser)?));
462 }
463 if starts_mf_comparison(parser.peek_at(1)) {
464 let name = mf_name(parser)?;
465 let operator = mf_comparison(parser)?;
466 let value = mf_value(parser)?;
467 return Ok(MediaFeature::Range(MfRange::NameFirst {
468 name,
469 operator,
470 value,
471 }));
472 }
473 }
474
475 let value = mf_value(parser)?;
476 let operator = mf_comparison(parser)?;
477 let name = mf_name(parser)?;
478 if parser.is_at_end() {
479 return Ok(MediaFeature::Range(MfRange::ValueFirst {
480 value,
481 operator,
482 name,
483 }));
484 }
485
486 let (direction, lower_inclusive) = match operator {
487 MfComparison::Lt => (MfRangeDirection::Ascending, false),
488 MfComparison::Le => (MfRangeDirection::Ascending, true),
489 MfComparison::Gt => (MfRangeDirection::Descending, false),
490 MfComparison::Ge => (MfRangeDirection::Descending, true),
491 MfComparison::Eq => return Err(ParseError::InvalidMfRangeInterval),
492 };
493 let upper_inclusive = match (direction, mf_comparison(parser)?) {
494 (MfRangeDirection::Ascending, MfComparison::Lt) => false,
495 (MfRangeDirection::Ascending, MfComparison::Le) => true,
496 (MfRangeDirection::Descending, MfComparison::Gt) => false,
497 (MfRangeDirection::Descending, MfComparison::Ge) => true,
498 _ => return Err(ParseError::InvalidMfRangeInterval),
499 };
500 let upper = mf_value(parser)?;
501 Ok(MediaFeature::Range(MfRange::Interval {
502 lower: value,
503 lower_inclusive,
504 name,
505 upper_inclusive,
506 upper,
507 direction,
508 }))
509}
510
511fn media_type(parser: &mut Parser) -> Result<MediaType, ParseError> {
514 match parser.advance() {
515 Some(Token::Ident(name)) => {
516 let lower = name.to_ascii_lowercase();
517 if matches!(lower.as_str(), "not" | "and" | "or" | "only" | "layer") {
518 Err(ParseError::ReservedMediaType(name))
519 } else {
520 Ok(MediaType(name))
521 }
522 }
523 other => Err(ParseError::ExpectedMediaType(other)),
524 }
525}
526
527fn media_query_type_branch(parser: &mut Parser) -> Result<MediaQuery, ParseError> {
530 let modifier = match parser.peek() {
531 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("not") => {
532 parser.advance();
533 Some(MediaModifier::Not)
534 }
535 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("only") => {
536 parser.advance();
537 Some(MediaModifier::Only)
538 }
539 _ => None,
540 };
541 let media_type = media_type(parser)?;
542 let condition = if is_ident_ci(parser.peek(), "and") {
543 parser.advance();
544 Some(media_condition_without_or(parser)?)
545 } else {
546 None
547 };
548 Ok(MediaQuery::TypeQuery {
549 modifier,
550 media_type,
551 condition,
552 })
553}
554
555fn media_query(parser: &mut Parser) -> Result<MediaQuery, ParseError> {
565 let starts_condition = matches!(
566 parser.peek(),
567 Some(Token::OpenParen) | Some(Token::Function(_))
568 ) || (is_ident_ci(parser.peek(), "not")
569 && matches!(
570 parser.peek_at(1),
571 Some(Token::OpenParen) | Some(Token::Function(_))
572 ));
573 if starts_condition {
574 Ok(MediaQuery::Condition(media_condition(parser)?))
575 } else {
576 media_query_type_branch(parser)
577 }
578}
579
580fn prepare_tokens(input: &str) -> Vec<PositionedToken> {
585 let mut result = Vec::new();
586 let mut preceded_by_whitespace = false;
587 for token in tokenize(input) {
588 match token {
589 Token::Whitespace => preceded_by_whitespace = true,
590 Token::Eof => {}
591 other => {
592 result.push((other, preceded_by_whitespace));
593 preceded_by_whitespace = false;
594 }
595 }
596 }
597 result
598}
599
600fn split_top_level_commas(tokens: &[PositionedToken]) -> Vec<Vec<PositionedToken>> {
604 let mut segments = Vec::new();
605 let mut current = Vec::new();
606 let mut closers: Vec<Token> = Vec::new();
607 for (token, preceded_by_whitespace) in tokens {
608 if closers.is_empty() && *token == Token::Comma {
609 segments.push(std::mem::take(&mut current));
610 continue;
611 }
612 track_bracket_depth(token, &mut closers);
613 current.push((token.clone(), *preceded_by_whitespace));
614 }
615 segments.push(current);
616 segments
617}
618
619fn parse_media_query_tokens(tokens: &[PositionedToken]) -> Result<MediaQuery, ParseError> {
620 if tokens.is_empty() {
621 return Err(ParseError::EmptyMediaQuery);
622 }
623 let mut parser = Parser::new(tokens);
624 let query = media_query(&mut parser)?;
625 expect_end(&parser)?;
626 Ok(query)
627}
628
629pub fn parse_media_query(input: &str) -> Result<MediaQuery, ParseError> {
631 parse_media_query_tokens(&prepare_tokens(input))
632}
633
634pub fn parse_media_query_list(input: &str) -> Vec<Result<MediaQuery, ParseError>> {
640 let tokens = prepare_tokens(input);
641 split_top_level_commas(&tokens)
642 .into_iter()
643 .map(|segment| parse_media_query_tokens(&segment))
644 .collect()
645}
646
647#[cfg(test)]
648mod tests {
649 use super::*;
650 use crate::ast::MediaCondition::*;
651 use crate::ast::MediaInParens as InParens;
652
653 fn feature_boolean(name: &str) -> InParens {
654 InParens::Feature(MediaFeature::Boolean(MfName(name.into())))
655 }
656
657 fn feature_plain(name: &str, value: MfValue) -> InParens {
658 InParens::Feature(MediaFeature::Plain {
659 name: MfName(name.into()),
660 value,
661 })
662 }
663
664 fn feature_range(range: MfRange) -> InParens {
665 InParens::Feature(MediaFeature::Range(range))
666 }
667
668 fn dim(value: f64, unit: &str) -> MfValue {
669 MfValue::Dimension {
670 value,
671 unit: unit.into(),
672 }
673 }
674
675 #[test]
676 fn media_query_list_multiple_entries() {
677 let results = parse_media_query_list("screen, print");
678 assert_eq!(results.len(), 2);
679 assert_eq!(
680 results[0],
681 Ok(MediaQuery::TypeQuery {
682 modifier: None,
683 media_type: MediaType("screen".into()),
684 condition: None,
685 })
686 );
687 assert_eq!(
688 results[1],
689 Ok(MediaQuery::TypeQuery {
690 modifier: None,
691 media_type: MediaType("print".into()),
692 condition: None,
693 })
694 );
695 }
696
697 #[test]
698 fn media_query_type_bare() {
699 assert_eq!(
700 parse_media_query("screen"),
701 Ok(MediaQuery::TypeQuery {
702 modifier: None,
703 media_type: MediaType("screen".into()),
704 condition: None,
705 })
706 );
707 }
708
709 #[test]
710 fn media_query_type_with_not_modifier() {
711 assert_eq!(
712 parse_media_query("not screen"),
713 Ok(MediaQuery::TypeQuery {
714 modifier: Some(MediaModifier::Not),
715 media_type: MediaType("screen".into()),
716 condition: None,
717 })
718 );
719 }
720
721 #[test]
722 fn media_query_type_with_only_modifier() {
723 assert_eq!(
724 parse_media_query("only screen"),
725 Ok(MediaQuery::TypeQuery {
726 modifier: Some(MediaModifier::Only),
727 media_type: MediaType("screen".into()),
728 condition: None,
729 })
730 );
731 }
732
733 #[test]
734 fn media_query_type_with_and_condition() {
735 assert_eq!(
736 parse_media_query("screen and (color)"),
737 Ok(MediaQuery::TypeQuery {
738 modifier: None,
739 media_type: MediaType("screen".into()),
740 condition: Some(MediaConditionWithoutOr::And(vec![feature_boolean("color")])),
741 })
742 );
743 }
744
745 #[test]
746 fn media_query_type_with_and_chain_condition() {
747 assert_eq!(
748 parse_media_query("screen and (color) and (monochrome)"),
749 Ok(MediaQuery::TypeQuery {
750 modifier: None,
751 media_type: MediaType("screen".into()),
752 condition: Some(MediaConditionWithoutOr::And(vec![
753 feature_boolean("color"),
754 feature_boolean("monochrome"),
755 ])),
756 })
757 );
758 }
759
760 #[test]
761 fn media_condition_without_or_rejects_or() {
762 assert_eq!(
765 parse_media_query("screen and (color) or (monochrome)"),
766 Err(ParseError::TrailingTokens(Token::Ident("or".into())))
767 );
768 }
769
770 #[test]
771 fn media_query_condition_shorthand() {
772 assert_eq!(
773 parse_media_query("(color)"),
774 Ok(MediaQuery::Condition(And(vec![feature_boolean("color")])))
775 );
776 }
777
778 #[test]
779 fn media_condition_not() {
780 assert_eq!(
781 parse_media_query("not (color)"),
782 Ok(MediaQuery::Condition(Not(feature_boolean("color"))))
783 );
784 }
785
786 #[test]
787 fn media_condition_and_chain() {
788 assert_eq!(
789 parse_media_query("(color) and (monochrome)"),
790 Ok(MediaQuery::Condition(And(vec![
791 feature_boolean("color"),
792 feature_boolean("monochrome"),
793 ])))
794 );
795 }
796
797 #[test]
798 fn media_condition_or_chain() {
799 assert_eq!(
800 parse_media_query("(color) or (monochrome)"),
801 Ok(MediaQuery::Condition(Or(vec![
802 feature_boolean("color"),
803 feature_boolean("monochrome"),
804 ])))
805 );
806 }
807
808 #[test]
809 fn media_in_parens_condition() {
810 assert_eq!(
811 parse_media_query("((color) and (monochrome))"),
812 Ok(MediaQuery::Condition(And(vec![InParens::Condition(
813 Box::new(And(vec![
814 feature_boolean("color"),
815 feature_boolean("monochrome"),
816 ]))
817 )])))
818 );
819 }
820
821 #[test]
822 fn media_in_parens_feature() {
823 assert_eq!(
824 parse_media_query("(width: 400px)"),
825 Ok(MediaQuery::Condition(And(vec![feature_plain(
826 "width",
827 MfValue::Dimension {
828 value: 400.0,
829 unit: "px".into()
830 }
831 )])))
832 );
833 }
834
835 #[test]
836 fn media_in_parens_general_enclosed() {
837 let Ok(MediaQuery::Condition(And(items))) = parse_media_query("(3 + 5)") else {
838 panic!("expected a bare condition with one <media-in-parens>");
839 };
840 assert_eq!(items.len(), 1);
841 assert!(matches!(items[0], InParens::GeneralEnclosed(_)));
842 }
843
844 #[test]
845 fn general_enclosed_function_token() {
846 let Ok(MediaQuery::Condition(And(items))) = parse_media_query("foo(bar)") else {
847 panic!("expected a bare condition with one <media-in-parens>");
848 };
849 assert_eq!(
850 items,
851 vec![InParens::GeneralEnclosed(GeneralEnclosed {
852 tokens: vec![Token::Function("foo".into()), Token::Ident("bar".into())],
853 })]
854 );
855 }
856
857 #[test]
858 fn mf_boolean() {
859 assert_eq!(
860 parse_media_query("(color)"),
861 Ok(MediaQuery::Condition(And(vec![feature_boolean("color")])))
862 );
863 }
864
865 #[test]
866 fn mf_plain_number() {
867 assert_eq!(
868 parse_media_query("(color-index: 2)"),
869 Ok(MediaQuery::Condition(And(vec![feature_plain(
870 "color-index",
871 MfValue::Number(2.0)
872 )])))
873 );
874 }
875
876 #[test]
877 fn mf_plain_dimension() {
878 assert_eq!(
879 parse_media_query("(width: 400px)"),
880 Ok(MediaQuery::Condition(And(vec![feature_plain(
881 "width",
882 MfValue::Dimension {
883 value: 400.0,
884 unit: "px".into()
885 }
886 )])))
887 );
888 }
889
890 #[test]
891 fn mf_plain_ident() {
892 assert_eq!(
893 parse_media_query("(orientation: landscape)"),
894 Ok(MediaQuery::Condition(And(vec![feature_plain(
895 "orientation",
896 MfValue::Ident("landscape".into())
897 )])))
898 );
899 }
900
901 #[test]
902 fn mf_plain_ratio() {
903 assert_eq!(
904 parse_media_query("(aspect-ratio: 16/9)"),
905 Ok(MediaQuery::Condition(And(vec![feature_plain(
906 "aspect-ratio",
907 MfValue::Ratio {
908 numerator: 16,
909 denominator: 9
910 }
911 )])))
912 );
913 }
914
915 #[test]
916 fn mf_plain_ratio_out_of_u32_range_is_invalid() {
917 let tokens = prepare_tokens("aspect-ratio: 99999999999999/1");
918 assert_eq!(
919 parse_fully(&tokens, media_feature),
920 Err(ParseError::InvalidRatio)
921 );
922 }
923
924 #[test]
925 fn media_type_collision_rule() {
926 assert_eq!(
927 parse_media_query("layer"),
928 Err(ParseError::ReservedMediaType("layer".into()))
929 );
930 assert_eq!(
931 parse_media_query("and"),
932 Err(ParseError::ReservedMediaType("and".into()))
933 );
934 }
935
936 #[test]
937 fn parse_error_on_unbalanced_parens() {
938 assert_eq!(
939 parse_media_query("(color"),
940 Err(ParseError::UnbalancedParens)
941 );
942 }
943
944 #[test]
945 fn parse_media_query_list_surfaces_per_entry_errors() {
946 let results = parse_media_query_list("screen, layer, (color");
947 assert_eq!(results.len(), 3);
948 assert!(results[0].is_ok());
949 assert_eq!(
950 results[1],
951 Err(ParseError::ReservedMediaType("layer".into()))
952 );
953 assert_eq!(results[2], Err(ParseError::UnbalancedParens));
954 }
955
956 #[test]
959 fn mf_comparison_lt() {
960 assert_eq!(
961 parse_fully(&prepare_tokens("<"), mf_comparison),
962 Ok(MfComparison::Lt)
963 );
964 }
965
966 #[test]
967 fn mf_comparison_gt() {
968 assert_eq!(
969 parse_fully(&prepare_tokens(">"), mf_comparison),
970 Ok(MfComparison::Gt)
971 );
972 }
973
974 #[test]
975 fn mf_comparison_eq() {
976 assert_eq!(
977 parse_fully(&prepare_tokens("="), mf_comparison),
978 Ok(MfComparison::Eq)
979 );
980 }
981
982 #[test]
983 fn mf_comparison_le_no_whitespace() {
984 assert_eq!(
985 parse_fully(&prepare_tokens("<="), mf_comparison),
986 Ok(MfComparison::Le)
987 );
988 }
989
990 #[test]
991 fn mf_comparison_ge_no_whitespace() {
992 assert_eq!(
993 parse_fully(&prepare_tokens(">="), mf_comparison),
994 Ok(MfComparison::Ge)
995 );
996 }
997
998 #[test]
999 fn mf_comparison_lt_then_eq_with_whitespace_is_not_le() {
1000 assert_eq!(
1008 parse_fully(&prepare_tokens("< ="), mf_comparison),
1009 Err(ParseError::TrailingTokens(Token::Delim('=')))
1010 );
1011 }
1012
1013 #[test]
1014 fn mf_comparison_gt_then_eq_with_whitespace_is_not_ge() {
1015 assert_eq!(
1016 parse_fully(&prepare_tokens("> ="), mf_comparison),
1017 Err(ParseError::TrailingTokens(Token::Delim('=')))
1018 );
1019 }
1020
1021 #[test]
1024 fn mf_range_name_first_lt() {
1025 assert_eq!(
1026 parse_media_query("(width < 400px)"),
1027 Ok(MediaQuery::Condition(And(vec![feature_range(
1028 MfRange::NameFirst {
1029 name: MfName("width".into()),
1030 operator: MfComparison::Lt,
1031 value: dim(400.0, "px"),
1032 }
1033 )])))
1034 );
1035 }
1036
1037 #[test]
1038 fn mf_range_name_first_le() {
1039 assert_eq!(
1040 parse_media_query("(width <= 400px)"),
1041 Ok(MediaQuery::Condition(And(vec![feature_range(
1042 MfRange::NameFirst {
1043 name: MfName("width".into()),
1044 operator: MfComparison::Le,
1045 value: dim(400.0, "px"),
1046 }
1047 )])))
1048 );
1049 }
1050
1051 #[test]
1052 fn mf_range_name_first_gt() {
1053 assert_eq!(
1054 parse_media_query("(width > 400px)"),
1055 Ok(MediaQuery::Condition(And(vec![feature_range(
1056 MfRange::NameFirst {
1057 name: MfName("width".into()),
1058 operator: MfComparison::Gt,
1059 value: dim(400.0, "px"),
1060 }
1061 )])))
1062 );
1063 }
1064
1065 #[test]
1066 fn mf_range_name_first_ge() {
1067 assert_eq!(
1068 parse_media_query("(width >= 400px)"),
1069 Ok(MediaQuery::Condition(And(vec![feature_range(
1070 MfRange::NameFirst {
1071 name: MfName("width".into()),
1072 operator: MfComparison::Ge,
1073 value: dim(400.0, "px"),
1074 }
1075 )])))
1076 );
1077 }
1078
1079 #[test]
1080 fn mf_range_name_first_eq_with_ident_value() {
1081 assert_eq!(
1082 parse_media_query("(orientation = landscape)"),
1083 Ok(MediaQuery::Condition(And(vec![feature_range(
1084 MfRange::NameFirst {
1085 name: MfName("orientation".into()),
1086 operator: MfComparison::Eq,
1087 value: MfValue::Ident("landscape".into()),
1088 }
1089 )])))
1090 );
1091 }
1092
1093 #[test]
1094 fn mf_range_name_first_number_value() {
1095 assert_eq!(
1096 parse_media_query("(color-index >= 2)"),
1097 Ok(MediaQuery::Condition(And(vec![feature_range(
1098 MfRange::NameFirst {
1099 name: MfName("color-index".into()),
1100 operator: MfComparison::Ge,
1101 value: MfValue::Number(2.0),
1102 }
1103 )])))
1104 );
1105 }
1106
1107 #[test]
1108 fn mf_range_name_first_ratio_value() {
1109 assert_eq!(
1110 parse_media_query("(aspect-ratio >= 16/9)"),
1111 Ok(MediaQuery::Condition(And(vec![feature_range(
1112 MfRange::NameFirst {
1113 name: MfName("aspect-ratio".into()),
1114 operator: MfComparison::Ge,
1115 value: MfValue::Ratio {
1116 numerator: 16,
1117 denominator: 9,
1118 },
1119 }
1120 )])))
1121 );
1122 }
1123
1124 #[test]
1125 fn mf_range_value_first_dimension() {
1126 assert_eq!(
1127 parse_media_query("(400px <= width)"),
1128 Ok(MediaQuery::Condition(And(vec![feature_range(
1129 MfRange::ValueFirst {
1130 value: dim(400.0, "px"),
1131 operator: MfComparison::Le,
1132 name: MfName("width".into()),
1133 }
1134 )])))
1135 );
1136 }
1137
1138 #[test]
1139 fn mf_range_value_first_number() {
1140 assert_eq!(
1141 parse_media_query("(2 < color-index)"),
1142 Ok(MediaQuery::Condition(And(vec![feature_range(
1143 MfRange::ValueFirst {
1144 value: MfValue::Number(2.0),
1145 operator: MfComparison::Lt,
1146 name: MfName("color-index".into()),
1147 }
1148 )])))
1149 );
1150 }
1151
1152 #[test]
1153 fn mf_range_value_first_ratio() {
1154 assert_eq!(
1155 parse_media_query("(16/9 <= aspect-ratio)"),
1156 Ok(MediaQuery::Condition(And(vec![feature_range(
1157 MfRange::ValueFirst {
1158 value: MfValue::Ratio {
1159 numerator: 16,
1160 denominator: 9,
1161 },
1162 operator: MfComparison::Le,
1163 name: MfName("aspect-ratio".into()),
1164 }
1165 )])))
1166 );
1167 }
1168
1169 #[test]
1170 fn mf_range_interval_ascending_inclusive() {
1171 assert_eq!(
1172 parse_media_query("(400px <= width <= 700px)"),
1173 Ok(MediaQuery::Condition(And(vec![feature_range(
1174 MfRange::Interval {
1175 lower: dim(400.0, "px"),
1176 lower_inclusive: true,
1177 name: MfName("width".into()),
1178 upper_inclusive: true,
1179 upper: dim(700.0, "px"),
1180 direction: MfRangeDirection::Ascending,
1181 }
1182 )])))
1183 );
1184 }
1185
1186 #[test]
1187 fn mf_range_interval_ascending_exclusive() {
1188 assert_eq!(
1189 parse_media_query("(400px < width < 700px)"),
1190 Ok(MediaQuery::Condition(And(vec![feature_range(
1191 MfRange::Interval {
1192 lower: dim(400.0, "px"),
1193 lower_inclusive: false,
1194 name: MfName("width".into()),
1195 upper_inclusive: false,
1196 upper: dim(700.0, "px"),
1197 direction: MfRangeDirection::Ascending,
1198 }
1199 )])))
1200 );
1201 }
1202
1203 #[test]
1204 fn mf_range_interval_descending_inclusive() {
1205 assert_eq!(
1206 parse_media_query("(700px >= width >= 400px)"),
1207 Ok(MediaQuery::Condition(And(vec![feature_range(
1208 MfRange::Interval {
1209 lower: dim(700.0, "px"),
1210 lower_inclusive: true,
1211 name: MfName("width".into()),
1212 upper_inclusive: true,
1213 upper: dim(400.0, "px"),
1214 direction: MfRangeDirection::Descending,
1215 }
1216 )])))
1217 );
1218 }
1219
1220 #[test]
1221 fn mf_range_interval_descending_exclusive() {
1222 assert_eq!(
1223 parse_media_query("(700px > width > 400px)"),
1224 Ok(MediaQuery::Condition(And(vec![feature_range(
1225 MfRange::Interval {
1226 lower: dim(700.0, "px"),
1227 lower_inclusive: false,
1228 name: MfName("width".into()),
1229 upper_inclusive: false,
1230 upper: dim(400.0, "px"),
1231 direction: MfRangeDirection::Descending,
1232 }
1233 )])))
1234 );
1235 }
1236
1237 #[test]
1238 fn mf_range_interval_rejects_mixed_family() {
1239 let tokens = prepare_tokens("400px <= width > 700px");
1240 assert_eq!(
1241 parse_fully(&tokens, media_feature),
1242 Err(ParseError::InvalidMfRangeInterval)
1243 );
1244 }
1245
1246 #[test]
1247 fn mf_range_interval_rejects_eq_as_first_operator() {
1248 let tokens = prepare_tokens("400px = width < 700px");
1249 assert_eq!(
1250 parse_fully(&tokens, media_feature),
1251 Err(ParseError::InvalidMfRangeInterval)
1252 );
1253 }
1254
1255 #[test]
1256 fn mf_range_incomplete_name_first_is_invalid() {
1257 let tokens = prepare_tokens("width <");
1258 assert_eq!(
1259 parse_fully(&tokens, media_feature),
1260 Err(ParseError::ExpectedMfValue(None))
1261 );
1262 }
1263
1264 #[test]
1265 fn mf_plain_and_boolean_regression_after_range_support() {
1266 assert_eq!(
1271 parse_media_query("(orientation: landscape)"),
1272 Ok(MediaQuery::Condition(And(vec![feature_plain(
1273 "orientation",
1274 MfValue::Ident("landscape".into())
1275 )])))
1276 );
1277 assert_eq!(
1278 parse_media_query("(color)"),
1279 Ok(MediaQuery::Condition(And(vec![feature_boolean("color")])))
1280 );
1281 }
1282}