we (web engine): Experimental web browser project to understand the limits of Claude
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 850 lines 26 kB view raw
1//! CSS Media Queries Level 4 (core subset). 2//! 3//! Parses and evaluates media queries such as 4//! `screen and (max-width: 600px)` or `(prefers-color-scheme: dark)`. 5 6use crate::tokenizer::Token; 7 8// --------------------------------------------------------------------------- 9// Media context (evaluation environment) 10// --------------------------------------------------------------------------- 11 12/// The environment against which media queries are evaluated. 13#[derive(Debug, Clone)] 14pub struct MediaContext { 15 pub viewport_width: f32, 16 pub viewport_height: f32, 17 pub color_scheme: ColorScheme, 18 pub reduced_motion: ReducedMotion, 19} 20 21impl MediaContext { 22 /// Create a context from viewport dimensions with sensible defaults. 23 pub fn from_viewport(width: f32, height: f32) -> Self { 24 Self { 25 viewport_width: width, 26 viewport_height: height, 27 color_scheme: ColorScheme::Light, 28 reduced_motion: ReducedMotion::NoPreference, 29 } 30 } 31 32 /// Viewport as `(width, height)` tuple for existing code. 33 pub fn viewport(&self) -> (f32, f32) { 34 (self.viewport_width, self.viewport_height) 35 } 36} 37 38impl Default for MediaContext { 39 fn default() -> Self { 40 Self::from_viewport(0.0, 0.0) 41 } 42} 43 44/// User's preferred color scheme. 45#[derive(Debug, Clone, Copy, PartialEq, Eq)] 46pub enum ColorScheme { 47 Light, 48 Dark, 49} 50 51/// User's reduced-motion preference. 52#[derive(Debug, Clone, Copy, PartialEq, Eq)] 53pub enum ReducedMotion { 54 NoPreference, 55 Reduce, 56} 57 58// --------------------------------------------------------------------------- 59// Media query AST 60// --------------------------------------------------------------------------- 61 62/// Comma-separated list of media queries. Evaluates `true` if **any** matches. 63#[derive(Debug, Clone, PartialEq)] 64pub struct MediaQueryList { 65 pub queries: Vec<MediaQuery>, 66} 67 68/// A single media query: `[not|only] [type] [and (feature)]*` 69#[derive(Debug, Clone, PartialEq)] 70pub struct MediaQuery { 71 pub modifier: Modifier, 72 pub media_type: MediaType, 73 pub features: Vec<MediaFeature>, 74} 75 76/// Optional modifier on a media query. 77#[derive(Debug, Clone, Copy, PartialEq, Eq)] 78pub enum Modifier { 79 None, 80 Not, 81 Only, 82} 83 84/// Media type. 85#[derive(Debug, Clone, Copy, PartialEq, Eq)] 86pub enum MediaType { 87 All, 88 Screen, 89 Print, 90} 91 92/// A single media feature condition inside parentheses. 93#[derive(Debug, Clone, PartialEq)] 94pub enum MediaFeature { 95 /// Width comparison (covers `width`, `min-width`, `max-width`, range syntax). 96 Width(Comparison, f32), 97 /// Height comparison (covers `height`, `min-height`, `max-height`, range syntax). 98 Height(Comparison, f32), 99 /// `prefers-color-scheme: light | dark` 100 PrefersColorScheme(ColorScheme), 101 /// `prefers-reduced-motion: reduce | no-preference` 102 PrefersReducedMotion(ReducedMotion), 103} 104 105/// Comparison operator for range features. 106#[derive(Debug, Clone, Copy, PartialEq, Eq)] 107pub enum Comparison { 108 Eq, 109 Gt, 110 Gte, 111 Lt, 112 Lte, 113} 114 115// --------------------------------------------------------------------------- 116// Evaluation 117// --------------------------------------------------------------------------- 118 119impl MediaQueryList { 120 /// An empty list matches everything (per spec). 121 pub fn evaluate(&self, ctx: &MediaContext) -> bool { 122 if self.queries.is_empty() { 123 return true; 124 } 125 self.queries.iter().any(|q| q.evaluate(ctx)) 126 } 127} 128 129impl MediaQuery { 130 pub fn evaluate(&self, ctx: &MediaContext) -> bool { 131 let type_matches = match self.media_type { 132 MediaType::All | MediaType::Screen => true, 133 MediaType::Print => false, 134 }; 135 136 let features_match = self.features.iter().all(|f| f.evaluate(ctx)); 137 let result = type_matches && features_match; 138 139 match self.modifier { 140 Modifier::Not => !result, 141 Modifier::Only | Modifier::None => result, 142 } 143 } 144} 145 146impl MediaFeature { 147 pub fn evaluate(&self, ctx: &MediaContext) -> bool { 148 match self { 149 MediaFeature::Width(cmp, val) => compare(*cmp, ctx.viewport_width, *val), 150 MediaFeature::Height(cmp, val) => compare(*cmp, ctx.viewport_height, *val), 151 MediaFeature::PrefersColorScheme(scheme) => ctx.color_scheme == *scheme, 152 MediaFeature::PrefersReducedMotion(motion) => ctx.reduced_motion == *motion, 153 } 154 } 155} 156 157fn compare(cmp: Comparison, actual: f32, expected: f32) -> bool { 158 match cmp { 159 Comparison::Eq => (actual - expected).abs() < 0.5, 160 Comparison::Gt => actual > expected, 161 Comparison::Gte => actual >= expected, 162 Comparison::Lt => actual < expected, 163 Comparison::Lte => actual <= expected, 164 } 165} 166 167// --------------------------------------------------------------------------- 168// Parsing 169// --------------------------------------------------------------------------- 170 171/// Parse a media query list from tokens (the prelude between `@media` and `{`). 172pub fn parse_media_query_list(tokens: &[Token]) -> MediaQueryList { 173 let mut queries = Vec::new(); 174 let mut start = 0; 175 176 for i in 0..tokens.len() { 177 if matches!(tokens[i], Token::Comma) { 178 if let Some(q) = parse_single_query(&tokens[start..i]) { 179 queries.push(q); 180 } 181 start = i + 1; 182 } 183 } 184 185 // Last (or only) segment. 186 if start <= tokens.len() { 187 if let Some(q) = parse_single_query(&tokens[start..]) { 188 queries.push(q); 189 } 190 } 191 192 MediaQueryList { queries } 193} 194 195/// Parse a single media query (no commas). 196fn parse_single_query(tokens: &[Token]) -> Option<MediaQuery> { 197 let tokens: Vec<&Token> = tokens 198 .iter() 199 .filter(|t| !matches!(t, Token::Whitespace)) 200 .collect(); 201 if tokens.is_empty() { 202 return None; 203 } 204 205 let mut pos = 0; 206 207 // Optional modifier: `not` | `only` 208 let modifier = match tokens.get(pos) { 209 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("not") => { 210 pos += 1; 211 Modifier::Not 212 } 213 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("only") => { 214 pos += 1; 215 Modifier::Only 216 } 217 _ => Modifier::None, 218 }; 219 220 // Optional media type: `screen` | `print` | `all` 221 let mut media_type = MediaType::All; 222 let mut has_explicit_type = false; 223 224 if let Some(Token::Ident(s)) = tokens.get(pos) { 225 match s.to_ascii_lowercase().as_str() { 226 "screen" => { 227 media_type = MediaType::Screen; 228 has_explicit_type = true; 229 pos += 1; 230 } 231 "print" => { 232 media_type = MediaType::Print; 233 has_explicit_type = true; 234 pos += 1; 235 } 236 "all" => { 237 media_type = MediaType::All; 238 has_explicit_type = true; 239 pos += 1; 240 } 241 _ => {} 242 } 243 } 244 245 // If we have a modifier but no recognized type, and the next token is an ident 246 // (not a paren), treat it as an unknown type that never matches. 247 if modifier != Modifier::None && !has_explicit_type { 248 if let Some(Token::Ident(_)) = tokens.get(pos) { 249 // Unknown media type — skip the ident. Evaluated as never-matching 250 // (the `not` modifier will invert it). 251 pos += 1; 252 } 253 } 254 255 // Parse `and (feature)` clauses. 256 let mut features = Vec::new(); 257 loop { 258 if pos >= tokens.len() { 259 break; 260 } 261 262 // Expect `and` between type and features, or between features. 263 if let Some(Token::Ident(s)) = tokens.get(pos) { 264 if s.eq_ignore_ascii_case("and") { 265 pos += 1; 266 if pos >= tokens.len() { 267 break; 268 } 269 } 270 } 271 272 // Expect `(` ... `)` 273 if !matches!(tokens.get(pos), Some(Token::LeftParen)) { 274 break; 275 } 276 pos += 1; 277 278 // Collect tokens until matching `)`. 279 let feature_start = pos; 280 let mut depth = 1u32; 281 while pos < tokens.len() && depth > 0 { 282 match tokens[pos] { 283 Token::LeftParen => depth += 1, 284 Token::RightParen => { 285 depth -= 1; 286 if depth == 0 { 287 break; 288 } 289 } 290 _ => {} 291 } 292 pos += 1; 293 } 294 let feature_end = pos; 295 if pos < tokens.len() { 296 pos += 1; // skip `)` 297 } 298 299 let feature_tokens: Vec<&Token> = tokens[feature_start..feature_end].to_vec(); 300 let parsed = parse_media_feature(&feature_tokens); 301 features.extend(parsed); 302 } 303 304 // A modifier without an explicit type or features isn't useful, but still valid. 305 Some(MediaQuery { 306 modifier, 307 media_type, 308 features, 309 }) 310} 311 312/// Parse a single media feature from the tokens inside parentheses. 313/// Returns zero or more features (double ranges produce two). 314fn parse_media_feature(tokens: &[&Token]) -> Vec<MediaFeature> { 315 if tokens.is_empty() { 316 return Vec::new(); 317 } 318 319 // Try traditional syntax first: `name : value` 320 if let Some(colon_pos) = tokens.iter().position(|t| matches!(t, Token::Colon)) { 321 let name_tokens = &tokens[..colon_pos]; 322 let value_tokens = &tokens[colon_pos + 1..]; 323 324 if name_tokens.len() == 1 { 325 if let Token::Ident(name) = name_tokens[0] { 326 if let Some(f) = parse_named_feature(&name.to_ascii_lowercase(), value_tokens) { 327 return vec![f]; 328 } 329 } 330 } 331 return Vec::new(); 332 } 333 334 // Try range syntax. 335 parse_range_feature(tokens) 336} 337 338/// Parse a feature with traditional `name: value` syntax. 339fn parse_named_feature(name: &str, value_tokens: &[&Token]) -> Option<MediaFeature> { 340 match name { 341 "min-width" => Some(MediaFeature::Width( 342 Comparison::Gte, 343 parse_length(value_tokens)?, 344 )), 345 "max-width" => Some(MediaFeature::Width( 346 Comparison::Lte, 347 parse_length(value_tokens)?, 348 )), 349 "width" => Some(MediaFeature::Width( 350 Comparison::Eq, 351 parse_length(value_tokens)?, 352 )), 353 "min-height" => Some(MediaFeature::Height( 354 Comparison::Gte, 355 parse_length(value_tokens)?, 356 )), 357 "max-height" => Some(MediaFeature::Height( 358 Comparison::Lte, 359 parse_length(value_tokens)?, 360 )), 361 "height" => Some(MediaFeature::Height( 362 Comparison::Eq, 363 parse_length(value_tokens)?, 364 )), 365 "prefers-color-scheme" => { 366 let Token::Ident(v) = value_tokens.first()? else { 367 return None; 368 }; 369 match v.to_ascii_lowercase().as_str() { 370 "light" => Some(MediaFeature::PrefersColorScheme(ColorScheme::Light)), 371 "dark" => Some(MediaFeature::PrefersColorScheme(ColorScheme::Dark)), 372 _ => None, 373 } 374 } 375 "prefers-reduced-motion" => { 376 let Token::Ident(v) = value_tokens.first()? else { 377 return None; 378 }; 379 match v.to_ascii_lowercase().as_str() { 380 "reduce" => Some(MediaFeature::PrefersReducedMotion(ReducedMotion::Reduce)), 381 "no-preference" => Some(MediaFeature::PrefersReducedMotion( 382 ReducedMotion::NoPreference, 383 )), 384 _ => None, 385 } 386 } 387 _ => None, 388 } 389} 390 391/// Parse range syntax: `name op value`, `value op name`, or 392/// `value op name op value` (double range). 393fn parse_range_feature(tokens: &[&Token]) -> Vec<MediaFeature> { 394 // Find comparison operator positions. 395 let mut comparisons: Vec<(usize, usize, Comparison)> = Vec::new(); // (start, len, op) 396 397 let mut i = 0; 398 while i < tokens.len() { 399 if i + 1 < tokens.len() { 400 if let (Token::Delim('>'), Token::Delim('=')) = (tokens[i], tokens[i + 1]) { 401 comparisons.push((i, 2, Comparison::Gte)); 402 i += 2; 403 continue; 404 } 405 if let (Token::Delim('<'), Token::Delim('=')) = (tokens[i], tokens[i + 1]) { 406 comparisons.push((i, 2, Comparison::Lte)); 407 i += 2; 408 continue; 409 } 410 } 411 match tokens[i] { 412 Token::Delim('>') => { 413 comparisons.push((i, 1, Comparison::Gt)); 414 i += 1; 415 continue; 416 } 417 Token::Delim('<') => { 418 comparisons.push((i, 1, Comparison::Lt)); 419 i += 1; 420 continue; 421 } 422 Token::Delim('=') => { 423 comparisons.push((i, 1, Comparison::Eq)); 424 i += 1; 425 continue; 426 } 427 _ => {} 428 } 429 i += 1; 430 } 431 432 if comparisons.len() == 1 { 433 let (op_pos, op_len, cmp) = comparisons[0]; 434 let left = &tokens[..op_pos]; 435 let right = &tokens[op_pos + op_len..]; 436 437 if let Some(name) = extract_feature_name(left) { 438 // `name op value` 439 if let Some(val) = parse_length(right) { 440 if let Some(f) = make_dimension_feature(&name, cmp, val) { 441 return vec![f]; 442 } 443 } 444 } else if let Some(name) = extract_feature_name(right) { 445 // `value op name` → flip 446 if let Some(val) = parse_length(left) { 447 if let Some(f) = make_dimension_feature(&name, flip_comparison(cmp), val) { 448 return vec![f]; 449 } 450 } 451 } 452 } else if comparisons.len() == 2 { 453 // Double range: `value op1 name op2 value` 454 let (op1_pos, op1_len, cmp1) = comparisons[0]; 455 let (op2_pos, op2_len, cmp2) = comparisons[1]; 456 457 let left = &tokens[..op1_pos]; 458 let middle = &tokens[op1_pos + op1_len..op2_pos]; 459 let right = &tokens[op2_pos + op2_len..]; 460 461 if let Some(name) = extract_feature_name(middle) { 462 if let (Some(low), Some(high)) = (parse_length(left), parse_length(right)) { 463 // `low cmp1 name cmp2 high` means `name flip(cmp1) low AND name cmp2 high` 464 let f1 = make_dimension_feature(&name, flip_comparison(cmp1), low); 465 let f2 = make_dimension_feature(&name, cmp2, high); 466 if let (Some(a), Some(b)) = (f1, f2) { 467 return vec![a, b]; 468 } 469 } 470 } 471 } 472 473 Vec::new() 474} 475 476/// Extract a feature name from a single-ident token slice. 477fn extract_feature_name(tokens: &[&Token]) -> Option<String> { 478 if tokens.len() == 1 { 479 if let Token::Ident(s) = tokens[0] { 480 return Some(s.to_ascii_lowercase()); 481 } 482 } 483 None 484} 485 486/// Create a Width or Height feature from a dimension name and comparison. 487fn make_dimension_feature(name: &str, cmp: Comparison, value: f32) -> Option<MediaFeature> { 488 match name { 489 "width" => Some(MediaFeature::Width(cmp, value)), 490 "height" => Some(MediaFeature::Height(cmp, value)), 491 _ => None, 492 } 493} 494 495/// Flip a comparison for reversed range syntax: `value op name` → `name flip(op) value`. 496fn flip_comparison(cmp: Comparison) -> Comparison { 497 match cmp { 498 Comparison::Gt => Comparison::Lt, 499 Comparison::Gte => Comparison::Lte, 500 Comparison::Lt => Comparison::Gt, 501 Comparison::Lte => Comparison::Gte, 502 Comparison::Eq => Comparison::Eq, 503 } 504} 505 506/// Parse a pixel length from value tokens. Returns the value in `px`. 507fn parse_length(tokens: &[&Token]) -> Option<f32> { 508 if tokens.len() != 1 { 509 return None; 510 } 511 match tokens[0] { 512 Token::Dimension(val, _, unit) if unit.eq_ignore_ascii_case("px") => Some(*val as f32), 513 Token::Dimension(val, _, unit) if unit.eq_ignore_ascii_case("em") => { 514 Some(*val as f32 * 16.0) 515 } 516 Token::Dimension(val, _, unit) if unit.eq_ignore_ascii_case("rem") => { 517 Some(*val as f32 * 16.0) 518 } 519 Token::Number(val, _) if *val == 0.0 => Some(0.0), 520 _ => None, 521 } 522} 523 524// --------------------------------------------------------------------------- 525// Tests 526// --------------------------------------------------------------------------- 527 528#[cfg(test)] 529mod tests { 530 use super::*; 531 use crate::tokenizer::Tokenizer; 532 533 /// Helper: tokenize a media query prelude (no @media keyword or braces). 534 fn parse_mq(input: &str) -> MediaQueryList { 535 let tokens = Tokenizer::tokenize(input); 536 // Remove EOF 537 let tokens: Vec<Token> = tokens 538 .into_iter() 539 .filter(|t| !matches!(t, Token::Eof)) 540 .collect(); 541 parse_media_query_list(&tokens) 542 } 543 544 fn ctx(w: f32, h: f32) -> MediaContext { 545 MediaContext::from_viewport(w, h) 546 } 547 548 fn dark_ctx(w: f32, h: f32) -> MediaContext { 549 MediaContext { 550 viewport_width: w, 551 viewport_height: h, 552 color_scheme: ColorScheme::Dark, 553 reduced_motion: ReducedMotion::NoPreference, 554 } 555 } 556 557 // --- Parsing --- 558 559 #[test] 560 fn parse_media_type_screen() { 561 let mq = parse_mq("screen"); 562 assert_eq!(mq.queries.len(), 1); 563 assert_eq!(mq.queries[0].media_type, MediaType::Screen); 564 assert_eq!(mq.queries[0].modifier, Modifier::None); 565 assert!(mq.queries[0].features.is_empty()); 566 } 567 568 #[test] 569 fn parse_media_type_print() { 570 let mq = parse_mq("print"); 571 assert_eq!(mq.queries[0].media_type, MediaType::Print); 572 } 573 574 #[test] 575 fn parse_media_type_all() { 576 let mq = parse_mq("all"); 577 assert_eq!(mq.queries[0].media_type, MediaType::All); 578 } 579 580 #[test] 581 fn parse_not_modifier() { 582 let mq = parse_mq("not print"); 583 assert_eq!(mq.queries[0].modifier, Modifier::Not); 584 assert_eq!(mq.queries[0].media_type, MediaType::Print); 585 } 586 587 #[test] 588 fn parse_only_modifier() { 589 let mq = parse_mq("only screen"); 590 assert_eq!(mq.queries[0].modifier, Modifier::Only); 591 assert_eq!(mq.queries[0].media_type, MediaType::Screen); 592 } 593 594 #[test] 595 fn parse_min_width() { 596 let mq = parse_mq("(min-width: 768px)"); 597 assert_eq!(mq.queries[0].features.len(), 1); 598 assert_eq!( 599 mq.queries[0].features[0], 600 MediaFeature::Width(Comparison::Gte, 768.0) 601 ); 602 } 603 604 #[test] 605 fn parse_max_width() { 606 let mq = parse_mq("(max-width: 600px)"); 607 assert_eq!( 608 mq.queries[0].features[0], 609 MediaFeature::Width(Comparison::Lte, 600.0) 610 ); 611 } 612 613 #[test] 614 fn parse_screen_and_feature() { 615 let mq = parse_mq("screen and (max-width: 600px)"); 616 assert_eq!(mq.queries[0].media_type, MediaType::Screen); 617 assert_eq!(mq.queries[0].features.len(), 1); 618 assert_eq!( 619 mq.queries[0].features[0], 620 MediaFeature::Width(Comparison::Lte, 600.0) 621 ); 622 } 623 624 #[test] 625 fn parse_multiple_features() { 626 let mq = parse_mq("screen and (min-width: 400px) and (max-width: 800px)"); 627 assert_eq!(mq.queries[0].features.len(), 2); 628 assert_eq!( 629 mq.queries[0].features[0], 630 MediaFeature::Width(Comparison::Gte, 400.0) 631 ); 632 assert_eq!( 633 mq.queries[0].features[1], 634 MediaFeature::Width(Comparison::Lte, 800.0) 635 ); 636 } 637 638 #[test] 639 fn parse_comma_separated() { 640 let mq = parse_mq("screen, print"); 641 assert_eq!(mq.queries.len(), 2); 642 assert_eq!(mq.queries[0].media_type, MediaType::Screen); 643 assert_eq!(mq.queries[1].media_type, MediaType::Print); 644 } 645 646 #[test] 647 fn parse_range_gte() { 648 let mq = parse_mq("(width >= 768px)"); 649 assert_eq!( 650 mq.queries[0].features[0], 651 MediaFeature::Width(Comparison::Gte, 768.0) 652 ); 653 } 654 655 #[test] 656 fn parse_range_lt() { 657 let mq = parse_mq("(width < 600px)"); 658 assert_eq!( 659 mq.queries[0].features[0], 660 MediaFeature::Width(Comparison::Lt, 600.0) 661 ); 662 } 663 664 #[test] 665 fn parse_range_reversed() { 666 let mq = parse_mq("(768px <= width)"); 667 assert_eq!( 668 mq.queries[0].features[0], 669 MediaFeature::Width(Comparison::Gte, 768.0) 670 ); 671 } 672 673 #[test] 674 fn parse_double_range() { 675 let mq = parse_mq("(400px <= width <= 1200px)"); 676 assert_eq!(mq.queries[0].features.len(), 2); 677 assert_eq!( 678 mq.queries[0].features[0], 679 MediaFeature::Width(Comparison::Gte, 400.0) 680 ); 681 assert_eq!( 682 mq.queries[0].features[1], 683 MediaFeature::Width(Comparison::Lte, 1200.0) 684 ); 685 } 686 687 #[test] 688 fn parse_prefers_color_scheme_dark() { 689 let mq = parse_mq("(prefers-color-scheme: dark)"); 690 assert_eq!( 691 mq.queries[0].features[0], 692 MediaFeature::PrefersColorScheme(ColorScheme::Dark) 693 ); 694 } 695 696 #[test] 697 fn parse_prefers_color_scheme_light() { 698 let mq = parse_mq("(prefers-color-scheme: light)"); 699 assert_eq!( 700 mq.queries[0].features[0], 701 MediaFeature::PrefersColorScheme(ColorScheme::Light) 702 ); 703 } 704 705 #[test] 706 fn parse_prefers_reduced_motion() { 707 let mq = parse_mq("(prefers-reduced-motion: reduce)"); 708 assert_eq!( 709 mq.queries[0].features[0], 710 MediaFeature::PrefersReducedMotion(ReducedMotion::Reduce) 711 ); 712 } 713 714 #[test] 715 fn parse_height_features() { 716 let mq = parse_mq("(min-height: 500px)"); 717 assert_eq!( 718 mq.queries[0].features[0], 719 MediaFeature::Height(Comparison::Gte, 500.0) 720 ); 721 } 722 723 // --- Evaluation --- 724 725 #[test] 726 fn eval_screen_matches() { 727 let mq = parse_mq("screen"); 728 assert!(mq.evaluate(&ctx(800.0, 600.0))); 729 } 730 731 #[test] 732 fn eval_print_does_not_match() { 733 let mq = parse_mq("print"); 734 assert!(!mq.evaluate(&ctx(800.0, 600.0))); 735 } 736 737 #[test] 738 fn eval_not_print_matches() { 739 let mq = parse_mq("not print"); 740 assert!(mq.evaluate(&ctx(800.0, 600.0))); 741 } 742 743 #[test] 744 fn eval_min_width_matches() { 745 let mq = parse_mq("(min-width: 768px)"); 746 assert!(mq.evaluate(&ctx(1024.0, 768.0))); 747 assert!(!mq.evaluate(&ctx(500.0, 768.0))); 748 } 749 750 #[test] 751 fn eval_max_width_matches() { 752 let mq = parse_mq("(max-width: 600px)"); 753 assert!(mq.evaluate(&ctx(400.0, 300.0))); 754 assert!(!mq.evaluate(&ctx(800.0, 600.0))); 755 } 756 757 #[test] 758 fn eval_width_range() { 759 let mq = parse_mq("(width >= 768px)"); 760 assert!(mq.evaluate(&ctx(768.0, 600.0))); 761 assert!(mq.evaluate(&ctx(1024.0, 600.0))); 762 assert!(!mq.evaluate(&ctx(767.0, 600.0))); 763 } 764 765 #[test] 766 fn eval_double_range() { 767 let mq = parse_mq("(400px <= width <= 1200px)"); 768 assert!(mq.evaluate(&ctx(800.0, 600.0))); 769 assert!(!mq.evaluate(&ctx(300.0, 600.0))); 770 assert!(!mq.evaluate(&ctx(1300.0, 600.0))); 771 } 772 773 #[test] 774 fn eval_comma_or_semantics() { 775 let mq = parse_mq("screen, print"); 776 assert!(mq.evaluate(&ctx(800.0, 600.0))); 777 778 let mq2 = parse_mq("print, (min-width: 768px)"); 779 assert!(mq2.evaluate(&ctx(1024.0, 768.0))); 780 assert!(!mq2.evaluate(&ctx(500.0, 400.0))); 781 } 782 783 #[test] 784 fn eval_prefers_color_scheme_dark() { 785 let mq = parse_mq("(prefers-color-scheme: dark)"); 786 assert!(mq.evaluate(&dark_ctx(800.0, 600.0))); 787 assert!(!mq.evaluate(&ctx(800.0, 600.0))); 788 } 789 790 #[test] 791 fn eval_screen_and_min_width() { 792 let mq = parse_mq("screen and (min-width: 768px)"); 793 assert!(mq.evaluate(&ctx(1024.0, 768.0))); 794 assert!(!mq.evaluate(&ctx(500.0, 400.0))); 795 } 796 797 #[test] 798 fn eval_not_screen_does_not_match() { 799 let mq = parse_mq("not screen"); 800 assert!(!mq.evaluate(&ctx(800.0, 600.0))); 801 } 802 803 #[test] 804 fn eval_only_screen_matches() { 805 let mq = parse_mq("only screen"); 806 assert!(mq.evaluate(&ctx(800.0, 600.0))); 807 } 808 809 #[test] 810 fn eval_empty_query_list_matches() { 811 let mq = MediaQueryList { 812 queries: Vec::new(), 813 }; 814 assert!(mq.evaluate(&ctx(800.0, 600.0))); 815 } 816 817 #[test] 818 fn eval_all_type_matches() { 819 let mq = parse_mq("all"); 820 assert!(mq.evaluate(&ctx(800.0, 600.0))); 821 } 822 823 #[test] 824 fn eval_height_features() { 825 let mq = parse_mq("(min-height: 500px)"); 826 assert!(mq.evaluate(&ctx(800.0, 600.0))); 827 assert!(!mq.evaluate(&ctx(800.0, 400.0))); 828 } 829 830 #[test] 831 fn eval_multiple_and_features() { 832 let mq = parse_mq("screen and (min-width: 400px) and (max-width: 1200px)"); 833 assert!(mq.evaluate(&ctx(800.0, 600.0))); 834 assert!(!mq.evaluate(&ctx(300.0, 600.0))); 835 assert!(!mq.evaluate(&ctx(1300.0, 600.0))); 836 } 837 838 #[test] 839 fn eval_reduced_motion() { 840 let mq = parse_mq("(prefers-reduced-motion: reduce)"); 841 let reduced = MediaContext { 842 viewport_width: 800.0, 843 viewport_height: 600.0, 844 color_scheme: ColorScheme::Light, 845 reduced_motion: ReducedMotion::Reduce, 846 }; 847 assert!(mq.evaluate(&reduced)); 848 assert!(!mq.evaluate(&ctx(800.0, 600.0))); 849 } 850}